Przejdź do treści
School IT / Przestrzeń do nauki

DispatcherTimer — zegar w programie

Do tej pory program robił coś tylko wtedy, gdy użytkownik kliknął albo nacisnął klawisz. Zegar, stoper, odliczanie, ruchomy napis, gra, w której coś porusza się samo — to wszystko wymaga kodu, który wykonuje się regularnie, co określony czas. W WPF służy do tego DispatcherTimer. Nauczysz się go uruchamiać, zatrzymywać i mierzyć czas tak, żeby wynik był dokładny.

C# XAML DispatcherTimer Interval Tick TimeSpan 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Utworzysz zegar, ustawisz odstęp między „tyknięciami” i podepniesz metodę Tick.
  • Uruchomisz, zatrzymasz i wznowisz zegar przyciskami.
  • Policzysz i wyświetlisz odcinek czasu typu TimeSpan w postaci „01:25:07”.
  • Wyjaśnisz, dlaczego liczenie tyknięć jest niedokładne, i zmierzysz czas przez różnicę DateTime.
  • Przesuniesz kontrolkę o mały krok przy każdym tyknięciu — to podstawa animacji i gier.
TEORIA

Pierwszy zegar

MainWindow.xaml.cs
using System.Windows.Threading;     // DispatcherTimer

public partial class MainWindow : Window
{
    private readonly DispatcherTimer zegar = new DispatcherTimer();

    public MainWindow()
    {
        InitializeComponent();

        zegar.Interval = TimeSpan.FromSeconds(1);   // co ile „tyka”
        zegar.Tick += Zegar_Tick;                   // co ma się wtedy dziać
        zegar.Start();
    }

    private void Zegar_Tick(object? sender, EventArgs e)
    {
        Godzina.Text = DateTime.Now.ToString("HH:mm:ss");
    }
}

Zegar jest polem klasy okna — tworzymy go raz, w konstruktorze ustawiamy odstęp i metodę, i uruchamiamy. Od tej chwili co sekundę WPF wywołuje Zegar_Tick, tak jakby ktoś klikał niewidzialny przycisk. Zegara nie ma w XAML, dlatego zdarzenie podpinamy w kodzie przez +=. Parametr ma zwykły typ EventArgs, a sender zapisujemy jako object?, bo tak opisuje go WPF.

Dlaczego właśnie DispatcherTimer?

W .NET jest kilka klas o nazwie „Timer”. DispatcherTimer wywołuje Tick w tym samym wątku, w którym działa okno — dlatego w Tick możesz bez przeszkód zmieniać napisy i kontrolki. Inne zegary działają w tle i taka zmiana kończy się wyjątkiem.

TEORIA

Start, Stop, Interval

ZapisCo robi
zegar.Start()uruchamia zegar; pierwszy Tick po upływie Interval, nie od razu
zegar.Stop()zatrzymuje — kolejne Tick nie przyjdą
zegar.IsEnabledtrue, gdy zegar chodzi; przypisanie też uruchamia/zatrzymuje
Interval = TimeSpan.FromMilliseconds(100)10 tyknięć na sekundę — płynniejszy ruch, dokładniejszy stoper

Przycisk „Start/Stop” w jednym zwykle wygląda tak: if (zegar.IsEnabled) zegar.Stop(); else zegar.Start(); i zmiana napisu przycisku. Kilka niezależnych rzeczy dzieje się w różnym tempie? Utwórz kilka zegarów albo licz tyknięcia w jednym.

TEORIA

TimeSpan — odcinek czasu

DateTime to chwila („8.10.2026, 10:42”), a TimeSpan — odcinek czasu („1 h 25 min”). Różnica dwóch chwil jest odcinkiem:

MainWindow.xaml.cs
TimeSpan egzamin = TimeSpan.FromMinutes(150);        // 2:30:00
DateTime koniec = DateTime.Now + egzamin;            // chwila + odcinek = chwila
TimeSpan zostalo = koniec - DateTime.Now;            // chwila − chwila = odcinek

Info.Text = zostalo.ToString(@"hh\:mm\:ss");         // „02:29:59”
double sekundy = zostalo.TotalSeconds;               // 8999,4…
int minuty = zostalo.Minutes;                        // tylko część minutowa: 29
double ulamek = zostalo / egzamin;                   // 0,99993…

