ProgressBar – pasek postępu
Poznasz ProgressBar – kontrolkę pokazującą postęp długiej operacji. Nauczysz się aktualizować go za pomocą DispatcherTimer oraz poznasz podstawy async/await – wyjaśnione od zera, najprościej jak można.
Czym jest ProgressBar?
ProgressBar to pasek, który wizualnie pokazuje postęp jakiejś operacji – pobierania pliku, instalacji programu, przetwarzania danych. Wypełnia się od lewej do prawej, w miarę jak operacja się kończy.
<ProgressBar x:Name="pbPostep" Width="300" Height="25" /> <!-- Domyślnie: Minimum=0, Maximum=100, Value=0 -->
Minimum, Maximum, Value
Te trzy właściwości działają identycznie jak w Sliderze – określają zakres i aktualny postęp.
| Właściwość | Co określa | Domyślnie |
|---|---|---|
Minimum | Wartość przy 0% wypełnienia | 0 |
Maximum | Wartość przy 100% wypełnienia | 100 |
Value | Aktualny postęp | 0 |
<ProgressBar x:Name="pbPostep" Minimum="0" Maximum="100" Value="50" Width="300" Height="25" />
// Ustawienie konkretnej wartości pbPostep.Value = 75; // Zwiększanie o krok pbPostep.Value += 10; // Wyświetlenie procentów w TextBlock lblProcent.Text = $"{pbPostep.Value:F0}%";
IsIndeterminate – nieznany czas trwania
Czasem nie wiesz ile dokładnie potrwa operacja
(np. łączenie z serwerem, którego czas odpowiedzi jest nieznany).
W takim przypadku ustaw IsIndeterminate="True" –
ProgressBar pokaże animację „w nieskończoność” (przesuwający się
prostokąt), bez konkretnej wartości procentowej.
<ProgressBar x:Name="pbLadowanie" IsIndeterminate="True" Width="300" Height="25" /> <!-- Value jest ignorowane gdy IsIndeterminate="True" -->
| Sytuacja | Tryb |
|---|---|
| Wczytywanie 1000 elementów z listy (znana liczba) | Zwykły, z Value |
| Łączenie z serwerem (czas nieznany) | IsIndeterminate="True" |
| Pobieranie pliku o znanym rozmiarze | Zwykły, z Value |
| Sprawdzanie aktualizacji aplikacji | IsIndeterminate="True" |
Możesz też przełączać tryb dynamicznie z C# – np. pokazać nieokreślony postęp na początku operacji, a potem przejść do konkretnych wartości, gdy poznasz całkowity rozmiar zadania.
pbPostep.IsIndeterminate = true; // pokaż animację "w nieskończoność" // ... gdy poznasz dokładny postęp: pbPostep.IsIndeterminate = false; pbPostep.Value = 0;
Aktualizacja z DispatcherTimer
DispatcherTimer – znany już z tematu o Canvas i animacjach –
to dobry sposób na płynne wypełnianie ProgressBar krok po kroku,
gdy operacja nie jest „prawdziwie” długotrwała (np. symulacja postępu
bez rzeczywistego oczekiwania na coś zewnętrznego).
<StackPanel Margin="20"> <ProgressBar x:Name="pbPostep" Minimum="0" Maximum="100" Height="25" /> <Button Content="Start" Margin="0,10,0,0" Click="btnStart_Click" /> </StackPanel>
DispatcherTimer _timer; private void btnStart_Click(object sender, RoutedEventArgs e) { pbPostep.Value = 0; _timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromMilliseconds(100); _timer.Tick += Timer_Tick; _timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { pbPostep.Value += 2; // +2% co 100ms = pełny pasek po 5 sekundach if (pbPostep.Value >= pbPostep.Maximum) { _timer.Stop(); // zatrzymujemy timer po dojściu do 100% MessageBox.Show("Zakończono!"); } }
DispatcherTimer działa „w tle” – jego Tick odpala się
okresowo, ale między wywołaniami okno normalnie reaguje na kliknięcia,
przesuwanie itd. To dobre rozwiązanie dla animacji i symulowanego postępu.
Nie nadaje się jednak do prawdziwie długich operacji
(pobieranie pliku, zapytanie do serwera) – do tego służy async/await,
które poznasz w następnej sekcji.
Problem: zamrożone okno
Wyobraź sobie, że masz operację, która trwa naprawdę długo – np. wczytywanie dużego pliku albo czekanie na odpowiedź serwera. Jeśli napiszesz to w najprostszy, „naiwny” sposób, stanie się coś złego.
private void btnStart_Click(object sender, RoutedEventArgs e) { for (int i = 0; i <= 100; i++) { pbPostep.Value = i; System.Threading.Thread.Sleep(50); // "czekaj" 50 milisekund } // Pasek "zaktualizuje się" dopiero na samym końcu pętli! Przez 5 sekund okno jest CAŁKOWICIE zamrożone – nie reaguje na kliknięcia, nie można go przesunąć, Windows pokazuje "Nie odpowiada" --> }
Aplikacja WPF ma jeden główny „wątek” odpowiedzialny za rysowanie
okna i reagowanie na kliknięcia – nazywa się to UI thread
(wątek interfejsu użytkownika). Kiedy wywołujesz Thread.Sleep()
albo robisz coś, co długo trwa, ten jedyny wątek jest zajęty –
nie może w tym czasie przerysować okna ani obsłużyć kliknięcia myszką.
Stąd „zamrożenie”.
To właśnie problem, który rozwiązuje async/await – mechanizm pozwalający „czekać” na coś długiego, bez zamrażania okna.
async i await – wyjaśnienie od zera
Analogia z kuchnią
Wstawiasz wodę na makaron. Zamiast stać i wpatrywać się
w garnek czekając aż się zagrzeje (to byłby Thread.Sleep –
„zamrażasz się” i nic innego nie robisz), robisz coś
innego w tym czasie – kroisz warzywa na sałatkę.
Kiedy woda się zagrzeje (zagwizdze czajnik), wracasz do niej.
To jest właśnie async/await: „zacznij długą operację,
a w czasie gdy ona trwa, zajmij się czymś innym (np. odpowiadaniem
na kliknięcia użytkownika). Gdy operacja się zakończy, wróć
i dokończ swoją pracę”.
Słowo async – „ta metoda może czekać bez blokowania”
Słowo async dodajesz przed metodą, żeby powiedzieć
kompilatorowi: „w tej metodzie będę używać await”.
To jak etykieta na drzwiach – informuje, że wewnątrz odbywa się
coś specjalnego.
private async void btnStart_Click(object sender, RoutedEventArgs e) { // "async" w sygnaturze metody – pozwala używać "await" w środku }
Samo dodanie async nie sprawia,
że metoda działa „w tle” albo jest szybsza. To tylko zgoda
kompilatora na użycie await wewnątrz tej metody.
Cała magia dzieje się przy await.
Słowo await – „poczekaj tutaj, ale nie blokuj okna”
await stawiasz przed długą operacją.
Mówi: „zacznij tę operację, i jeśli ona jeszcze trwa –
oddaj kontrolę z powrotem do okna (żeby mogło
reagować na kliknięcia), a gdy operacja się zakończy,
wróć tutaj i wykonaj resztę kodu metody”.
private async void btnStart_Click(object sender, RoutedEventArgs e) { lblStatus.Text = "Zaczynam..."; // 1. To wykona się od razu await Task.Delay(3000); // 2. "Czekaj" 3 sekundy, ale okno żyje normalnie! lblStatus.Text = "Zakończono!"; // 3. To wykona się po 3 sekundach }
1. Klikasz przycisk – lblStatus.Text = "Zaczynam..."
wykonuje się natychmiast.
2. Dochodzimy do await Task.Delay(3000) – metoda
„zawiesza się” w tym miejscu i oddaje kontrolę oknu.
Okno żyje normalnie – możesz klikać inne przyciski, przesuwać okno.
3. Po 3 sekundach kod „wraca” do miejsca, w którym przerwał,
i wykonuje lblStatus.Text = "Zakończono!".
Task.Delay(3000) to specjalna metoda, która „czeka”
3000 milisekund (3 sekundy) bez blokowania niczego –
w przeciwieństwie do Thread.Sleep(3000), które blokuje cały wątek.
Używamy jej w nauce do symulowania długich operacji
(w prawdziwych aplikacjach await stawia się przed
np. pobieraniem danych z internetu albo odczytem dużego pliku).
| Thread.Sleep(3000) | await Task.Delay(3000) | |
|---|---|---|
| Okno reaguje w tym czasie? | ❌ Nie – zamrożone | ✅ Tak – normalnie |
| Wymaga słowa async w metodzie? | Nie | ✅ Tak |
| Kiedy używać | Praktycznie nigdy w WPF | Symulacja długiej operacji, prawdziwe długie operacje |
Task – co to za typ?
Task reprezentuje „obietnicę, że coś się wykona” –
operację, która dzieje się w tle i kiedyś się zakończy.
Na początku wystarczy Ci wiedzieć, że metody, przed którymi
stawiasz await, zwracają Task
(albo Task<T>, jeśli operacja ma zwrócić jakąś wartość).
// Task – operacja, która nic nie zwraca (tylko się wykonuje) await Task.Delay(1000); // Task<T> – operacja, która zwraca wartość typu T string wynik = await PobierzDaneZInternetu(); // Task<string>
Na tym etapie nauki najważniejsze jest używanie
await z gotowymi metodami (jak Task.Delay).
Pisanie własnych metod zwracających Task to temat
na kolejne zajęcia (w bloku zaawansowanym kursu).
ProgressBar z async/await
Połączmy wszystko: pętla, która krok po kroku zwiększa wartość
ProgressBar, a między krokami robi krótkie await Task.Delay() –
dzięki temu okno nie zamraża się, a użytkownik widzi
płynnie wypełniający się pasek.
<StackPanel Margin="20"> <ProgressBar x:Name="pbPostep" Minimum="0" Maximum="100" Height="25" /> <TextBlock x:Name="lblStatus" Text="Gotowy" Margin="0,8,0,0" /> <Button x:Name="btnStart" Content="Start" Margin="0,10,0,0" Click="btnStart_Click" /> </StackPanel>
private async void btnStart_Click(object sender, RoutedEventArgs e) { pbPostep.Value = 0; lblStatus.Text = "Przetwarzanie..."; for (int i = 0; i <= 100; i += 2) { pbPostep.Value = i; // aktualizujemy pasek await Task.Delay(50); // "czekaj" 50ms bez zamrażania okna } lblStatus.Text = "Zakończono!"; }
Różnica między „złym” kodem z sekcji 5 a tym jest tylko jedna:
zamiast Thread.Sleep(50) mamy await Task.Delay(50)
(i metoda ma dodane async). To wystarczy, żeby okno
przestało się zamrażać – pasek wypełnia się płynnie,
a okno cały czas reaguje na kliknięcia i przesuwanie.
Blokowanie przycisków na czas operacji
Skoro okno nie zamraża się podczas await, użytkownik
może kliknąć przycisk „Start” kilka razy zanim
operacja się zakończy! Trzeba to zablokować ręcznie –
ustawiając IsEnabled = false na czas trwania operacji.
private async void btnStart_Click(object sender, RoutedEventArgs e) { btnStart.IsEnabled = false; // blokujemy przed startem pbPostep.Value = 0; lblStatus.Text = "Przetwarzanie..."; for (int i = 0; i <= 100; i += 2) { pbPostep.Value = i; await Task.Delay(50); } lblStatus.Text = "Zakończono!"; btnStart.IsEnabled = true; // odblokowujemy po zakończeniu }
Zawsze gdy używasz async/await do operacji wywołanej
przyciskiem: zablokuj przycisk na początku
(IsEnabled = false), wykonaj operację,
odblokuj na końcu (IsEnabled = true).
To zapobiega przypadkowemu wielokrotnemu uruchomieniu tej samej akcji.
Praktyczny przykład – pobieranie plików (symulacja)
<StackPanel Margin="20" Width="320"> <TextBlock Text="Pobieranie plików" FontSize="16" FontWeight="Bold" Margin="0,0,0,12" /> <ProgressBar x:Name="pbPostep" Minimum="0" Maximum="100" Height="22" /> <TextBlock x:Name="lblPlik" Text="Gotowy do pobierania" Margin="0,8,0,0" FontSize="12" Foreground="Gray" /> <Button x:Name="btnPobierz" Content="Pobierz pliki" Margin="0,16,0,0" Click="btnPobierz_Click" /> </StackPanel>
private readonly string[] _pliki = { "raport.pdf", "zdjecie1.jpg", "zdjecie2.jpg", "prezentacja.pptx", "dane.xlsx" }; private async void btnPobierz_Click(object sender, RoutedEventArgs e) { btnPobierz.IsEnabled = false; pbPostep.Value = 0; for (int i = 0; i < _pliki.Length; i++) { lblPlik.Text = $"Pobieranie: {_pliki[i]}..."; // Symulujemy pobieranie pliku (1 sekunda na plik) await Task.Delay(1000); // Obliczamy procent na podstawie liczby ukończonych plików double procent = (i + 1) * 100.0 / _pliki.Length; pbPostep.Value = procent; } lblPlik.Text = $"Pobrano {_pliki.Length} plików!"; btnPobierz.IsEnabled = true; }
Częste błędy
❌ Błąd 1: Thread.Sleep zamiast await Task.Delay
❌ Okno zamraża się
private void btn_Click(...)
{
Thread.Sleep(3000);
// Okno nie odpowiada przez 3s!
}
✅ async/await
private async void btn_Click(...)
{
await Task.Delay(3000);
// Okno działa normalnie
}
❌ Błąd 2: await bez async w sygnaturze metody
❌ Błąd kompilacji
private void btn_Click(object sender, ...)
{
await Task.Delay(1000);
// Błąd! Brak "async"
// w sygnaturze metody
}
✅ Dodaj async
private async void btn_Click(object sender, ...)
{
await Task.Delay(1000);
}
❌ Błąd 3: Niezablokowany przycisk – wielokrotne kliknięcie
❌ Można odpalić operację wiele razy naraz
private async void btnStart_Click(...)
{
// Brak IsEnabled = false –
// szybkie klikanie odpala
// kilka kopii tej samej operacji!
for (int i = 0; i <= 100; i += 2)
{
pbPostep.Value = i;
await Task.Delay(50);
}
}
✅ Blokada na czas operacji
private async void btnStart_Click(...)
{
btnStart.IsEnabled = false;
for (int i = 0; i <= 100; i += 2)
{
pbPostep.Value = i;
await Task.Delay(50);
}
btnStart.IsEnabled = true;
}
❌ Błąd 4: Zapomniany Minimum/Maximum przy nietypowym zakresie
❌ Pasek wypełnia się za szybko/wolno
// 250 plików, ale ProgressBar // ma domyślne Maximum="100" pbPostep.Value = numerPliku; // Pasek "wypełni się" przy pliku 100, // a zostanie jeszcze 150!
✅ Maximum zgodne z rzeczywistą liczbą
pbPostep.Maximum = listaPlikow.Count; // 250 pbPostep.Value = numerPliku;
Zadania do wykonania
Stwórz okno z ProgressBar i przyciskiem „Start”. Po kliknięciu,
używając async/await i Task.Delay,
wypełnij pasek od 0 do 100 w ciągu 4 sekund (podziel na odpowiednią
liczbę kroków). Zablokuj przycisk na czas trwania operacji
i wyświetl MessageBox po zakończeniu.
Stwórz przycisk „Sprawdź aktualizacje”, który pokazuje
ProgressBar z IsIndeterminate="True"
przez 2 sekundy (symulacja sprawdzania), a następnie
wyświetla MessageBox „Aplikacja jest aktualna” i chowa pasek
(Visibility.Collapsed).
Rozbuduj przykład z sekcji 9 (pobieranie plików) –
dodaj listę ListBox, do której po pobraniu
każdego pliku dodaje się jego nazwa z dopiskiem „✓ pobrano”.
Dodaj przycisk „Anuluj”, który (na razie wystarczy) tylko
wyświetla informację, że anulowanie nie jest jeszcze obsługiwane
(prawdziwe anulowanie poznasz w dalszych tematach o async).
Napisz prosty „instalator” aplikacji: 4 etapy
(Kopiowanie plików, Konfiguracja, Rejestracja komponentów,
Finalizacja), każdy trwający losową liczbę sekund (1-3, użyj
Random). Pod ProgressBar pokazuj nazwę aktualnego
etapu. Pasek ma odpowiednio dzielić 100% na 4 etapy
(każdy etap to 25 punktów Maximum).