Lekcja 8. Kontrolki III — Slider, ProgressBar, DispatcherTimer
ŚredniPo co się tego uczymy?
Nie każda kontrolka służy do wpisywania tekstu — czasem potrzebujesz suwaka do wyboru wartości liczbowej, paska pokazującego postęp długiej operacji, albo mechanizmu, który sam, cyklicznie coś robi (np. odświeża zegar). Ta lekcja kończy przegląd kontrolek WPF, omawiając Slider, ProgressBar i DispatcherTimer.
Teoria
Slider (WPF) / TrackBar (Windows Forms) — suwak wartości liczbowej
| Właściwość WPF (Slider) | Odpowiednik Windows Forms (TrackBar) | Opis |
|---|---|---|
Minimum, Maximum |
Minimum, Maximum |
Zakres wartości, jakie może przyjąć suwak. |
Value |
Value |
Aktualna pozycja suwaka. |
TickFrequency |
TickFrequency |
Co ile jednostek pojawia się kreska (znacznik) na suwaku. |
IsSnapToTickEnabled |
TickStyle |
Czy wartość ma się "przyciągać" do najbliższego znacznika, czy przesuwać płynnie. |
Orientation |
Orientation |
Suwak poziomy lub pionowy. |
Windows Forms — reakcja na przesunięcie suwaka:
private void trackBar1_Scroll(object sender, EventArgs e)
{
panel1.BackColor = Color.FromArgb(trackBar1.Value, 0, 0);
}
WPF — ten sam efekt przez binding dwukierunkowy (Mode=TwoWay — zmiana suwaka aktualizuje właściwość, i odwrotnie):
<Slider Minimum="0" Maximum="255"
Value="{Binding R, Mode=TwoWay}"
TickFrequency="10"
IsSnapToTickEnabled="True" />
ProgressBar — pasek postępu
| Właściwość WPF | Odpowiednik Windows Forms | Opis |
|---|---|---|
Minimum, Maximum |
Minimum, Maximum |
Zakres wartości postępu (zwykle 0-100). |
Value |
Value |
Aktualny postęp. |
IsIndeterminate |
Style = Marquee |
Animowany, "nieskończony" pasek używany, gdy nie da się z góry określić czasu trwania operacji (np. trwające pobieranie o nieznanym rozmiarze). |
Aktualizacja paska w pętli z opóźnieniem (Windows Forms, składnia identyczna również w WPF):
private async void btnStart_Click(object sender, EventArgs e)
{
for (int i = 0; i <= 100; i += 10)
{
progressBar1.Value = i;
await Task.Delay(100);
}
}
Słowo kluczowe async/await jest tu KLUCZOWE — bez niego pętla for zablokowałaby cały interfejs użytkownika (okno "zamarzłoby") aż do jej zakończenia, bo aktualizacje UI i logika programu domyślnie działają na tym samym wątku.
Timer (Windows Forms) / DispatcherTimer (WPF) — cykliczne działania
Timer pozwala uruchamiać jakiś kod co określony odstęp czasu — bez pętli, bez blokowania interfejsu. Klasyczne zastosowanie: zegar na ekranie, automatyczne odświeżanie danych, animacje.
| WPF (DispatcherTimer) | Windows Forms (Timer) | Opis |
|---|---|---|
Interval (typu TimeSpan) |
Interval (w milisekundach, liczba całkowita) |
Co ile czasu ma się wykonać zdarzenie Tick. |
Tick |
Tick |
Zdarzenie wywoływane cyklicznie, co Interval. |
Start() / Stop() |
Start() / Stop() |
Uruchomienie i zatrzymanie odliczania. |
Windows Forms:
timer1.Interval = 1000;
timer1.Tick += (_, __) => lblCzas.Text = DateTime.Now.ToString("HH:mm:ss");
timer1.Start();
WPF — analogicznie, ale Interval jest typu TimeSpan, a timer tworzy się w kodzie (nie przeciąga z Toolboxa):
var zegar = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
zegar.Tick += (_, __) => Zegar = DateTime.Now.ToString("HH:mm:ss");
zegar.Start();
DispatcherTimer jest szczególny, bo — w przeciwieństwie do zwykłego System.Threading.Timer — jego zdarzenie Tick wykonuje się na tym samym wątku, co interfejs użytkownika (tzw. UI thread / Dispatcher). Dzięki temu można bezpiecznie zmieniać z jego wnętrza właściwości kontrolek bez dodatkowych zabezpieczeń wielowątkowości.
Schemat
Kiedy uzyc ktorej kontrolki:
Slider -> uzytkownik WYBIERA wartosc liczbowa (np. glosnosc, jasnosc)
ProgressBar -> PROGRAM pokazuje postep dlugiej operacji (uzytkownik nie ingeruje)
DispatcherTimer -> kod ma sie wykonywac CYKLICZNIE, samoczynnie, w tle
(np. zegar, automatyczne odswiezanie co X sekund)
Przykład z życia
Odtwarzacz muzyki w telefonie to jeden interfejs, a trzy z omawianych dziś kontrolek: Slider do przewijania utworu i regulacji głośności, ProgressBar (często zamaskowany pod wyglądem paska odtwarzania) pokazujący, ile utworu już minęło, oraz Timer odświeżający co sekundę wyświetlany aktualny czas odtwarzania.
MainWindow.xaml
<Window x:Class="KontrolkiApp3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Regulator jasności i zegar" Height="260" Width="360">
<StackPanel Margin="20">
<TextBlock Text="Jasność ekranu:" Margin="0,0,0,5"/>
<Slider x:Name="SuwakJasnosci"
Minimum="0" Maximum="100"
TickFrequency="10"
IsSnapToTickEnabled="True"
ValueChanged="SuwakJasnosci_ValueChanged"/>
<TextBlock x:Name="EtykietaJasnosc" Text="Wartość: 0%" Margin="0,5,0,20"/>
<TextBlock Text="Postęp zapisu:" Margin="0,0,0,5"/>
<ProgressBar x:Name="PasekPostepu" Minimum="0" Maximum="100" Height="20"/>
<Button Content="Rozpocznij zapis" Click="PrzyciskStart_Click" Margin="0,10,0,20"/>
<TextBlock x:Name="EtykietaZegar" FontSize="20" HorizontalAlignment="Center"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace KontrolkiApp3
{
public partial class MainWindow : Window
{
private readonly DispatcherTimer _zegar = new DispatcherTimer();
public MainWindow()
{
InitializeComponent();
_zegar.Interval = TimeSpan.FromSeconds(1);
_zegar.Tick += Zegar_Tick;
_zegar.Start();
}
private void Zegar_Tick(object? sender, EventArgs e)
{
EtykietaZegar.Text = DateTime.Now.ToString("HH:mm:ss");
}
private void SuwakJasnosci_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
EtykietaJasnosc.Text = $"Wartość: {(int)SuwakJasnosci.Value}%";
}
private async void PrzyciskStart_Click(object sender, RoutedEventArgs e)
{
for (int i = ; i <= 100; i += 5)
{
PasekPostepu.Value = i;
await Task.Delay(80);
}
}
}
}
Komentarz i wyjaśnienie kodu
Konstruktor tworzy DispatcherTimer, ustawia jego Interval na jedną sekundę, podpina metodę Zegar_Tick pod zdarzenie Tick i od razu wywołuje Start() — od tego momentu Zegar_Tick będzie wykonywać się samoczynnie, co sekundę, aż do zamknięcia okna, aktualizując EtykietaZegar aktualną godziną.
SuwakJasnosci_ValueChanged uruchamia się przy KAŻDEJ zmianie pozycji suwaka — rzutujemy Value (typu double) na int, żeby wyświetlić czystą liczbę procentów bez miejsc po przecinku.
PrzyciskStart_Click jest oznaczony słowem async, a wewnątrz pętli używamy await Task.Delay(80) zamiast zwykłego Thread.Sleep — dzięki temu w trakcie 80-milisekundowej przerwy okno POZOSTAJE responsywne (można je przesuwać, klikać inne kontrolki), a pasek postępu i tak aktualizuje się płynnie, krok po kroku.
Ćwiczenie samodzielne
Utwórz nowy projekt WPF z powyższym kodem. Uruchom aplikację i sprawdź: (1) czy zegar w dolnej etykiecie tyka co sekundę bez Twojej ingerencji; (2) czy przesuwanie suwaka aktualizuje etykietę z procentami "na żywo"; (3) czy kliknięcie "Rozpocznij zapis" animuje pasek postępu od 0 do 100, a w trakcie animacji nadal możesz przesuwać suwak (dowód, że interfejs nie jest zablokowany).
Zadania do pracy własnej
Zmień interwał zegara z 1 sekundy na 500 milisekund (pół sekundy) i zaobserwuj różnicę.
Dodaj drugi Slider (Głośność, zakres 0-10) i wyświetlaj jego wartość w osobnej etykiecie. Dodatkowo ustaw IsIndeterminate="True" na ProgressBar na czas trwania operacji 'zapisu', a po jej zakończeniu wróć do normalnego trybu z Value.
Zbuduj minutnik (stoper odliczający w dół): pole tekstowe do wpisania liczby sekund, przycisk 'Start' uruchamiający DispatcherTimer, który co sekundę zmniejsza pozostały czas i aktualizuje etykietę, oraz ProgressBar pokazujący, ile procent czasu już upłynęło. Gdy czas dojdzie do zera, timer ma się zatrzymać (Stop()), a etykieta wyświetlić 'Koniec czasu!'.
Typowe błędy
Użycie Thread.Sleep zamiast await Task.Delay w kodzie UI — Thread.Sleep całkowicie blokuje wątek interfejsu, okno "zamarza" i przestaje reagować na kliknięcia aż do zakończenia opóźnienia.
Zapomnienie o Start() po utworzeniu DispatcherTimer — sam obiekt timera nic nie robi, dopóki nie wywołasz Start(); to częsta literówka pomijana przy przepisywaniu przykładu.
Mylenie Interval w WPF (TimeSpan) z Windows Forms (int, milisekundy) — w WPF nie da się napisać Interval = 1000, trzeba jawnie utworzyć TimeSpan.FromSeconds(1) albo TimeSpan.FromMilliseconds(1000).
Brak rzutowania typu przy odczycie Slider.Value — Value jest typu double, więc bezpośrednie przypisanie do int bez rzutowania nie skompiluje się.
Nawiązanie do egzaminu zawodowego
To dopełnienie realizacji INF.04.5.3 pełnym zestawem kontrolek WPF wymaganych w podstawie programowej — łącznie z Slider, ProgressBar i mechanizmem cyklicznego wykonywania kodu (DispatcherTimer), niezbędnym przy każdej animacji, odświeżaniu danych w czasie rzeczywistym czy prostym stoperze/zegarze w aplikacji.