W formacie @"hh\:mm\:ss" dwukropki trzeba poprzedzić \, a @ sprawia, że \ nie jest znakiem specjalnym C#. Uwaga na różnicę: Minutes to część minutowa (0–59), a TotalMinutes — cały odcinek wyrażony w minutach.

TEORIA

Dokładne mierzenie czasu

Najprostszy minutnik zmniejsza licznik sekund o 1 przy każdym tyknięciu. Kłopot w tym, że Tick nigdy nie przychodzi idealnie co sekundę — zwykle odrobinę później, a gdy program jest zajęty innymi obliczeniami — nawet dużo później. Po godzinie licznik może się rozjechać z prawdziwym czasem o kilka sekund.

SposóbDziałanieDokładność
liczenie tyknięćsekundy-- w każdym Tickbłędy się sumują
różnica czasuna starcie zapamiętaj koniec = DateTime.Now + czas; w Tick licz koniec - DateTime.Nowzawsze zgodna z zegarem komputera

Zegar służy wtedy tylko do odświeżania napisu, a czas mierzy zegar systemowy. Dla gier, w których liczy się „krok”, liczenie tyknięć jest w porządku; dla stopera i minutnika — używaj różnicy czasu.

TEORIA

Ruch krok po kroku

Animacja w grze to po prostu bardzo częste, małe zmiany. Każde tyknięcie przesuwa kontrolkę o kilka pikseli — oko widzi płynny ruch. Na razie do przesuwania wystarczy Margin:

MainWindow.xaml.cs
// w XAML: w Grid stoi <Border x:Name="Pilka" Width="30" Height="30"
//         HorizontalAlignment="Left" VerticalAlignment="Top" .../>
private double x = 0;
private double krok = 4;

private void Zegar_Tick(object? sender, EventArgs e)    // Interval = 20 ms
{
    x += krok;
    if (x <= 0 || x >= Obszar.ActualWidth - Pilka.Width)
    {
        krok = -krok;                                   // odbicie od ściany
    }
    Pilka.Margin = new Thickness(x, 0, 0, 0);
}

Interwał 20 ms daje około 50 klatek na sekundę. Wygodniejsze przesuwanie po współrzędnych poznasz w lekcji 29.

PRZYKŁAD

Minutnik egzaminacyjny

Minutnik dla egzaminatora: wybór czasu z listy, duże odliczanie, pasek pozostałego czasu i godzina zakończenia. Pauza zatrzymuje odliczanie, Start je wznawia, Od nowa wraca do wybranego czasu. W ostatnich 10% czasu cyfry i pasek robią się czerwone, a po upływie czasu pojawia się komunikat. W rogu okna chodzi bieżąca godzina.

Minutnik egzaminacyjny–✕
10:42:17
Czas egzaminu:
150 minut▾
01:37:42
Koniec o 12:20:00
Start
Pauza
Od nowa

egzamin 150 minut w trakcie — lista czasu zablokowana, aktywna pauza

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja27.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Minutnik egzaminacyjny" SizeToContent="Height" Width="460"
        ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">

    <StackPanel Margin="20">
        <TextBlock x:Name="Zegar" FontSize="14" Foreground="DimGray" HorizontalAlignment="Right"/>

        <StackPanel Orientation="Horizontal" Margin="0,6,0,0">
            <TextBlock Text="Czas egzaminu:" VerticalAlignment="Center" Margin="0,0,8,0"/>
            <ComboBox x:Name="Czas" Width="140" SelectedIndex="0">
                <ComboBoxItem Content="1 minuta (test)" Tag="1"/>
                <ComboBoxItem Content="45 minut" Tag="45"/>
                <ComboBoxItem Content="90 minut" Tag="90"/>
                <ComboBoxItem Content="150 minut" Tag="150"/>
            </ComboBox>
        </StackPanel>

        <TextBlock x:Name="Pozostalo" Text="00:01:00" FontSize="56" FontFamily="Consolas"
                   HorizontalAlignment="Center" Margin="0,16,0,6"/>
        <ProgressBar x:Name="Pasek" Height="14" Maximum="1" Value="1" Foreground="SeaGreen"/>
        <TextBlock x:Name="Koniec" HorizontalAlignment="Center" Margin="0,6,0,0" Foreground="DimGray"/>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,16,0,0">
            <Button x:Name="Start" Content="Start" Width="90" Height="32" Margin="0,0,8,0" Click="Start_Click"/>
            <Button x:Name="Pauza" Content="Pauza" Width="90" Height="32" Margin="0,0,8,0"
                    IsEnabled="False" Click="Pauza_Click"/>
            <Button Content="Od nowa" Width="90" Height="32" Click="OdNowa_Click"/>
        </StackPanel>
    </StackPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;

