C# · WPF · INF.04 · Kontrolki

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.

ProgressBar IsIndeterminate DispatcherTimer async/await
1

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.

Najprostszy ProgressBar
<ProgressBar x:Name="pbPostep"
             Width="300"
             Height="25" />
<!-- Domyślnie: Minimum=0, Maximum=100, Value=0 -->
2

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ślaDomyślnie
MinimumWartość przy 0% wypełnienia0
MaximumWartość przy 100% wypełnienia100
ValueAktualny postęp0
ProgressBar w 50%
<ProgressBar x:Name="pbPostep"
             Minimum="0"
             Maximum="100"
             Value="50"
             Width="300"
             Height="25" />
Zmiana wartości z C#
// 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}%";
3

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 bez znanej wartości postępu
<ProgressBar x:Name="pbLadowanie"
             IsIndeterminate="True"
             Width="300"
             Height="25" />
<!-- Value jest ignorowane gdy IsIndeterminate="True" -->
SytuacjaTryb
Wczytywanie 1000 elementów z listy (znana liczba)Zwykły, z Value
Łączenie z serwerem (czas nieznany)IsIndeterminate="True"
Pobieranie pliku o znanym rozmiarzeZwykły, z Value
Sprawdzanie aktualizacji aplikacjiIsIndeterminate="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.

Przełączanie trybu z C#
pbPostep.IsIndeterminate = true;   // pokaż animację "w nieskończoność"
// ... gdy poznasz dokładny postęp:
pbPostep.IsIndeterminate = false;
pbPostep.Value = 0;
4

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

XAML
<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>
C# – wypełnianie paska co 100ms
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 nie blokuje okna

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.

5

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.

❌ Kod, który zamraża okno
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" -->
}
Dlaczego tak się dzieje?

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.

6

async i await – wyjaśnienie od zera

Analogia z kuchnią

Wyobraź sobie, że gotujesz obiad

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.

Metoda async – sama deklaracja
private async void btnStart_Click(object sender, RoutedEventArgs e)
{
    // "async" w sygnaturze metody – pozwala używać "await" w środku
}
async sam z siebie nic nie robi!

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

await w praktyce
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
}
Co się dzieje krok po kroku

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 WPFSymulacja 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 vs Task<T>
// 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>
Nie musisz na razie pisać własnych metod z Task

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

7

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.

XAML
<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>
C# – async/await z ProgressBar
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!";
}
Porównaj z kodem z sekcji 5

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.

8

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.

Blokada przycisku podczas 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
}
Dobry wzorzec do zapamiętania

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.

9

Praktyczny przykład – pobieranie plików (symulacja)

MainWindow.xaml
<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>
MainWindow.xaml.cs
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;
}
10

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;
11

Zadania do wykonania

Zadanie 1 łatwe

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.

Zadanie 2 łatwe

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

Zadanie 3 średnie

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

Zadanie 4 średnie

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