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
| WPF | Windows Forms | Co robi |
|---|---|---|
Minimum, Maximum | Minimum, Maximum | Zakres wartości suwaka lub paska. |
Value | Value | Aktualna wartość (w WPF typ double). |
TickFrequency | TickFrequency | Co ile jednostek pojawia się kreska na suwaku. |
IsSnapToTickEnabled | TickStyle | Czy wartość „przyciąga się” do kresek, czy zmienia płynnie. |
Orientation | Orientation | Suwak poziomy lub pionowy. |
IsIndeterminate (ProgressBar) | Style = Marquee | Animowany pasek bez znanego końca, gdy czas operacji jest nieznany. |
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.
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. |
Tick | Tick | Zdarzenie wywoływane cyklicznie. |
Start() / Stop() | Start() / Stop() | Uruchomienie i zatrzymanie odliczania. |
| tworzony w kodzie | przeciągany z Toolboxa | W WPF nie ma timera w przyborniku. |
5 · Wygląd
MainWindow.xaml
Slider i ProgressBar mają ten sam zakres 0 do 100. Przycisk ma Click, a zegar jest zwykłym TextBlock.
<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>
TickFrequency="10"iIsSnapToTickEnabled="True"Suwak skacze co 10 jednostek.ValueChangedZdarzenie wywoływane przy każdej zmianie pozycji suwaka.ProgressBarPasek bez zdarzeń: kod zmienia tylko jegoValue.
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.
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);
}
}
}
_zegar.Interval = TimeSpan.FromSeconds(1)Tick co sekundę._zegar.Tick += Zegar_TickPodpięcie metody do zdarzenia z poziomu kodu._zegar.Start()Bez tej linii timer w ogóle nie rusza.(int)SuwakJasnosci.ValueValuejest typemdouble. Rzutowanie naintusuwa miejsca po przecinku.async voidiawait Task.Delay(80)Czeka 80 ms bez blokowania okna. Zwykła pętla zThread.Sleepzamroził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.
// 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;
100.0 * (…) / calkowityLiczba100.0wymusza dzielenie zmiennoprzecinkowe. Przy samychintwynik byłby obcięty do całości.IsIndeterminateTryb pasku bez znanej wartości. Po jego wyłączeniu pasek znów używaValue.- Etykieta suwaka W
ValueChangedsprawdź, 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
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.
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
_zegar.Interval = TimeSpan.FromSeconds(1);
_zegar.Tick += Zegar_Tick;Zegar nigdy nie tyka.
_zegar.Interval = TimeSpan.FromSeconds(1);
_zegar.Tick += Zegar_Tick;
_zegar.Start();Samo utworzenie timera niczego nie uruchamia.
`Interval` jako liczba
_zegar.Interval = 1000;Błąd kompilatora: nie można przekonwertować int na TimeSpan.
_zegar.Interval = TimeSpan.FromMilliseconds(1000);W WPF Interval ma typ TimeSpan, a nie liczbę milisekund.
`Slider.Value` przypisane do `int` bez rzutowania
int procent = SuwakJasnosci.Value;Błąd kompilatora: nie można niejawnie przekonwertować double na int.
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.
Podsumowanie
Co warto zapamiętać
- Slider:
Minimum,Maximum,Value(typdouble), zdarzenieValueChanged. - ProgressBar:
Valuepokazuje postęp,IsIndeterminateoznacza czas nieznany. - DispatcherTimer:
Interval(typTimeSpan), zdarzenieTick,Start()iStop(). - Wątek interfejsu rysuje okno i wykonuje Twoje metody, więc nie wolno go blokować.
await Task.Delay(...)zamiastThread.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ę →