Przejdź do treści
School IT / Przestrzeń do nauki

RadioButton — jedna opcja z kilku

Rodzaj biletu, rozmiar koszulki, odpowiedź w teście — gdy z kilku możliwości wolno wybrać dokładnie jedną, używasz przycisków opcji. Zaznaczenie jednego automatycznie odznacza pozostałe z tej samej grupy. W tej lekcji nauczysz się tworzyć niezależne grupy, ustawiać wybór domyślny bez błędu przy starcie okna i jedną metodą znajdować zaznaczoną opcję.

XAML C# RadioButton GroupName Checked 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zbudujesz grupę przycisków opcji, z której można wybrać tylko jedną.
  • Rozdzielisz opcje na niezależne grupy panelami albo właściwością GroupName.
  • Ustawisz opcję domyślną tak, żeby okno nie zgłosiło błędu przy starcie.
  • Napiszesz metodę, która zwraca zaznaczoną opcję z grupy.
  • Przeliczysz wynik na żywo po każdej zmianie wyboru.
  • Dobierzesz właściwą kontrolkę: RadioButton czy CheckBox.
TEORIA

Grupa przycisków opcji

MainWindow.xaml
<StackPanel Margin="15">
    <TextBlock Text="Rozmiar koszulki klasowej:" FontWeight="Bold"/>
    <RadioButton x:Name="RozmiarS" Content="S" Margin="0,4"/>
    <RadioButton x:Name="RozmiarM" Content="M" Margin="0,4"/>
    <RadioButton x:Name="RozmiarL" Content="L" Margin="0,4"/>
    <RadioButton x:Name="RozmiarXL" Content="XL" Margin="0,4"/>
</StackPanel>
Koszulka–□✕
Rozmiar koszulki klasowej:
S
M
L
XL

kliknięcie „L” zaznaczy L i odznaczy M

RadioButton ma te same podstawowe elementy co CheckBox: Content, IsChecked typu bool? i zdarzenie Checked. Różnica: przyciski opcji stojące w tym samym panelu tworzą grupę — zaznaczenie jednego odznacza resztę. Użytkownik nie może odznaczyć opcji kliknięciem w nią samą; może tylko wybrać inną.

TEORIA

Kilka grup: panele i GroupName

Grupa powstaje na dwa sposoby:

SposóbJak działa
osobne paneleKażda grupa stoi we własnym StackPanel (lub innym panelu). Opcje z różnych paneli są od siebie niezależne.
GroupNameOpcje z tą samą wartością GroupName tworzą grupę — nawet gdy stoją w różnych panelach. Opcje z różnymi nazwami są niezależne, nawet w jednym panelu.

GroupName przydaje się, gdy układ okna nie pokrywa się z podziałem na grupy — np. w siatce, gdzie każdy wiersz to osobne pytanie ankiety:

MainWindow.xaml
<Grid Margin="10">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="50"/>
        <ColumnDefinition Width="50"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" Text="Lekcje są ciekawe"/>
    <RadioButton Grid.Row="0" Grid.Column="1" GroupName="P1" Content="Tak"/>
    <RadioButton Grid.Row="0" Grid.Column="2" GroupName="P1" Content="Nie"/>

    <TextBlock Grid.Row="1" Text="Zadania są za trudne"/>
    <RadioButton Grid.Row="1" Grid.Column="1" GroupName="P2" Content="Tak"/>
    <RadioButton Grid.Row="1" Grid.Column="2" GroupName="P2" Content="Nie"/>
</Grid>

Wszystkie cztery przyciski stoją w jednym Grid. Bez GroupName tworzyłyby jedną grupę i w całej ankiecie dałoby się zaznaczyć tylko jedną odpowiedź.

TEORIA

Wybór domyślny

Dobra grupa ma od początku zaznaczoną jedną opcję — wtedy użytkownik nie może „zapomnieć” wyboru. Najprościej wpisać IsChecked="True" w XAML. Uwaga jednak na pułapkę z lekcji 14:

pułapka
<RadioButton Content="normalny" IsChecked="True" Checked="Opcja_Checked"/>
...
<TextBlock x:Name="Suma"/>   <!-- powstaje PÓŹNIEJ niż przycisk opcji -->

Zdarzenie Checked wywoła się w trakcie InitializeComponent(), gdy Suma jeszcze nie istnieje — Suma.Text = … zakończy program wyjątkiem NullReferenceException. Są dwa wyjścia:

  • ustawić wybór domyślny w konstruktorze, po InitializeComponent() — wtedy wszystkie kontrolki już istnieją;
  • na początku metody obsługi sprawdzić, czy potrzebne kontrolki nie są null, i jeśli są — zakończyć metodę.
