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ę.
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ę:
RadioButtonczyCheckBox.
Grupa przycisków opcji
<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>
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ą.
Kilka grup: panele i GroupName
Grupa powstaje na dwa sposoby:
| Sposób | Jak działa |
|---|---|
| osobne panele | Każda grupa stoi we własnym StackPanel (lub innym panelu). Opcje z różnych paneli są od siebie niezależne. |
GroupName | Opcje 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:
<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ź.
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:
<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ę.
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:
// 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.
RadioButton czy CheckBox?
| Pytanie w formularzu | Kontrolka | Dlaczego |
|---|---|---|
| Rodzaj biletu | RadioButton | dokładnie jeden |
| Dodatki do pizzy | CheckBox | dowolnie wiele, także żaden |
| Akceptuję regulamin | CheckBox | jedno „tak / nie” |
| Płeć / forma zwracania się | RadioButton | jedna z kilku, opcja „nie podaję” też jest opcją |
| Lista 30 miast | ani jedno, ani drugie | za dużo opcji — lista rozwijana z lekcji 16 |
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ą.
bilet 60-minutowy ulgowy na dwie strefy, dwie sztuki
<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>
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 bezGroupName. Nagłówek grupy stoi poza tym panelem — w panelu są same przyciski opcji, więcforeach (RadioButton …)nie trafi na napis. - Wybór domyślny ustawia konstruktor, po
InitializeComponent(). Każde przypisanieIsChecked = truewywołujeCheckedi przeliczenie. - Pole liczby biletów ma w XAML
Text="1", więc jegoTextChangedprzychodzi jeszcze w trakcieInitializeComponent(). DlategoPrzeliczzaczyna od sprawdzenia, czySumaiKupjuż istnieją. Przeliczkończy pracę także wtedy, gdy któraś grupa nie ma jeszcze wyboru — przy starcie pierwszeCheckedprzychodzi, zanim zaznaczą się pozostałe grupy.- W
Tagsą 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
Contentzaznaczonych 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
- Przenieś trzy linie z
IsChecked = truez konstruktora do XAML (IsChecked="True") i usuń warunekif (Suma == null …). Program zakończy się wyjątkiem przy starcie. - Dopisz wszystkim przyciskom opcji
GroupName="Bilet". Trzy grupy staną się jedną — da się zaznaczyć tylko jedną opcję w całym oknie. - Wpisz liczbę biletów 0, potem 11, potem literę. Za każdym razem przycisk się wyłącza.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
RadioButton.Content | object | Opis opcji; kliknięcie opisu też zaznacza opcję. |
IsChecked | bool? | Czy opcja jest zaznaczona. W if: == true. |
GroupName | string | Nazwa grupy; opcje z tą samą nazwą wykluczają się nawet w różnych panelach. |
Checked | zdarzenie | Opcja została zaznaczona (także z kodu). |
Unchecked | zdarzenie | Opcja została odznaczona, bo wybrano inną z grupy. |
RadioButton? | typ | Typ, który może przyjąć wartość null. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
dwie grupy w jednym StackPanel bez GroupName | Tworzą jedną grupę — wybór w jednej odznacza drugą. |
IsChecked="True" + Checked zmieniające kontrolkę niżej w pliku | NullReferenceException przy starcie. Ustaw wybór w konstruktorze. |
foreach (RadioButton r in Panel.Children), gdy w panelu jest nagłówek | InvalidCastException na napisie. Nagłówek wyjmij z panelu. |
| brak wyboru domyślnego | Metoda 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
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.
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ż:Foregroundokna. - Każda grupa może mieć jedną metodę
Checked, która sprawdza, który przycisk jest zaznaczony.
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 = falsei przywrócenie koloruForegrounddla każdego przycisku.
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
GroupNamesześć opcji w jednymGridto jedna grupa. - Najprościej: zamień wartość wejściową na °C, a potem z °C na jednostkę docelową. Zero absolutne to −273,15 °C.
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
Tagw groszach ("1299") i dziel przez 100 — unikniesz problemu przecinka i kropki. - Godzinę bez dwukropków da
DateTime.Now.ToString("HHmmss").
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.
pokaż wskazówkę
- Wszystkie przyciski stoją w jednym
Grid, więc wiersze rozdzielaGroupName, np."P1"–"P5". Ocenę zapisz wTag. - 25 przycisków łatwiej utworzyć w pętli:
GroupName = $"P{wiersz}",Grid.SetRow,Grid.SetColumn.
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
ContentiIsChecked. - Odpowiedzi zapisuj w tablicy
int[]z wartością-1dla „brak odpowiedzi”. Zapisuj wybór przed przejściem do innego pytania. - Tablica odpowiedzi do pytań:
string[,](5 × 4) albo pięć tablicstring[].
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ę sprawdzeniemnull. - 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.