namespace Lekcja27;

public partial class MainWindow : Window
{
    // zegar „tyka” co pół sekundy i wywołuje metodę Zegar_Tick
    private readonly DispatcherTimer zegar = new DispatcherTimer();

    private TimeSpan calyCzas;        // ile trwa egzamin
    private TimeSpan zostalo;         // ile zostało w chwili pauzy / startu
    private DateTime koniec;          // kiedy upłynie czas (gdy zegar chodzi)
    private bool odliczanie = false;

    public MainWindow()
    {
        InitializeComponent();

        zegar.Interval = TimeSpan.FromMilliseconds(500);
        zegar.Tick += Zegar_Tick;
        zegar.Start();                // zegar działa cały czas — pokazuje też godzinę

        UstawCzas();
        Czas.SelectionChanged += Czas_SelectionChanged;
    }

    private void Czas_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        UstawCzas();
    }

    private void Start_Click(object sender, RoutedEventArgs e)
    {
        koniec = DateTime.Now + zostalo;
        odliczanie = true;
        UstawPrzyciski();
        PokazStan();
    }

    private void Pauza_Click(object sender, RoutedEventArgs e)
    {
        zostalo = koniec - DateTime.Now;
        odliczanie = false;
        UstawPrzyciski();
        PokazStan();
    }

    private void OdNowa_Click(object sender, RoutedEventArgs e)
    {
        UstawCzas();
    }

    // Wywoływana przez zegar co 0,5 s
    private void Zegar_Tick(object? sender, EventArgs e)
    {
        Zegar.Text = DateTime.Now.ToString("HH:mm:ss");

        if (!odliczanie)
        {
            return;
        }

        zostalo = koniec - DateTime.Now;

        if (zostalo <= TimeSpan.Zero)
        {
            zostalo = TimeSpan.Zero;
            odliczanie = false;
            UstawPrzyciski();
            PokazStan();
            Start.IsEnabled = false;
            MessageBox.Show("Koniec czasu! Zapisz pracę i odłóż długopis.", "Minutnik",
                            MessageBoxButton.OK, MessageBoxImage.Information);
            return;
        }

        PokazStan();
    }

    private void UstawCzas()
    {
        ComboBoxItem pozycja = (ComboBoxItem)Czas.SelectedItem;
        calyCzas = TimeSpan.FromMinutes(int.Parse($"{pozycja.Tag}"));
        zostalo = calyCzas;
        odliczanie = false;
        UstawPrzyciski();
        PokazStan();
    }

    private void UstawPrzyciski()
    {
        Start.IsEnabled = !odliczanie;
        Pauza.IsEnabled = odliczanie;
        Czas.IsEnabled = !odliczanie;
    }

    private void PokazStan()
    {
        Pozostalo.Text = zostalo.ToString(@"hh\:mm\:ss");
        Pasek.Value = zostalo / calyCzas;          // ułamek 0–1

        // ostatnie 10% czasu na czerwono
        Brush kolor = zostalo.TotalSeconds <= calyCzas.TotalSeconds * 0.1 ? Brushes.Firebrick : Brushes.SeaGreen;
        Pozostalo.Foreground = kolor;
        Pasek.Foreground = kolor;

        Koniec.Text = odliczanie ? $"Koniec o {koniec:HH:mm:ss}" : "Zatrzymany";
    }
}

Co dzieje się po kolei

  • Jeden zegar tyka co 0,5 s przez cały czas działania programu. W każdym tyknięciu odświeża godzinę w rogu, a gdy trwa odliczanie — także pozostały czas.
  • Stan minutnika opisują cztery pola: cały czas egzaminu, czas pozostały, chwila końca i to, czy odliczanie trwa.
  • Start wylicza chwilę końca: teraz + pozostały czas. Pauza robi odwrotnie: zapamiętuje, ile zostało do końca. Dzięki temu pauza może trwać dowolnie długo.
  • Zegar_Tick liczy pozostały czas jako różnicę „koniec − teraz” — minutnik się nie rozjeżdża, nawet jeśli tyknięcia się spóźniają.
  • Pasek ma zakres 0–1, a wartością jest ułamek zostalo / calyCzas (dzielenie dwóch TimeSpan daje double).
  • Kolor wybiera operator ? : — czerwony, gdy zostało nie więcej niż 10% czasu.
  • SelectionChanged listy jest podpięte w konstruktorze po pierwszym UstawCzas — w XAML wywołałoby się przy SelectedIndex="0", zanim powstaną pozostałe kontrolki.
  • Pozycja „1 minuta (test)” pozwala sprawdzić koniec odliczania bez czekania półtorej godziny.

