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.
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
TimeSpanw 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.
Pierwszy zegar
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.
Start, Stop, Interval
| Zapis | Co robi |
|---|---|
zegar.Start() | uruchamia zegar; pierwszy Tick po upływie Interval, nie od razu |
zegar.Stop() | zatrzymuje — kolejne Tick nie przyjdą |
zegar.IsEnabled | true, 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.
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:
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.
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ób | Działanie | Dokładność |
|---|---|---|
| liczenie tyknięć | sekundy-- w każdym Tick | błędy się sumują |
| różnica czasu | na starcie zapamiętaj koniec = DateTime.Now + czas; w Tick licz koniec - DateTime.Now | zawsze 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.
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:
// 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.
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.
egzamin 150 minut w trakcie — lista czasu zablokowana, aktywna pauza
<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>
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_Tickliczy 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óchTimeSpandajedouble). - Kolor wybiera operator
? :— czerwony, gdy zostało nie więcej niż 10% czasu. SelectionChangedlisty jest podpięte w konstruktorze po pierwszymUstawCzas— w XAML wywołałoby się przySelectedIndex="0", zanim powstaną pozostałe kontrolki.- Pozycja „1 minuta (test)” pozwala sprawdzić koniec odliczania bez czekania półtorej godziny.
Sprawdź trzy rzeczy
- 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.
- Zmień
Intervalna 5 sekund. Godzina i odliczanie zmieniają się skokami —Intervaldecyduje, jak często odświeżasz widok. - Zakomentuj
zegar.Start();w konstruktorze. Nic się nie dzieje — zegar bezStartnie tyka.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
new DispatcherTimer() | obiekt | Zegar wywołujący Tick w wątku okna (System.Windows.Threading). |
Interval | TimeSpan | Odstęp między tyknięciami. |
Tick | zdarzenie | Co Interval; parametr EventArgs. |
Start(), Stop(), IsEnabled | metody / bool | Uruchamianie i zatrzymywanie. |
TimeSpan.FromSeconds, FromMinutes, FromMilliseconds | TimeSpan | Tworzenie odcinka czasu. |
TotalSeconds, TotalMinutes | double | Cały odcinek w sekundach / minutach. |
Hours, Minutes, Seconds | int | Części odcinka. |
ToString(@"hh\:mm\:ss") | string | Odcinek jako „01:05:09”. |
TimeSpan.Zero | TimeSpan | Odcinek zerowy. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
zegar tworzony w metodzie Click | Każ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_Click | Każ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.Timer | Zmiana kontrolki w zdarzeniu kończy się wyjątkiem — to inny wątek. |
zostalo.Minutes zamiast TotalMinutes | Przy 1 h 5 min daje 5, a nie 65. |
ToString("hh:mm:ss") dla TimeSpan | FormatException — dwukropki muszą być poprzedzone \. |
MessageBox w Tick bez zatrzymania zegara | Zegar tyka dalej i otwiera kolejne okna komunikatu. Najpierw Stop() lub zmiana stanu. |
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.
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
booli wstawiaj „:” albo spację. - Formaty:
"dddd, d MMMM yyyy","hh:mm tt". Polskie ustawienia nie mają „AM/PM” — dopisz je sam na podstawieHour.
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").
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.
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.Nowprzy obu naciśnięciach. Zegar odświeża tylko cyfry. - Ignoruj powtórzenia przytrzymanej spacji (
e.IsRepeat).
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", przesuwanyMargin(lewy margines może być ujemny). - Szerokość napisu po ułożeniu:
ActualWidth. Napis nie może się zawijać.
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 zDateTime.Now.TimeOfDay. - „Teraz” bierz z jednej metody — w trybie testowym zwraca czas przyspieszony. Wtedy reszta programu nie musi wiedzieć o trybie.
Co trzeba zapamiętać
DispatcherTimerto pole klasy:Interval,Tick += …iStart()— raz, w konstruktorze.Tickdziała w wątku okna, więc może zmieniać kontrolki.TimeSpanto 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.