GroupBox i Expander
Długi formularz z kilkunastoma polami jeden pod drugim jest trudny do ogarnięcia wzrokiem. Dwie proste kontrolki porządkują go: GroupBox otacza powiązane pola ramką z tytułem, a Expander chowa rzadziej używane opcje pod nagłówkiem, który można rozwinąć jednym kliknięciem. Arkusze INF.04 często wymagają właśnie takiego podziału — „dane osobowe”, „wybór usługi”, „podsumowanie”.
Czego się dziś nauczysz
- Pogrupujesz pola formularza w ramki z tytułem.
- Umieścisz w nagłówku grupy pole wyboru, które włącza i wyłącza całą grupę.
- Schowasz rzadziej używane opcje w sekcji zwijanej.
- Rozwiniesz i zwiniesz sekcję z kodu oraz zareagujesz na jej rozwinięcie.
- Zbudujesz „akordeon” — kilka sekcji, z których otwarta jest tylko jedna.
GroupBox
GroupBox to ramka z tytułem wpisanym w górną krawędź. Tak jak Border z lekcji 11 ma jedno dziecko — zwykle panel z polami. Tytuł podaje się w Header:
<StackPanel Margin="12" Width="260">
<GroupBox Header="Dane ucznia" Padding="8">
<StackPanel>
<TextBlock Text="Imię i nazwisko:"/>
<TextBox x:Name="Nazwisko"/>
<TextBlock Text="Klasa:" Margin="0,6,0,0"/>
<TextBox x:Name="Klasa" Width="60" HorizontalAlignment="Left"/>
</StackPanel>
</GroupBox>
<GroupBox Header="Wyjazd" Padding="8" Margin="0,8,0,0">
<StackPanel>
<RadioButton Content="Zielona szkoła" IsChecked="True"/>
<RadioButton Content="Wycieczka jednodniowa"/>
</StackPanel>
</GroupBox>
</StackPanel>
dwie grupy — każda ma swoją ramkę i tytuł
Padding odsuwa zawartość od ramki, Margin — ramkę od sąsiadów. Właściwości ustawione na GroupBox dziedziczą się w dół jak w lekcji 5: IsEnabled="False" wyszarza całą grupę razem z tytułem, a FontSize zmienia tytuł i wszystkie pola.
GroupBox a przyciski opcji
Ramka nie łączy przycisków opcji w grupę — robi to panel, w którym leżą (lekcja 15). Dwa panele w dwóch ramkach to dwie niezależne grupy, więc GroupName nie jest tu potrzebne.
Nagłówek z kontrolką
Header nie musi być tekstem. Zapisany jako element (jak Grid.RowDefinitions w lekcji 9) może zawierać dowolną kontrolkę — najczęściej pole wyboru, które włącza grupę:
<GroupBox Padding="8">
<GroupBox.Header>
<CheckBox x:Name="Faktura" Content="Chcę fakturę" Click="Faktura_Click"/>
</GroupBox.Header>
<StackPanel x:Name="DaneFaktury" IsEnabled="False">
<TextBlock Text="NIP:"/>
<TextBox x:Name="Nip"/>
</StackPanel>
</GroupBox>
private void Faktura_Click(object sender, RoutedEventArgs e)
{
DaneFaktury.IsEnabled = Faktura.IsChecked == true;
}
Wyłączamy panel w środku, a nie cały GroupBox. Gdyby wyłączyć ramkę, wyłączyłoby się także pole wyboru w nagłówku i nie dałoby się już grupy włączyć z powrotem.
pole niezaznaczone — zawartość grupy jest wyszarzona
Expander
Expander to nagłówek ze strzałką i jedno dziecko, które widać tylko po rozwinięciu. Zwinięta zawartość nie zajmuje miejsca — elementy pod spodem przesuwają się w górę, jak przy Visibility="Collapsed".
<StackPanel Margin="12" Width="280">
<TextBlock Text="Nazwa pliku:"/>
<TextBox x:Name="NazwaPliku" Text="raport"/>
<Expander x:Name="Zaawansowane" Header="Ustawienia zaawansowane" Margin="0,10,0,0">
<StackPanel Margin="20,6,0,0">
<CheckBox Content="Dodaj datę do nazwy"/>
<CheckBox Content="Nadpisuj bez pytania"/>
</StackPanel>
</Expander>
<Button Content="Zapisz" Margin="0,10,0,0"/>
</StackPanel>
ta sama sekcja zwinięta i rozwinięta — przycisk przesuwa się w dół
| Właściwość | Znaczenie |
|---|---|
Header | Tekst lub kontrolka obok strzałki. Można go zmieniać z kodu. |
IsExpanded | true — rozwinięty. Domyślnie false. Ustawienie z kodu rozwija lub zwija sekcję. |
ExpandDirection | Down (domyślnie), Up, Left, Right — w którą stronę wysuwa się zawartość. |
Gdy okno ma stałą wysokość, rozwinięta sekcja może się nie zmieścić. Wtedy pomaga SizeToContent="Height" (okno rośnie razem z zawartością, lekcja 10) albo ScrollViewer wokół całego formularza (lekcja 11).
Zdarzenia Expanded i Collapsed
Expanded zachodzi po rozwinięciu, Collapsed — po zwinięciu, niezależnie od tego, czy kliknął użytkownik, czy kod zmienił IsExpanded. Typowe zastosowanie to akordeon: kilka sekcji, z których otwarta jest najwyżej jedna. Wszystkie sekcje obsługuje ta sama metoda:
// w XAML każdy z trzech Expanderów ma Expanded="Sekcja_Expanded"
private void Sekcja_Expanded(object sender, RoutedEventArgs e)
{
// zwijamy wszystkie sekcje oprócz tej, która się rozwinęła
if (sender != Krok1) Krok1.IsExpanded = false;
if (sender != Krok2) Krok2.IsExpanded = false;
if (sender != Krok3) Krok3.IsExpanded = false;
}
private void Krok1_Collapsed(object sender, RoutedEventArgs e)
{
Krok1.Header = $"1. Dane ucznia — {Nazwisko.Text}"; // podsumowanie w nagłówku
}
Zwinięcie sekcji z kodu nie wywołuje ponownie Expanded, więc nie ma tu zapętlenia, przed którym chroniła flaga w lekcji 32.
Zamówienie sprzętu do pracowni
Szkoła zamawia komputery do pracowni. Formularz jest podzielony na trzy ramki: zestaw, liczba stanowisk i dodatki (włączane polem wyboru w nagłówku). Rzadziej zmieniane opcje — montaż i gwarancja — oraz rozpisanie ceny leżą w dwóch sekcjach zwijanych działających jak akordeon. Suma jest zawsze widoczna na dole, a nagłówek zwiniętej sekcji „Szczegóły ceny” pokazuje cenę jednego stanowiska.
Dodatki: 12 × 599,00 zł
Rabat od 10 stanowisk (5%): −2 458,80 zł
Gwarancja (+5%): 2 335,86 zł
Montaż: 600,00 zł
rozwinięta sekcja „Szczegóły ceny”; „Opcje dodatkowe” zwinęły się same (montaż i gwarancja 3 lata są w nich zaznaczone)
<Window x:Class="Lekcja33.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Zamówienie sprzętu do pracowni" Width="440" SizeToContent="Height"
ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
<StackPanel Margin="12">
<!-- 1. zestaw: grupa przycisków opcji -->
<GroupBox Header="Zestaw komputerowy" Padding="8">
<StackPanel x:Name="Zestawy">
<RadioButton Content="Biurowy — 2 499 zł" Tag="2499" IsChecked="True" Click="Zmiana"/>
<RadioButton Content="Programisty — 3 499 zł" Tag="3499" Margin="0,4,0,0" Click="Zmiana"/>
<RadioButton Content="Grafika — 4 999 zł" Tag="4999" Margin="0,4,0,0" Click="Zmiana"/>
</StackPanel>
</GroupBox>
<!-- 2. liczba stanowisk -->
<GroupBox Header="Liczba stanowisk" Padding="8" Margin="0,8,0,0">
<DockPanel>
<TextBlock x:Name="IleOpis" DockPanel.Dock="Right" Width="40" TextAlignment="Right"/>
<Slider x:Name="Ile" Minimum="1" Maximum="16" Value="8"
IsSnapToTickEnabled="True" TickFrequency="1"
ValueChanged="Ile_ValueChanged"/>
</DockPanel>
</GroupBox>
<!-- 3. nagłówek grupy to pole wyboru, które włącza zawartość -->
<GroupBox Padding="8" Margin="0,8,0,0">
<GroupBox.Header>
<CheckBox x:Name="ZDodatkami" Content="Dodatki do każdego stanowiska"
FontWeight="Bold" Click="Zmiana"/>
</GroupBox.Header>
<StackPanel x:Name="Dodatki" IsEnabled="False">
<CheckBox Content="Monitor 24″ — 599 zł" Tag="599" Click="Zmiana"/>
<CheckBox Content="Słuchawki z mikrofonem — 149 zł" Tag="149" Margin="0,4,0,0" Click="Zmiana"/>
<CheckBox Content="Kamera internetowa — 199 zł" Tag="199" Margin="0,4,0,0" Click="Zmiana"/>
</StackPanel>
</GroupBox>
<!-- 4. sekcje zwijane: otwarta może być tylko jedna -->
<Expander x:Name="Opcje" Header="Opcje dodatkowe" Margin="0,10,0,0"
Expanded="Expander_Expanded">
<StackPanel Margin="20,6,0,0">
<CheckBox x:Name="Montaz" Content="Montaż w pracowni — 50 zł za stanowisko"
Click="Zmiana"/>
<TextBlock Text="Gwarancja:" Margin="0,8,0,2"/>
<ComboBox x:Name="Gwarancja" Width="200" HorizontalAlignment="Left"
SelectedIndex="0" SelectionChanged="Gwarancja_SelectionChanged">
<ComboBoxItem Content="2 lata (w cenie)" Tag="0"/>
<ComboBoxItem Content="3 lata (+5%)" Tag="5"/>
<ComboBoxItem Content="5 lat (+12%)" Tag="12"/>
</ComboBox>
</StackPanel>
</Expander>
<Expander x:Name="Szczegoly" Header="Szczegóły ceny" Margin="0,6,0,0"
Expanded="Expander_Expanded">
<TextBlock x:Name="SzczegolyTekst" Margin="20,6,0,0" LineHeight="20"/>
</Expander>
<!-- podsumowanie zawsze widoczne -->
<Border Background="#FFEFF6FF" CornerRadius="6" Padding="10" Margin="0,12,0,0">
<DockPanel>
<Button x:Name="Zamow" DockPanel.Dock="Right" Content="Zamów" Width="90"
Click="Zamow_Click"/>
<TextBlock x:Name="Razem" FontSize="16" FontWeight="Bold"
VerticalAlignment="Center"/>
</DockPanel>
</Border>
</StackPanel>
</Window>
using System.Windows;
using System.Windows.Controls;
namespace Lekcja33;
public partial class MainWindow : Window
{
const decimal CenaMontazu = 50;
const int ProgRabatu = 10; // od tylu stanowisk rabat 5%
public MainWindow()
{
InitializeComponent();
Przelicz();
}
// Wspólna obsługa przycisków opcji i pól wyboru
private void Zmiana(object sender, RoutedEventArgs e)
{
Przelicz();
}
private void Ile_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
Przelicz();
}
private void Gwarancja_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Przelicz();
}
// Akordeon: rozwinięcie jednej sekcji zwija drugą
private void Expander_Expanded(object sender, RoutedEventArgs e)
{
if (sender == Opcje)
{
Szczegoly.IsExpanded = false;
}
else
{
Opcje.IsExpanded = false;
}
}
private void Przelicz()
{
// zdarzenia przychodzą już podczas wczytywania XAML
if (Razem == null)
{
return;
}
int ile = (int)Ile.Value;
IleOpis.Text = $"{ile}";
// zestaw
RadioButton? zestaw = Zaznaczony();
decimal cenaZestawu = 0;
if (zestaw != null)
{
cenaZestawu = decimal.Parse($"{zestaw.Tag}");
}
// dodatki liczą się tylko wtedy, gdy grupa jest włączona
bool zDodatkami = ZDodatkami.IsChecked == true;
Dodatki.IsEnabled = zDodatkami;
decimal cenaDodatkow = 0;
if (zDodatkami)
{
foreach (CheckBox dodatek in Dodatki.Children)
{
if (dodatek.IsChecked == true)
{
cenaDodatkow += decimal.Parse($"{dodatek.Tag}");
}
}
}
decimal sprzet = (cenaZestawu + cenaDodatkow) * ile;
decimal rabat = 0;
if (ile >= ProgRabatu)
{
rabat = sprzet * 5 / 100;
}
decimal montaz = 0;
if (Montaz.IsChecked == true)
{
montaz = CenaMontazu * ile;
}
decimal procentGwarancji = 0;
ComboBoxItem? gwarancja = Gwarancja.SelectedItem as ComboBoxItem;
if (gwarancja != null)
{
procentGwarancji = decimal.Parse($"{gwarancja.Tag}");
}
decimal dodatekGwarancji = (sprzet - rabat) * procentGwarancji / 100;
decimal razem = sprzet - rabat + dodatekGwarancji + montaz;
SzczegolyTekst.Text =
$"Zestawy: {ile} × {cenaZestawu:N2} zł\n" +
$"Dodatki: {ile} × {cenaDodatkow:N2} zł\n" +
$"Rabat od {ProgRabatu} stanowisk (5%): −{rabat:N2} zł\n" +
$"Gwarancja (+{procentGwarancji}%): {dodatekGwarancji:N2} zł\n" +
$"Montaż: {montaz:N2} zł";
Razem.Text = $"Razem: {razem:N2} zł";
// nagłówek zwiniętej sekcji też może coś mówić
Szczegoly.Header = $"Szczegóły ceny — {ile} stanowisk, za jedno {razem / ile:N2} zł";
}
private RadioButton? Zaznaczony()
{
foreach (RadioButton opcja in Zestawy.Children)
{
if (opcja.IsChecked == true)
{
return opcja;
}
}
return null;
}
private void Zamow_Click(object sender, RoutedEventArgs e)
{
MessageBoxResult odp = MessageBox.Show(
$"{Razem.Text}\n\nCzy wysłać zamówienie do działu zakupów?",
"Potwierdzenie", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (odp == MessageBoxResult.Yes)
{
MessageBox.Show("Zamówienie zostało wysłane.", "Gotowe",
MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
Co dzieje się po kolei
- Każda ramka zawiera jeden panel. Przyciski opcji zestawu leżą w panelu
Zestawy, więc tworzą jedną grupę; ceny są zapisane wTag, jak w lekcjach 14 i 15. - Nagłówek trzeciej ramki to pole wyboru
ZDodatkami.Przeliczwłącza albo wyłącza panelDodatkii liczy dodatki tylko wtedy, gdy grupa jest włączona — zaznaczenia w wyłączonej grupie zostają, ale się nie liczą. - Wszystkie pola wywołują
Przelicz, a osłonaRazem == nullchroni przed zdarzeniami z czasu wczytywania XAML (suwak zValue="8", lista zSelectedIndex="0"). - Oba
Expandery mają tę samą metodęExpander_Expanded: rozwinięcie jednego zwija drugi. - Nagłówek sekcji „Szczegóły ceny” jest ustawiany z kodu przy każdym przeliczeniu — nawet zwinięta sekcja coś mówi.
- Okno ma stałą szerokość i
SizeToContent="Height", więc rośnie i maleje razem z rozwijanymi sekcjami.
Sprawdź trzy rzeczy
- Zaznacz monitor, a potem odznacz pole w nagłówku ramki dodatków. Monitor zostaje zaznaczony, ale cena spada.
- Usuń
SizeToContent="Height"i ustawHeight="480". Po rozwinięciu sekcji przycisk Zamów zniknie za dolną krawędzią. - Nadaj trzeciej ramce nazwę
RamkaDodatkowi wPrzeliczzamieńDodatki.IsEnablednaRamkaDodatkow.IsEnabled. Pole w nagłówku od razu jest wyszarzone i nie da się go zaznaczyć.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
GroupBox | kontrolka | Ramka z tytułem wokół jednego dziecka. |
Expander | kontrolka | Nagłówek ze strzałką i zwijana zawartość. |
Header | object | Tytuł: tekst albo kontrolka (<GroupBox.Header>). |
IsExpanded | bool | Czy sekcja jest rozwinięta; można ustawiać z kodu. |
ExpandDirection | wyliczenie | Down, Up, Left, Right. |
Expanded, Collapsed | zdarzenia | Po rozwinięciu i po zwinięciu; parametr RoutedEventArgs. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
dwa pola bezpośrednio w GroupBox | Błąd XAML — „Właściwość Content jest ustawiona więcej niż raz”. Włóż pola do panelu. |
IsEnabled="False" na GroupBox z polem w nagłówku | Wyłącza też nagłówek — grupy nie da się włączyć. Wyłączaj panel w środku. |
okno o stałej wysokości z kilkoma Expanderami | Rozwinięta zawartość jest ucięta. Użyj SizeToContent albo ScrollViewer. |
| odczyt pól ze zwiniętej sekcji jako „pustych” | Zwinięta zawartość istnieje i zachowuje wartości — jest tylko niewidoczna. |
Header zmieniany w Expanded na tekst z polami, które jeszcze są null | NullReferenceException przy IsExpanded="True" w XAML — dodaj osłonę jak w Przelicz. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Formularz zgłoszenia na szkolne zawody pływackie w trzech ramkach: „Zawodnik” (imię, nazwisko, klasa), „Konkurencje” (pola wyboru: 50 m kraulem, 50 m grzbietowym, 100 m dowolnym, sztafeta) i „Opiekun” (nazwisko i telefon). Przycisk Zgłoś sprawdza, czy pola są wypełnione i czy wybrano 1–2 konkurencje (regulamin), i pokazuje podsumowanie w oknie komunikatu.
pokaż wskazówkę
- Konkurencje policzysz pętlą po
Childrenpanelu z ramki, jak w lekcji 14.
Ramka „Adres zamieszkania” (ulica, kod, miasto) i druga ramka, w której nagłówku jest pole wyboru „Inny adres do korespondencji”. Dopóki pole nie jest zaznaczone, zawartość drugiej ramki jest wyszarzona i pokazuje na bieżąco kopię adresu zamieszkania. Po zaznaczeniu pola można wpisać inny adres. Kod pocztowy w obu ramkach jest sprawdzany (format 00-000) przyciskiem Zapisz.
pokaż wskazówkę
- Kopię zrób w
TextChangedpól adresu zamieszkania — tylko wtedy, gdy pole wyboru nie jest zaznaczone.
Okno „Pomoc — e-dziennik” z sześcioma pytaniami jako Expandery (np. „Jak usprawiedliwić nieobecność?”). Odpowiedź to zawijany tekst. Na górze dwa przyciski: Rozwiń wszystkie i Zwiń wszystkie, na dole licznik „Przeczytane: 3 z 6” — pytanie liczy się jako przeczytane po pierwszym rozwinięciu.
pokaż wskazówkę
- Umieść Expandery w jednym panelu i przechodź pętlą po jego
Children. Przeczytane zapamiętaj wTagsekcji.
Akordeon z trzech kroków: „1. Gdzie?” (sala, numer stanowiska), „2. Co nie działa?” (lista rodzaju usterki, opis), „3. Kontakt” (nazwisko, e-mail). Pod każdym krokiem przycisk Dalej, który sprawdza pola tego kroku i dopiero wtedy zwija go i rozwija następny. Nagłówek zwiniętego, poprawnie wypełnionego kroku zmienia się na podsumowanie, np. „1. Sala 14, stanowisko 7 ✔”. Przycisk Wyślij jest aktywny dopiero po trzech zaliczonych krokach.
pokaż wskazówkę
- Stan kroków trzymaj w tablicy
bool[3]. Akordeon zrób jedną metodąExpandedjak w teorii.
Na górze pole szukania i lista ofert praktyk zawodowych (klasa Oferta: firma, miasto, branża, liczba miejsc). Pod polem szukania Expander „Filtry” z trzema ramkami: „Miasto” (lista rozwijana z pozycją „wszystkie”), „Branża” (pola wyboru), „Tylko z wolnymi miejscami” (pole wyboru). Lista filtruje się przy każdej zmianie. Nagłówek zwiniętych filtrów pokazuje, ile filtrów jest aktywnych: „Filtry (2 aktywne)”.
pokaż wskazówkę
- Jedna metoda
Filtrujczyści listę i dodaje oferty, które przechodzą wszystkie warunki — jak w lekcji 23.
Ramki: „Cel” (przyciski opcji z ceną za osobę), „Uczestnicy” (liczba uczniów i opiekunów — suwaki; jeden opiekun na 15 uczniów, opiekunowie jadą za darmo), „Transport” (autokar 50 miejsc / bus 20 miejsc — program liczy, ile pojazdów potrzeba, i dolicza koszt). W nagłówku ramki „Ubezpieczenie” pole wyboru włączające wybór wariantu. Dwie sekcje zwijane: „Program dnia” (lista atrakcji z cenami do zaznaczenia) i „Rozliczenie”, w którym jest koszt całkowity i na ucznia. Przy za małej liczbie opiekunów ramka „Uczestnicy” dostaje czerwony nagłówek z ostrzeżeniem, a przycisk Zatwierdź jest wyłączony.
pokaż wskazówkę
- Liczba pojazdów: dzielenie z zaokrągleniem w górę —
(osoby + miejsca - 1) / miejscana liczbach całkowitych. - Czerwony nagłówek: ustaw z kodu
Headerna nowy tekst iForegroundramki — zmieni się też kolor pól w środku, więc przywróć go panelowi.
Co trzeba zapamiętać
GroupBox— ramka z tytułem; jedno dziecko, zwykle panel.Headermoże być kontrolką — wtedy wyłączaj panel w środku, nie ramkę.Expanderchowa zawartość;IsExpandedrozwija i zwija ją z kodu.Expanded/Collapsedpozwalają zbudować akordeon i podsumowanie w nagłówku.- Rozwijane sekcje potrzebują miejsca:
SizeToContentalboScrollViewer.
Dokumentacja: Microsoft Learn — GroupBox, Expander.