Dataczwartek, 13 sierpnia 2026 Czas18:53:01
← Aplikacje desktopowe (WPF)

Lekcja 8. Kontrolki III — Slider, ProgressBar, DispatcherTimer

Średni

Po 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

  1. Zmień interwał zegara z 1 sekundy na 500 milisekund (pół sekundy) i zaobserwuj różnicę.

  2. 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.

  3. 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 UIThread.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.ValueValue 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.