Sprawdź trzy rzeczy

  1. Uruchom odliczanie, kliknij Pauza, odczekaj 10 sekund i kliknij Start. Odliczanie rusza od tej samej wartości — pauza zapamiętała pozostały czas, a nowa chwila końca jest liczona od teraz.
  2. Zmień Interval na 5 sekund. Godzina i odliczanie zmieniają się skokami — Interval decyduje, jak często odświeżasz widok.
  3. Zakomentuj zegar.Start(); w konstruktorze. Nic się nie dzieje — zegar bez Start nie tyka.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
new DispatcherTimer()obiektZegar wywołujący Tick w wątku okna (System.Windows.Threading).
IntervalTimeSpanOdstęp między tyknięciami.
TickzdarzenieCo Interval; parametr EventArgs.
Start(), Stop(), IsEnabledmetody / boolUruchamianie i zatrzymywanie.
TimeSpan.FromSeconds, FromMinutes, FromMillisecondsTimeSpanTworzenie odcinka czasu.
TotalSeconds, TotalMinutesdoubleCały odcinek w sekundach / minutach.
Hours, Minutes, SecondsintCzęści odcinka.
ToString(@"hh\:mm\:ss")stringOdcinek jako „01:05:09”.
TimeSpan.ZeroTimeSpanOdcinek zerowy.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
zegar tworzony w metodzie ClickKażde kliknięcie tworzy nowy zegar — po trzech kliknięciach odliczanie biegnie trzy razy szybciej. Zegar jest polem klasy, tworzonym raz.
zegar.Tick += … w Start_ClickKażde kliknięcie dopina metodę jeszcze raz — Tick wywołuje ją wielokrotnie. Podpinaj w konstruktorze.
brak Start()Zegar nie tyka.
System.Threading.Timer albo System.Timers.TimerZmiana kontrolki w zdarzeniu kończy się wyjątkiem — to inny wątek.
zostalo.Minutes zamiast TotalMinutesPrzy 1 h 5 min daje 5, a nie 65.
ToString("hh:mm:ss") dla TimeSpanFormatException — dwukropki muszą być poprzedzone \.
MessageBox w Tick bez zatrzymania zegaraZegar tyka dalej i otwiera kolejne okna komunikatu. Najpierw Stop() lub zmiana stanu.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Czas mierz przez różnicę DateTime, a zegar wykorzystuj do odświeżania. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Zegar na korytarz★☆☆

Duży zegar cyfrowy „10:42”, pod nim sekundy mniejszą czcionką i data z dniem tygodnia („czwartek, 8 października 2026”). Dwukropek między godziną a minutami miga co pół sekundy. Pole wyboru Format 12-godzinny zmienia wyświetlanie na „10:42 AM”. Okno na czarnym tle z jasnozielonymi cyframi.

pokaż wskazówkę
  • Migający dwukropek: przy każdym tyknięciu co 500 ms przełączaj pole bool i wstawiaj „:” albo spację.
  • Formaty: "dddd, d MMMM yyyy", "hh:mm tt". Polskie ustawienia nie mają „AM/PM” — dopisz je sam na podstawie Hour.
ZAD 2Stoper z okrążeniami★☆☆

Stoper na lekcję WF: wyświetlacz „00:01:23,4” (dziesiąte części sekundy), przyciski Start/Stop (jeden przycisk zmieniający napis), Okrążenie (dopisuje na górę listy „Okr. 3 — 00:00:41,2 (razem 00:02:05,9)”) i Zeruj (aktywny tylko przy zatrzymanym stoperze). Najszybsze okrążenie na liście oznacz gwiazdką.

pokaż wskazówkę
  • Stoper jak minutnik, tylko „w drugą stronę”: zapamiętaj chwilę startu i czas zebrany przed pauzą.
  • Dziesiąte części: ToString(@"hh\:mm\:ss\,f").