TEORIA

Która opcja jest zaznaczona?

Przy dwóch opcjach wystarczy if. Przy większej grupie lepiej przejść pętlą po panelu i zwrócić zaznaczony przycisk. Taka metoda działa dla każdej grupy:

MainWindow.xaml.cs
// w panelu muszą być same przyciski opcji
private RadioButton? Zaznaczony(StackPanel grupa)
{
    foreach (RadioButton opcja in grupa.Children)
    {
        if (opcja.IsChecked == true)
        {
            return opcja;
        }
    }
    return null;        // nic nie zaznaczono
}

// użycie
RadioButton? rozmiar = Zaznaczony(Rozmiary);
if (rozmiar != null)
{
    Info.Text = $"Wybrano rozmiar {rozmiar.Content}";
}

Znak zapytania w RadioButton? mówi kompilatorowi, że metoda może zwrócić null. Dlatego przed użyciem wyniku sprawdzasz != null. Dane opcji — cenę, mnożnik, punkty — trzymasz w Tag, tak jak przy polach wyboru.

Przy grupach liczonych na żywo zwykle wystarczy samo Checked: gdy wybór przechodzi z jednej opcji na drugą, nowa opcja dostaje Checked, więc i tak przeliczasz wszystko.

TEORIA

RadioButton czy CheckBox?

Pytanie w formularzuKontrolkaDlaczego
Rodzaj biletuRadioButtondokładnie jeden
Dodatki do pizzyCheckBoxdowolnie wiele, także żaden
Akceptuję regulaminCheckBoxjedno „tak / nie”
Płeć / forma zwracania sięRadioButtonjedna z kilku, opcja „nie podaję” też jest opcją
Lista 30 miastani jedno, ani drugieza dużo opcji — lista rozwijana z lekcji 16
PRZYKŁAD

Biletomat komunikacji miejskiej

Pasażer wybiera rodzaj biletu, uprawnienia i strefę, a potem wpisuje liczbę biletów. Cena przelicza się przy każdej zmianie. Każda grupa ma od startu zaznaczoną opcję domyślną.

Biletomat MZK–□✕
Rodzaj biletu
jednorazowy (4 zł)
60-minutowy (5 zł)
24-godzinny (15 zł)
7-dniowy (45 zł)
Uprawnienia
normalny
ulgowy — uczeń (−50%)
Karta Dużej Rodziny (−37%)
Strefa
I — miasto
I + II — z gminą (×1,5)
Liczba biletów:
2
(1–10)
2 × 60-minutowy (5 zł), ulgowy — uczeń (−50%), strefa I + II — z gminą (×1,5)
Do zapłaty: 7,50 zł (3,75 zł za bilet)
Kupuję

bilet 60-minutowy ulgowy na dwie strefy, dwie sztuki

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja15.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Biletomat MZK" Height="400" Width="640"
        ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">

    <Grid Margin="15">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- Trzy grupy: każda w osobnym panelu, więc GroupName nie jest potrzebne -->
        <UniformGrid Grid.Row="0" Columns="3">
            <Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="6"
                    Padding="10" Margin="0,0,8,0">
                <StackPanel>
                    <TextBlock Text="Rodzaj biletu" FontWeight="Bold" Margin="0,0,0,6"/>
                    <StackPanel x:Name="Rodzaje">
                        <RadioButton x:Name="Jednorazowy" Content="jednorazowy (4 zł)" Tag="4"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                        <RadioButton Content="60-minutowy (5 zł)" Tag="5"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                        <RadioButton Content="24-godzinny (15 zł)" Tag="15"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                        <RadioButton Content="7-dniowy (45 zł)" Tag="45"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                    </StackPanel>
                </StackPanel>
            </Border>

            <Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="6"
                    Padding="10" Margin="4,0">
                <StackPanel>
                    <TextBlock Text="Uprawnienia" FontWeight="Bold" Margin="0,0,0,6"/>
                    <StackPanel x:Name="Ulgi">
                        <RadioButton x:Name="Normalny" Content="normalny" Tag="100"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                        <RadioButton Content="ulgowy — uczeń (−50%)" Tag="50"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                        <RadioButton Content="Karta Dużej Rodziny (−37%)" Tag="63"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                    </StackPanel>
                </StackPanel>
            </Border>

            <Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="6"
                    Padding="10" Margin="8,0,0,0">
                <StackPanel>
                    <TextBlock Text="Strefa" FontWeight="Bold" Margin="0,0,0,6"/>
                    <StackPanel x:Name="Strefy">
                        <RadioButton x:Name="StrefaI" Content="I — miasto" Tag="100"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                        <RadioButton Content="I + II — z gminą (×1,5)" Tag="150"
                                     Margin="0,3" Checked="Opcja_Checked"/>
                    </StackPanel>
                </StackPanel>
            </Border>
        </UniformGrid>

        <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,15,0,0">
            <TextBlock Text="Liczba biletów:" VerticalAlignment="Center"/>
            <TextBox x:Name="Liczba" Text="1" Width="40" Margin="8,0" MaxLength="2"
                     TextChanged="Liczba_TextChanged"/>
            <TextBlock Text="(1–10)" Foreground="Gray" VerticalAlignment="Center"/>
        </StackPanel>

        <Grid Grid.Row="2" VerticalAlignment="Bottom">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <StackPanel>
                <TextBlock x:Name="Opis" Foreground="DimGray"/>
                <TextBlock x:Name="Suma" FontSize="22" FontWeight="Bold"/>
            </StackPanel>
            <Button x:Name="Kup" Grid.Column="1" Content="Kupuję" Width="110" Height="36"
                    FontSize="14" VerticalAlignment="Bottom" Click="Kup_Click"/>
        </Grid>
    </Grid>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;

