Ożywiamy formularze w WPF: Jak okiełznać IsChecked, zdarzenia Checked i pisać kod reagujący w locie!

Zaprojektowanie estetycznego formularza w XAML z ramkami GroupBox i równo ułożonymi kontrolkami CheckBox czy RadioButton to świetny początek. Prawda jest jednak taka, że bez kodu w C# Twój formularz jest jak samochód bez silnika - ładnie wygląda na parkingu, ale nigdzie nim nie pojedziesz.

Czas połączyć widok z logiką, czyli wejść do pliku Code-Behind (MainWindow.xaml.cs).

Dowiesz się dzisiaj, dlaczego kompilator buntuje się przy zwykłym if (box.IsChecked), jak napisać kalkulator usług na egzamin INF.04 oraz jak sprawić, by aplikacja reagowała na kliknięcia użytkownika w ułamku sekundy!

1. Tajemnica bool?: Dlaczego if (mojBox.IsChecked) wyrzuca błąd?

Gdy zaczynasz pisać logikę w C#, Twoim naturalnym odruchem jest sprawdzenie stanu zaznaczenia kontrolki tak jak zwykłej zmiennej logicznej:

// BŁĄD KOMPILACJI! Kod się nie uruchomi!
if (cbKlimatyzacja.IsChecked) 
{
    // ...
}

Dlaczego kompilator krzyczy: "Cannot implicitly convert type 'bool?' to 'bool'"?

O co chodzi z tym pytajnikiem?

W WPF kontrolka CheckBox może mieć nie dwa, a trzy stany:

  1. true - zaznaczony (ptaszek),
  2. false - odznaczony (pusty),
  3. null - stan nieokreślony (często wyświetlany jako wypełniony kwadracik, gdy zaznaczono tylko część podopcji).

Z tego powodu właściwość IsChecked nie jest zwykłym typem bool, lecz typem bool? (Nullable<bool>). Zwykła instrukcja if oczekuje twardej prawdy lub fałszu i nie wie, co zrobić z wartością null.

🧠 Złota reguła na egzamin:

Zawsze jawnie porównuj stan kontrolki z wartością true:

if (cbKlimatyzacja.IsChecked == true)
{
    cena += 150;
}

Ten prosty zapis eliminuje problem z typem nullowalnym raz na zawsze.

2. Dwa podejścia do formularzy: "Przycisk" vs "W locie"

W aplikacjach okienkowych stan kontrolek wyboru możemy przetwarzać na dwa sposoby. Zobaczmy różnicę na przykładzie cennika warsztatu samochodowego.

3. Podejście #1: Odczyt po kliknięciu przycisku (Standard INF.04)

Użytkownik klika przycisk "Podsumuj usługę", a my w metodzie obsługującej zdarzenie Click sprawdzamy zaznaczone pozycje:

private void btnPodsumuj_Click(object sender, RoutedEventArgs e)
{
    decimal suma = 0;

    // 1. Sprawdzamy opcje wykluczające się (RadioButton)
    if (rbMyciePodstawowe.IsChecked == true)
    {
        suma += 50;
    }
    else if (rbMycieKompleksowe.IsChecked == true)
    {
        suma += 120;
    }

    // 2. Sprawdzamy opcje dodatkowe (CheckBox)
    if (cbWoskowanie.IsChecked == true)
    {
        suma += 40;
    }

    if (cbOdkurzanie.IsChecked == true)
    {
        suma += 30;
    }

    // 3. Wyświetlamy wynik na ekranie
    lblKosztCalkowity.Content = $"Razem do zapłaty: {suma} zł";
}

To podejście jest proste i niezawodne, ale wymaga od użytkownika wciśnięcia przycisku, by zobaczyć jakąkolwiek zmianę.

4. Podejście #2: Reakcja w czasie rzeczywistym (Checked i Unchecked)

Nowoczesne aplikacje nie każą klikać przycisku po każdej drobnej zmianie - reagują natychmiast!

Służą do tego dwa kluczowe zdarzenia:

Przykład: Dynamiczne odblokowywanie pola (Klasyk egzaminacyjny!)

Wyobraź sobie formularz zgłoszeniowy z opcją:

[ ] Inna usterka (opisz poniżej)

Pole tekstowe pod spodem powinno być zablokowane i aktywować się tylko wtedy, gdy ten ptaszek jest zaznaczony.

Krok 1: XAML

<StackPanel Margin="10">
    <!-- TextBox jest domyślnie zablokowany: IsEnabled="False" -->
    <CheckBox x:Name="cbInnaUsterka" 
              Content="Inna usterka (wymaga opisu)"
              Checked="cbInnaUsterka_Checked"
              Unchecked="cbInnaUsterka_Unchecked" />

    <TextBox x:Name="txtOpisUsterki" 
             IsEnabled="False" 
             Margin="0,5,0,0" 
             Height="60" 
             TextWrapping="Wrap" />
</StackPanel>

Krok 2: Code-Behind (C#)

Do obsługi zdarzeń tworzymy metody zwane EventHandlerami:

// Użytkownik zaznaczył opcję -> Aktywujemy pole
private void cbInnaUsterka_Checked(object sender, RoutedEventArgs e)
{
    txtOpisUsterki.IsEnabled = true;
    txtOpisUsterki.Focus(); // Automatycznie ustawiamy kursor w polu tekstowym
}

// Użytkownik odznaczył opcję -> Blokujemy pole i czyścimy jego treść
private void cbInnaUsterka_Unchecked(object sender, RoutedEventArgs e)
{
    txtOpisUsterki.IsEnabled = false;
    txtOpisUsterki.Clear(); // Usuwamy tekst, skoro opcja została cofnięta
}

Efekt

⚠️ Pamiętaj o parze zdarzeń!
Jeśli podepniesz tylko Checked, a zapomnisz o Unchecked, pole odblokuje się po zaznaczeniu, ale po odznaczeniu ptaszka pozostanie aktywne. Aplikacja zachowa się nielogicznie!

5. Trik zawodowca: Jeden EventHandler dla wielu kontrolek

Chcesz przeliczać łączną kwotę zamówienia w czasie rzeczywistym po każdym kliknięciu w dowolny CheckBox? Nie musisz tworzyć 10 osobnych metod!

Stwórz jedną ogólną metodę przeliczającą:

private void AktualizujCene(object sender, RoutedEventArgs e)
{
    decimal suma = 0;

    if (cbOpcja1.IsChecked == true) suma += 100;
    if (cbOpcja2.IsChecked == true) suma += 200;
    if (cbOpcja3.IsChecked == true) suma += 300;

    lblSuma.Content = $"Aktualna kwota: {suma} zł";
}

A w pliku XAML podepnij ją pod Checked oraz Unchecked każdego z pól:

<CheckBox x:Name="cbOpcja1" Content="Opcja 1" Checked="AktualizujCene" Unchecked="AktualizujCene" />
<CheckBox x:Name="cbOpcja2" Content="Opcja 2" Checked="AktualizujCene" Unchecked="AktualizujCene" />
<CheckBox x:Name="cbOpcja3" Content="Opcja 3" Checked="AktualizujCene" Unchecked="AktualizujCene" />

Dzięki temu Twój kod w C# pozostaje czysty, zwięzły i łatwy w modyfikacji!