Aplikacje desktopowe · Kontrolki III

Slider, ProgressBar i DispatcherTimer: wartości, postęp i czas

Nie każda kontrolka służy do wpisywania tekstu. Czasem potrzebujesz suwaka, paska postępu albo mechanizmu, który sam co jakiś czas coś robi, na przykład odświeża zegar. Zobaczysz też, dlaczego okno potrafi „zamarznąć”.

  • około 45 minut
  • WPF · Slider · ProgressBar · DispatcherTimer
  • INF.04.5.3

Po tej lekcji: ustawić zakres i odczytać wartość Slider, animować ProgressBar, uruchomić cykliczne działanie DispatcherTimer oraz wyjaśnić różnicę między Task.Delay a Thread.Sleep.

Zacznijmy od odtwarzacza

Trzy kontrolki w jednym odtwarzaczu muzyki

Odtwarzacz w telefonie używa dokładnie tego, co dziś poznasz: Slider do przewijania utworu i głośności, ProgressBar pokazujący, ile utworu już minęło, i timer, który co sekundę odświeża wyświetlany czas.

Ta lekcja domyka przegląd kontrolek WPF wymaganych w podstawie programowej.

1 · Wartości liczbowe

Slider i ProgressBar: właściwości

WPFWindows FormsCo robi
Minimum, MaximumMinimum, MaximumZakres wartości suwaka lub paska.
ValueValueAktualna wartość (w WPF typ double).
TickFrequencyTickFrequencyCo ile jednostek pojawia się kreska na suwaku.
IsSnapToTickEnabledTickStyleCzy wartość „przyciąga się” do kresek, czy zmienia płynnie.
OrientationOrientationSuwak poziomy lub pionowy.
IsIndeterminate (ProgressBar)Style = MarqueeAnimowany pasek bez znanego końca, gdy czas operacji jest nieznany.
Slider w Windows Forms nazywa się TrackBar. ProgressBar ma w obu technologiach tę samą nazwę.

Laboratorium · ćwiczenie w przeglądarce, do którego odsyłają rozgrzewki na końcu lekcji

2 · Sprawdź w działaniu

Dlaczego okno zamarza, a czasem nie

Zegar tyka, bo działa DispatcherTimer. Kliknij „Rozpocznij zapis” i przesuwaj suwak w trakcie: okno reaguje. Potem kliknij „Zapis z Thread.Sleep” i spróbuj to samo. Tym razem zamarznie naprawdę, bo blokujemy ten sam wątek, który rysuje okno.

Ta interakcja wymaga włączonego JavaScriptu.

W kodzie okna **nigdy nie czekaj przez Thread.Sleep**. Użyj async i await Task.Delay(...), a okno pozostanie responsywne.

3 · Dlaczego tak się dzieje

Jeden wątek rysuje okno i wykonuje Twój kod

Interfejs użytkownika działa na jednym wątku, zwanym wątkiem UI. Ten sam wątek odpowiada za rysowanie okna, reakcję na kliknięcia i wykonywanie Twoich metod obsługi zdarzeń.

Gdy metoda trwa długo bez oddania sterowania (pętla z Thread.Sleep), okno nie ma kiedy się odświeżyć ani zareagować i wygląda na zawieszone. Słowo **await** oddaje sterowanie na czas oczekiwania, więc okno żyje, a po upływie czasu pętla wraca do pracy.

DispatcherTimer jest wygodny, bo jego zdarzenie Tick wykonuje się na tym samym wątku UI, więc możesz bezpiecznie zmieniać kontrolki w jego wnętrzu.

4 · Cykliczne działania

DispatcherTimer w WPF i Timer w Windows Forms

WPF (`DispatcherTimer`)Windows Forms (`Timer`)Co robi
Interval (typ TimeSpan)Interval (milisekundy, int)Co ile czasu ma się wykonać Tick.
TickTickZdarzenie wywoływane cyklicznie.
Start() / Stop()Start() / Stop()Uruchomienie i zatrzymanie odliczania.
tworzony w kodzieprzeciągany z ToolboxaW WPF nie ma timera w przyborniku.
W WPF nie napiszesz `Interval = 1000`. Trzeba jawnie użyć `TimeSpan.FromSeconds(1)` albo `TimeSpan.FromMilliseconds(1000)`.

5 · Wygląd

MainWindow.xaml

Slider i ProgressBar mają ten sam zakres 0 do 100. Przycisk ma Click, a zegar jest zwykłym TextBlock.

MainWindow.xamlxaml
<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="300" 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>
  1. TickFrequency="10" i IsSnapToTickEnabled="True" Suwak skacze co 10 jednostek.
  2. ValueChanged Zdarzenie wywoływane przy każdej zmianie pozycji suwaka.
  3. ProgressBar Pasek bez zdarzeń: kod zmienia tylko jego Value.

6 · Działanie

MainWindow.xaml.cs

Trzy mechanizmy w jednej klasie: timer uruchamiany w konstruktorze, reakcja na suwak i asynchroniczna pętla animująca pasek.