namespace Lekcja15;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Wybór domyślny ustawiamy dopiero tutaj — wszystkie kontrolki już istnieją
        Jednorazowy.IsChecked = true;
        Normalny.IsChecked = true;
        StrefaI.IsChecked = true;
    }

    private void Opcja_Checked(object sender, RoutedEventArgs e)
    {
        Przelicz();
    }

    private void Liczba_TextChanged(object sender, TextChangedEventArgs e)
    {
        Przelicz();
    }

    private void Kup_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show($"{Opis.Text}\n{Suma.Text}\n\nZbliż kartę do czytnika.",
                        "Płatność", MessageBoxButton.OK, MessageBoxImage.Information);
    }

    private void Przelicz()
    {
        // Przy starcie zdarzenia mogą przyjść, zanim powstaną wszystkie kontrolki
        if (Suma == null || Kup == null)
        {
            return;
        }

        RadioButton? rodzaj = Zaznaczony(Rodzaje);
        RadioButton? ulga = Zaznaczony(Ulgi);
        RadioButton? strefa = Zaznaczony(Strefy);

        if (rodzaj == null || ulga == null || strefa == null)
        {
            return;     // jeszcze nie wszystkie grupy mają wybór
        }

        if (!int.TryParse(Liczba.Text, out int liczba) || liczba < 1 || liczba > 10)
        {
            Opis.Text = "";
            Suma.Text = "Podaj liczbę biletów od 1 do 10";
            Kup.IsEnabled = false;
            return;
        }

        decimal cena = decimal.Parse($"{rodzaj.Tag}");
        decimal procentUlgi = decimal.Parse($"{ulga.Tag}");
        decimal procentStrefy = decimal.Parse($"{strefa.Tag}");

        decimal jeden = cena * procentUlgi / 100 * procentStrefy / 100;
        decimal razem = jeden * liczba;

        Opis.Text = $"{liczba} × {rodzaj.Content}, {ulga.Content}, strefa {strefa.Content}";
        Suma.Text = $"Do zapłaty: {razem:F2} zł ({jeden:F2} zł za bilet)";
        Kup.IsEnabled = true;
    }

    // Zwraca zaznaczony przycisk z panelu albo null, gdy żaden nie jest zaznaczony
    private RadioButton? Zaznaczony(StackPanel grupa)
    {
        foreach (RadioButton opcja in grupa.Children)
        {
            if (opcja.IsChecked == true)
            {
                return opcja;
            }
        }
        return null;
    }
}