ZAD 3Sygnalizacja świetlna★★☆

Sygnalizator z trzech okrągłych świateł (ramki z dużym CornerRadius) w ciemnej obudowie. Cykl: czerwone 5 s → czerwone i żółte 2 s → zielone 5 s → żółte 2 s → od nowa. Zgaszone światło jest ciemnoszare. Pod sygnalizatorem napis z fazą i liczbą sekund do zmiany. Pole wyboru Tryb nocny wyłącza cykl i miga samym żółtym co sekundę. Przycisk Pieszy skraca bieżące zielone do 2 s.

pokaż wskazówkę
  • Fazy trzymaj w tablicach: czas trwania i które światła się palą (np. bool[,] 4 × 3). Numer fazy zwiększaj „w kółko”.
  • Zapamiętaj chwilę końca bieżącej fazy — zegar co 100 ms sprawdza, czy już minęła.
ZAD 4Zatrzymaj na 5,00★★☆

Gra na wyczucie czasu. Po naciśnięciu Spacji stoper rusza i widać go przez pierwsze 2 sekundy, potem cyfry znikają („?.??”). Gracz naciska Spację jeszcze raz, gdy uważa, że minęło dokładnie 5 sekund. Program pokazuje wynik („5,13 s — różnica 0,13 s”), ocenę słowną (do 0,05 s — „Perfekcyjnie!”, do 0,2 s — „Świetnie” itd.) i najlepszy wynik sesji. Na poziomie trudnym cyfry znikają już po 1 sekundzie, a cel to 10 s.

pokaż wskazówkę
  • Do pomiaru czasu reakcji zegar nie jest nawet potrzebny — wystarczy DateTime.Now przy obu naciśnięciach. Zegar odświeża tylko cyfry.
  • Ignoruj powtórzenia przytrzymanej spacji (e.IsRepeat).
ZAD 5Ruchomy pasek wiadomości★★☆

Pasek u dołu okna, po którym od prawej do lewej przesuwa się komunikat (jak pasek informacyjny w telewizji). Gdy napis całkowicie zniknie z lewej strony, pojawia się znów z prawej — z następnym komunikatem z listy. Suwak zmienia prędkość, najechanie myszą na pasek zatrzymuje przesuwanie, a pole tekstowe z przyciskiem Dodaj dopisuje nowy komunikat do kolejki.

pokaż wskazówkę
  • Napis w ramce z ClipToBounds="True", przesuwany Margin (lewy margines może być ujemny).
  • Szerokość napisu po ułożeniu: ActualWidth. Napis nie może się zawijać.
ZAD 6Dzwonki szkolne★★★

Tablica informacyjna z planem dzwonków (co najmniej 8 lekcji z godzinami w tablicach). Program na bieżąco pokazuje: czy trwa lekcja (która) czy przerwa, ile zostało do najbliższego dzwonka (odliczanie „00:12:47”) i pasek postępu bieżącej lekcji lub przerwy. Lista godzin lekcyjnych z wyróżnieniem bieżącej pozycji. Przed pierwszą lekcją: „Zajęcia zaczynają się o 8:00”, po ostatniej: „Koniec zajęć”. Do testów dodaj pole wyboru Tryb testowy, w którym czas płynie 60 razy szybciej, licząc od 7:55.

pokaż wskazówkę
  • Godziny zapisz jako TimeSpan (np. new TimeSpan(8, 45, 0)) i porównuj z DateTime.Now.TimeOfDay.
  • „Teraz” bierz z jednej metody — w trybie testowym zwraca czas przyspieszony. Wtedy reszta programu nie musi wiedzieć o trybie.
PODSUMOWANIE

Co trzeba zapamiętać

  • DispatcherTimer to pole klasy: Interval, Tick += … i Start() — raz, w konstruktorze.
  • Tick działa w wątku okna, więc może zmieniać kontrolki.
  • TimeSpan to odcinek czasu; chwila − chwila = odcinek; format @"hh\:mm\:ss".
  • Tyknięcia nie są dokładne — czas mierz różnicą DateTime, a zegarem tylko odświeżaj widok.
  • Ruch to mała zmiana pozycji przy każdym tyknięciu; interwał 20 ms daje płynną animację.

Dokumentacja: Microsoft Learn — DispatcherTimer.

Postęp zapisuje się w tej przeglądarce.