MainWindow.xaml.cscsharp
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 = 0; i <= 100; i += 5)
        {
            PasekPostepu.Value = i;
            await Task.Delay(80);
        }
    }
}
  1. _zegar.Interval = TimeSpan.FromSeconds(1) Tick co sekundę.
  2. _zegar.Tick += Zegar_Tick Podpięcie metody do zdarzenia z poziomu kodu.
  3. _zegar.Start() Bez tej linii timer w ogóle nie rusza.
  4. (int)SuwakJasnosci.Value Value jest typem double. Rzutowanie na int usuwa miejsca po przecinku.
  5. async void i await Task.Delay(80) Czeka 80 ms bez blokowania okna. Zwykła pętla z Thread.Sleep zamroziłaby cały interfejs.

Do zadań

Odliczanie, procent postępu i format czasu

Minutnik łączy DispatcherTimer, TextBox i ProgressBar. Poniżej szkielet, który uzupełnisz nazwami swoich kontrolek.

MainWindow.xaml.cscsharp
// Format zegara: .f to dziesiąte części sekundy
EtykietaCzas.Text = DateTime.Now.ToString("HH:mm:ss.f");
zegar.Interval = TimeSpan.FromMilliseconds(500);

// Minutnik
private int calkowity, pozostalo;

private void Start_Click(object sender, RoutedEventArgs e)
{
    if (!int.TryParse(PoleSekundy.Text, out calkowity) || calkowity <= 0)
    {
        MessageBox.Show("Podaj dodatnią liczbę sekund.");
        return;
    }
    pozostalo = calkowity;
    Pasek.Maximum = 100;
    Pasek.Value = 0;
    zegar.Start();
}

private void Zegar_Tick(object? sender, EventArgs e)
{
    pozostalo--;
    EtykietaCzas.Text = $"Pozostało: {pozostalo} s";
    Pasek.Value = 100.0 * (calkowity - pozostalo) / calkowity;   // procent, który już upłynął

    if (pozostalo <= 0)
    {
        zegar.Stop();
        EtykietaCzas.Text = "Koniec czasu!";
    }
}

// Czas nieznany: pasek „biegnie” bez wartości
Pasek.IsIndeterminate = true;
// ... po zakończeniu zapisu:
Pasek.IsIndeterminate = false;
Pasek.Value = 100;
  1. 100.0 * (…) / calkowity Liczba 100.0 wymusza dzielenie zmiennoprzecinkowe. Przy samych int wynik byłby obcięty do całości.
  2. IsIndeterminate Tryb pasku bez znanej wartości. Po jego wyłączeniu pasek znów używa Value.
  3. Etykieta suwaka W ValueChanged sprawdź, czy etykieta już istnieje (!= null), bo zdarzenie odpala się podczas wczytywania okna.

7 · Czego unikać

Cztery błędy z czasem i wartościami

`Thread.Sleep` w kodzie interfejsu
Tak nie
for (int i = 0; i <= 100; i += 5)
{
    PasekPostepu.Value = i;
    Thread.Sleep(80);
}

Okno zamarza. Pasek nie rusza do końca pętli, a potem od razu skacze do 100.

Tak
private async void PrzyciskStart_Click(object sender, RoutedEventArgs e)
{
    for (int i = 0; i <= 100; i += 5)
    {
        PasekPostepu.Value = i;
        await Task.Delay(80);
    }
}

await oddaje sterowanie wątkowi UI na czas oczekiwania, więc okno może się odświeżać.

Brak `Start()` timera
Tak nie
_zegar.Interval = TimeSpan.FromSeconds(1);
_zegar.Tick += Zegar_Tick;

Zegar nigdy nie tyka.

Tak
_zegar.Interval = TimeSpan.FromSeconds(1);
_zegar.Tick += Zegar_Tick;
_zegar.Start();

Samo utworzenie timera niczego nie uruchamia.

`Interval` jako liczba
Tak nie
_zegar.Interval = 1000;

Błąd kompilatora: nie można przekonwertować int na TimeSpan.

Tak
_zegar.Interval = TimeSpan.FromMilliseconds(1000);

W WPF Interval ma typ TimeSpan, a nie liczbę milisekund.

`Slider.Value` przypisane do `int` bez rzutowania
Tak nie
int procent = SuwakJasnosci.Value;

Błąd kompilatora: nie można niejawnie przekonwertować double na int.

Tak
int procent = (int)SuwakJasnosci.Value;

Value jest typu double. Utratę części ułamkowej trzeba zapisać jawnie.

8 · Sprawdź się

Którego mechanizmu użyjesz?

Cztery sytuacje z czasem i wartościami.

Ta interakcja wymaga włączonego JavaScriptu.

Podsumowanie

Co warto zapamiętać

  1. Slider: Minimum, Maximum, Value (typ double), zdarzenie ValueChanged.
  2. ProgressBar: Value pokazuje postęp, IsIndeterminate oznacza czas nieznany.
  3. DispatcherTimer: Interval (typ TimeSpan), zdarzenie Tick, Start() i Stop().
  4. Wątek interfejsu rysuje okno i wykonuje Twoje metody, więc nie wolno go blokować.
  5. await Task.Delay(...) zamiast Thread.Sleep(...) w kodzie okna.

Dalej na swoim koncie

Rozgrzewki, quiz i zadania są w biurku ucznia.

Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.

Zaloguj się →
Źródła i dalsza nauka