Co dzieje się po kolei

  • Każda grupa ma własny panel z nazwą (Rodzaje, Ulgi, Strefy), więc trzy grupy są od siebie niezależne bez GroupName. Nagłówek grupy stoi poza tym panelem — w panelu są same przyciski opcji, więc foreach (RadioButton …) nie trafi na napis.
  • Wybór domyślny ustawia konstruktor, po InitializeComponent(). Każde przypisanie IsChecked = true wywołuje Checked i przeliczenie.
  • Pole liczby biletów ma w XAML Text="1", więc jego TextChanged przychodzi jeszcze w trakcie InitializeComponent(). Dlatego Przelicz zaczyna od sprawdzenia, czy Suma i Kup już istnieją.
  • Przelicz kończy pracę także wtedy, gdy któraś grupa nie ma jeszcze wyboru — przy starcie pierwsze Checked przychodzi, zanim zaznaczą się pozostałe grupy.
  • W Tag są liczby całkowite: cena w złotych i procenty. Cena biletu = cena × procent ulgi / 100 × procent strefy / 100. Dla biletu 60-minutowego ulgowego w dwóch strefach: 5 × 50 / 100 × 150 / 100 = 3,75 zł.
  • Opis zamówienia składa się z Content zaznaczonych opcji — nie trzeba powtarzać nazw w kodzie.
  • Przy złej liczbie biletów przycisk Kupuję jest nieaktywny, a zamiast kwoty widać podpowiedź.

Sprawdź trzy rzeczy

  1. Przenieś trzy linie z IsChecked = true z konstruktora do XAML (IsChecked="True") i usuń warunek if (Suma == null …). Program zakończy się wyjątkiem przy starcie.
  2. Dopisz wszystkim przyciskom opcji GroupName="Bilet". Trzy grupy staną się jedną — da się zaznaczyć tylko jedną opcję w całym oknie.
  3. Wpisz liczbę biletów 0, potem 11, potem literę. Za każdym razem przycisk się wyłącza.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
RadioButton.ContentobjectOpis opcji; kliknięcie opisu też zaznacza opcję.
IsCheckedbool?Czy opcja jest zaznaczona. W if: == true.
GroupNamestringNazwa grupy; opcje z tą samą nazwą wykluczają się nawet w różnych panelach.
CheckedzdarzenieOpcja została zaznaczona (także z kodu).
UncheckedzdarzenieOpcja została odznaczona, bo wybrano inną z grupy.
RadioButton?typTyp, który może przyjąć wartość null.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
dwie grupy w jednym StackPanel bez GroupNameTworzą jedną grupę — wybór w jednej odznacza drugą.
IsChecked="True" + Checked zmieniające kontrolkę niżej w plikuNullReferenceException przy starcie. Ustaw wybór w konstruktorze.
foreach (RadioButton r in Panel.Children), gdy w panelu jest nagłówekInvalidCastException na napisie. Nagłówek wyjmij z panelu.
brak wyboru domyślnegoMetoda szukająca zaznaczonej opcji zwróci null — obsłuż to albo zaznacz opcję na starcie.
if (Normalny.IsChecked)CS0266 — bool?; dopisz == true.
jeden RadioButton „Zgadzam się”Raz zaznaczonego nie da się odznaczyć. Pojedyncze „tak / nie” to CheckBox.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Każda grupa opcji powinna mieć wybór domyślny, chyba że treść mówi inaczej. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Motyw okna★☆☆

Okno z krótkim tekstem powitania i grupą „Motyw”: Jasny, Ciemny, Wysoki kontrast (czarne tło, żółty tekst), Szkolny (granat i biel). Wybranie motywu od razu zmienia tło okna i kolor wszystkich napisów. Pod spodem druga, niezależna grupa „Rozmiar tekstu”: mały, średni, duży.

pokaż wskazówkę
  • Rozmiar czcionki ustawiony na oknie (FontSize) dziedziczą wszystkie napisy — jak w lekcji 05. Kolor tekstu też: Foreground okna.
  • Każda grupa może mieć jedną metodę Checked, która sprawdza, który przycisk jest zaznaczony.
ZAD 2Mini-quiz z sieci★☆☆

Trzy pytania z sieci komputerowych, każde z czterema odpowiedziami A–D (bez wyboru domyślnego). Przycisk Sprawdź jest aktywny dopiero, gdy na każde pytanie zaznaczono odpowiedź. Po sprawdzeniu poprawne odpowiedzi robią się zielone, błędne zaznaczenia — czerwone, a na dole pojawia się „Wynik: 2 / 3”. Przycisk Od nowa czyści zaznaczenia i kolory.

pokaż wskazówkę
  • Poprawną odpowiedź oznacz w XAML przez Tag="ok" — kod nie musi znać treści pytań.
  • Czyszczenie: IsChecked = false i przywrócenie koloru Foreground dla każdego przycisku.
ZAD 3Przelicznik temperatur★★☆

Pole na wartość i siatka 2 × 3 przycisków opcji: w lewej kolumnie grupa „Z” (°C, °F, K), w prawej grupa „Na” (°C, °F, K) — wszystkie w jednym Grid. Wynik przelicza się na żywo: „36,6 °C = 97,88 °F”. Gdy obie grupy wskazują tę samą jednostkę, wynik to ta sama wartość. Temperatura poniżej zera absolutnego daje komunikat „Poniżej zera absolutnego!”.

pokaż wskazówkę
  • Bez GroupName sześć opcji w jednym Grid to jedna grupa.
  • Najprościej: zamień wartość wejściową na °C, a potem z °C na jednostkę docelową. Zero absolutne to −273,15 °C.
ZAD 4Nadanie paczki★★☆

Gabaryt: A (do 8 cm, 12,99 zł), B (do 19 cm, 14,99 zł), C (do 41 cm, 17,99 zł). Sposób: paczkomat (cena gabarytu), kurier (+8 zł), odbiór w punkcie (+3 zł). Pole Waga w kg (do 25 kg) i pole wyboru Za pobraniem (+5 zł). Cena przelicza się na żywo. Przycisk Nadaj pokazuje podsumowanie i numer nadania złożony z litery gabarytu i godziny, np. „B-142305”.

pokaż wskazówkę
  • Cenę z groszami zapisz w Tag w groszach ("1299") i dziel przez 100 — unikniesz problemu przecinka i kropki.
  • Godzinę bez dwukropków da DateTime.Now.ToString("HHmmss").
ZAD 5Ankieta o lekcjach★★☆

Ankieta w formie tabeli: pięć stwierdzeń w wierszach i skala 1–5 w kolumnach (nagłówki kolumn: 1 = „zdecydowanie nie”, 5 = „zdecydowanie tak”). Każdy wiersz to osobna grupa, bez wyboru domyślnego. Pod tabelą „Odpowiedziano: 3 / 5”, a przycisk Wyślij jest aktywny dopiero przy komplecie. Po wysłaniu MessageBox pokazuje średnią ocen z jednym miejscem po przecinku.

Ankieta–□✕
12345 Lekcje są ciekawe Tempo jest dobre Zadania są zrozumiałe Przykłady pomagają Polecił(a)bym ten kurs
Odpowiedziano: 3 / 5
Wyślij
pokaż wskazówkę
  • Wszystkie przyciski stoją w jednym Grid, więc wiersze rozdziela GroupName, np. "P1"–"P5". Ocenę zapisz w Tag.
  • 25 przycisków łatwiej utworzyć w pętli: GroupName = $"P{wiersz}", Grid.SetRow, Grid.SetColumn.
ZAD 6Test wiedzy krok po kroku★★★

Test z pięciu pytań wyświetlanych po jednym: numer („Pytanie 2 z 5”), treść i cztery odpowiedzi. Pytania, odpowiedzi i numery poprawnych odpowiedzi trzymaj w tablicach. Przyciski ◀ Wstecz i Dalej ▶ przechodzą między pytaniami, a program pamięta, co zaznaczono w każdym pytaniu. Na ostatnim pytaniu zamiast „Dalej” jest Zakończ. Po zakończeniu okno pokazuje wynik w procentach, ocenę (próg zaliczenia 50%) i numery pytań z błędną odpowiedzią.

pokaż wskazówkę
  • Cztery przyciski opcji są stałe — przy zmianie pytania zmieniasz tylko ich Content i IsChecked.
  • Odpowiedzi zapisuj w tablicy int[] z wartością -1 dla „brak odpowiedzi”. Zapisuj wybór przed przejściem do innego pytania.
  • Tablica odpowiedzi do pytań: string[,] (5 × 4) albo pięć tablic string[].
PODSUMOWANIE

Co trzeba zapamiętać

  • RadioButton — dokładnie jedna opcja z grupy. Opcje w jednym panelu tworzą grupę.
  • GroupName łączy lub rozdziela grupy niezależnie od paneli.
  • Wybór domyślny ustawiaj w konstruktorze po InitializeComponent() albo zabezpiecz metodę sprawdzeniem null.
  • Zaznaczoną opcję znajdziesz metodą z pętlą po panelu; dane opcji trzymaj w Tag.
  • Do przeliczania na żywo zwykle wystarczy Checked.
  • Jedno „tak / nie” lub wiele naraz — CheckBox; jedna z kilku — RadioButton.

Dokumentacja: Microsoft Learn — klasa RadioButton.

Postęp zapisuje się w tej przeglądarce.