TabControl — zakładki
Gdy formularz ma kilka niezależnych części, nie trzeba ich wszystkich pokazywać naraz. Zakładki dzielą okno na „strony”, z których widać jedną — tak działają ustawienia systemu, okno właściwości pliku czy kreatory instalacji. W tej lekcji zbudujesz kreator rejestracji, w którym kolejne zakładki odblokowują się dopiero po poprawnym wypełnieniu poprzednich.
Czego się dziś nauczysz
- Podzielisz okno na zakładki z własną zawartością.
- Przełączysz zakładkę z kodu i odczytasz, która jest aktywna.
- Zablokujesz zakładkę i zmienisz jej nagłówek w trakcie działania programu.
- Odróżnisz zdarzenie zakładek od zdarzeń list leżących w zakładce.
- Zbudujesz kreator „krok po kroku” z przyciskami Wstecz i Dalej.
TabControl i TabItem
TabControl zawiera zakładki TabItem. Każda zakładka ma nagłówek Header i — jak GroupBox z lekcji 33 — jedno dziecko, zwykle panel:
<TabControl x:Name="Zakladki" Margin="10">
<TabItem Header="Ogólne">
<StackPanel Margin="10">
<TextBlock Text="Nazwa klasy:"/>
<TextBox x:Name="Klasa" Text="3TI"/>
</StackPanel>
</TabItem>
<TabItem Header="Uczniowie">
<ListBox x:Name="Uczniowie" Margin="10"/>
</TabItem>
<TabItem Header="Statystyki" IsEnabled="False">
<TextBlock Text="Brak danych" Margin="10"/>
</TabItem>
</TabControl>
aktywna zakładka „Ogólne”; „Statystyki” są wyłączone
Zawartość niewidocznych zakładek nadal istnieje: pola zachowują wpisany tekst, a kod może je czytać i zmieniać w każdej chwili, tak jak zawartość zwiniętego Expandera.
Przełączanie z kodu
TabControl zachowuje się jak ListBox z lekcji 17 — ma SelectedIndex i SelectedItem:
int nr = Zakladki.SelectedIndex; // 0 — pierwsza zakładka
Zakladki.SelectedIndex = 1; // przejdź do drugiej
Zakladki.SelectedItem = TabStatystyki; // przejdź do zakładki o tej nazwie (x:Name)
TabStatystyki.IsEnabled = true; // odblokowanie zakładki
TabStatystyki.Header = "Statystyki (24)";
TabStatystyki.IsSelected = true; // trzeci sposób przełączenia
int ile = Zakladki.Items.Count; // liczba zakładek
Wyłączonej zakładki (IsEnabled="False") użytkownik nie kliknie, ale kod nadal może ją wybrać — dlatego w kreatorze najpierw włączamy zakładkę, a dopiero potem na nią przechodzimy.
SelectionChanged i e.Source
Zmianę zakładki zgłasza zdarzenie SelectionChanged — to samo, które znasz z ComboBox i ListBox. I tu jest pułapka: zdarzenie to „wędruje w górę” drzewa elementów. Gdy użytkownik wybierze coś z listy rozwijanej leżącej w zakładce, metoda zakładek też się wywoła. Kto naprawdę zmienił wybór, mówi e.Source:
// w XAML: <TabControl x:Name="Zakladki" SelectionChanged="Zakladki_SelectionChanged">
private void Zakladki_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.Source != Zakladki) // zmiana w liście wewnątrz zakładki — nie nasza sprawa
{
return;
}
Title = $"Dziennik — {((TabItem)Zakladki.SelectedItem).Header}";
}
sender to zawsze element, do którego przypięliśmy metodę (tu: Zakladki), więc sprawdzanie sender nie pomoże. e.Source to element, w którym zdarzenie się zaczęło.
Objaw bez sprawdzenia
Bez warunku z e.Source każdy wybór godziny z listy w zakładce „Warsztaty” uruchamiałby kod przeznaczony dla przejścia między zakładkami. Jeśli ten kod liczy coś, pyta o coś albo czyści pola, program zachowuje się „bez powodu”.
Położenie i nagłówki
<TabControl TabStripPlacement="Left">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="⚙" Margin="0,0,6,0"/>
<TextBlock Text="Ustawienia"/>
</StackPanel>
</TabItem.Header>
<TextBlock Text="…" Margin="10"/>
</TabItem>
</TabControl>
TabStripPlacement (Top, Bottom, Left, Right) przenosi pasek zakładek na inną krawędź. Nagłówek, tak jak w GroupBox, może być dowolną zawartością — tu znak i tekst obok siebie.
TabStripPlacement="Left" — zakładki z lewej strony
Rejestracja na Dzień Otwarty
Kandydat rejestruje się na Dzień Otwarty technikum w trzech krokach. Zakładki „Warsztaty” i „Potwierdzenie” są na początku wyłączone; przycisk Dalej sprawdza bieżący krok, oznacza poprawny krok znakiem ✔ i odblokowuje następny. Na ostatniej zakładce przycisk zmienia się w Wyślij i działa dopiero po zaznaczeniu zgody.
Kontakt: wisniewscy@poczta.pl
Przyjazd: 11:00
Warsztaty:
• Programowanie gier
• Robotyka
Czas zajęć: 60 min
ostatni krok: dwa poprzednie mają znaczek ✔, przycisk „Dalej” zmienił się w „Wyślij”
<Window x:Class="Lekcja34.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Dzień Otwarty — rejestracja" Width="460" Height="400"
ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
<DockPanel Margin="12">
<!-- przyciski nawigacji pod zakładkami -->
<DockPanel DockPanel.Dock="Bottom" Margin="0,10,0,0">
<Button x:Name="Dalej" DockPanel.Dock="Right" Content="Dalej ›" Width="90"
Click="Dalej_Click"/>
<Button x:Name="Wstecz" Content="‹ Wstecz" Width="90" HorizontalAlignment="Left"
IsEnabled="False" Click="Wstecz_Click"/>
</DockPanel>
<TabControl x:Name="Zakladki" SelectionChanged="Zakladki_SelectionChanged">
<!-- krok 1 -->
<TabItem x:Name="TabUczen" Header="1. Uczeń">
<StackPanel Margin="10">
<TextBlock Text="Imię i nazwisko:"/>
<TextBox x:Name="Nazwisko"/>
<TextBlock Text="Szkoła podstawowa:" Margin="0,8,0,0"/>
<TextBox x:Name="Szkola"/>
<TextBlock Text="E-mail rodzica:" Margin="0,8,0,0"/>
<TextBox x:Name="Email"/>
<TextBlock x:Name="Blad1" Foreground="Firebrick" Margin="0,10,0,0"
TextWrapping="Wrap"/>
</StackPanel>
</TabItem>
<!-- krok 2: dostępny dopiero po poprawnym kroku 1 -->
<TabItem x:Name="TabWarsztaty" Header="2. Warsztaty" IsEnabled="False">
<StackPanel Margin="10">
<TextBlock Text="Wybierz od 1 do 3 warsztatów (po 30 min):"/>
<StackPanel x:Name="Warsztaty" Margin="0,6,0,0">
<CheckBox Content="Programowanie gier" Margin="0,2"/>
<CheckBox Content="Budowa sieci komputerowej" Margin="0,2"/>
<CheckBox Content="Grafika i druk 3D" Margin="0,2"/>
<CheckBox Content="Strony internetowe" Margin="0,2"/>
<CheckBox Content="Robotyka" Margin="0,2"/>
</StackPanel>
<TextBlock Text="Godzina przyjazdu:" Margin="0,10,0,2"/>
<ComboBox x:Name="Godzina" Width="120" HorizontalAlignment="Left">
<ComboBoxItem Content="9:00"/>
<ComboBoxItem Content="11:00"/>
<ComboBoxItem Content="13:00"/>
</ComboBox>
<TextBlock x:Name="Blad2" Foreground="Firebrick" Margin="0,10,0,0"
TextWrapping="Wrap"/>
</StackPanel>
</TabItem>
<!-- krok 3 -->
<TabItem x:Name="TabPotwierdzenie" Header="3. Potwierdzenie" IsEnabled="False">
<StackPanel Margin="10">
<Border Background="#FFF3F4F6" CornerRadius="6" Padding="10">
<TextBlock x:Name="Podsumowanie" TextWrapping="Wrap" LineHeight="20"/>
</Border>
<CheckBox x:Name="Zgoda" Margin="0,12,0,0" Click="Zgoda_Click">
<TextBlock TextWrapping="Wrap"
Text="Wyrażam zgodę na przetwarzanie danych w celu organizacji Dnia Otwartego."/>
</CheckBox>
</StackPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>
using System.Windows;
using System.Windows.Controls;
namespace Lekcja34;
public partial class MainWindow : Window
{
const int MaxWarsztatow = 3;
public MainWindow()
{
InitializeComponent();
}
private void Dalej_Click(object sender, RoutedEventArgs e)
{
if (Zakladki.SelectedIndex == 0)
{
if (SprawdzKrok1())
{
TabWarsztaty.IsEnabled = true;
Zakladki.SelectedIndex = 1; // przejście do następnej zakładki
}
}
else if (Zakladki.SelectedIndex == 1)
{
if (SprawdzKrok2())
{
TabPotwierdzenie.IsEnabled = true;
Zakladki.SelectedItem = TabPotwierdzenie; // to samo co SelectedIndex = 2
}
}
else
{
Wyslij();
}
}
private void Wstecz_Click(object sender, RoutedEventArgs e)
{
Zakladki.SelectedIndex--;
}
private void Zakladki_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// zdarzenie przychodzi też z listy rozwijanej leżącej w zakładce
if (e.Source != Zakladki)
{
return;
}
int nr = Zakladki.SelectedIndex;
Wstecz.IsEnabled = nr > 0;
if (nr == 2)
{
Dalej.Content = "Wyślij";
PokazPodsumowanie();
}
else
{
Dalej.Content = "Dalej ›";
Dalej.IsEnabled = true;
}
}
private void Zgoda_Click(object sender, RoutedEventArgs e)
{
PokazPodsumowanie();
}
// Sprawdza krok 1; nagłówek zakładki dostaje znaczek, gdy wszystko jest dobrze
private bool SprawdzKrok1()
{
string blad = "";
if (Nazwisko.Text.Trim() == "")
{
blad = "Wpisz imię i nazwisko.";
}
else if (Szkola.Text.Trim() == "")
{
blad = "Wpisz nazwę szkoły podstawowej.";
}
else if (!Email.Text.Contains('@') || !Email.Text.Contains('.'))
{
blad = "Adres e-mail jest niepoprawny.";
}
Blad1.Text = blad;
TabUczen.Header = blad == "" ? "1. Uczeń ✔" : "1. Uczeń";
return blad == "";
}
private bool SprawdzKrok2()
{
int ile = WybraneWarsztaty().Count;
string blad = "";
if (ile == 0)
{
blad = "Wybierz przynajmniej jeden warsztat.";
}
else if (ile > MaxWarsztatow)
{
blad = $"Możesz wybrać najwyżej {MaxWarsztatow} warsztaty (wybrano {ile}).";
}
else if (Godzina.SelectedIndex == -1)
{
blad = "Wybierz godzinę przyjazdu.";
}
Blad2.Text = blad;
TabWarsztaty.Header = blad == "" ? "2. Warsztaty ✔" : "2. Warsztaty";
return blad == "";
}
private List<string> WybraneWarsztaty()
{
List<string> wybrane = new List<string>();
foreach (CheckBox w in Warsztaty.Children)
{
if (w.IsChecked == true)
{
wybrane.Add($"{w.Content}");
}
}
return wybrane;
}
// Użytkownik mógł wrócić do kroku 1 lub 2 i coś zepsuć — sprawdzamy jeszcze raz
private void PokazPodsumowanie()
{
bool ok1 = SprawdzKrok1();
bool ok2 = SprawdzKrok2();
if (!ok1 || !ok2)
{
Podsumowanie.Text = "Dane w krokach bez znaczka ✔ wymagają poprawy.";
Dalej.IsEnabled = false;
return;
}
List<string> warsztaty = WybraneWarsztaty();
ComboBoxItem? godzina = Godzina.SelectedItem as ComboBoxItem;
string lista = "";
foreach (string w in warsztaty)
{
lista += $"\n • {w}";
}
Podsumowanie.Text =
$"Uczeń: {Nazwisko.Text.Trim()} ({Szkola.Text.Trim()})\n" +
$"Kontakt: {Email.Text.Trim()}\n" +
$"Przyjazd: {godzina?.Content}\n" +
$"Warsztaty:{lista}\n" +
$"Czas zajęć: {warsztaty.Count * 30} min";
Dalej.IsEnabled = Zgoda.IsChecked == true;
}
private void Wyslij()
{
MessageBox.Show($"Dziękujemy! Potwierdzenie wyślemy na adres {Email.Text.Trim()}.",
"Rejestracja przyjęta", MessageBoxButton.OK, MessageBoxImage.Information);
// nowy formularz dla kolejnej osoby
Nazwisko.Clear();
Szkola.Clear();
Email.Clear();
foreach (CheckBox w in Warsztaty.Children)
{
w.IsChecked = false;
}
Godzina.SelectedIndex = -1;
Zgoda.IsChecked = false;
TabUczen.Header = "1. Uczeń";
TabWarsztaty.Header = "2. Warsztaty";
TabWarsztaty.IsEnabled = false;
TabPotwierdzenie.IsEnabled = false;
Zakladki.SelectedIndex = 0;
Nazwisko.Focus();
}
}
Co dzieje się po kolei
- Przyciski nawigacji leżą w
DockPanelpod zakładkami, więc są widoczne niezależnie od wybranej zakładki. Dalej_Clicksprawdza numer aktywnej zakładki. Dla kroków 1 i 2 wywołuje metodę sprawdzającą; jeśli krok jest poprawny, włącza następną zakładkę i na nią przechodzi.SprawdzKrok1iSprawdzKrok2zwracająbool, pokazują błąd w zakładce i ustawiają nagłówek ze znakiem ✔ albo bez niego.Zakladki_SelectionChangednajpierw odrzuca zdarzenia z listyGodzina(e.Source), potem ustawia przyciski i — na ostatniej zakładce — buduje podsumowanie.- Użytkownik może kliknąć z powrotem zakładkę 1, skasować nazwisko i wrócić do 3. Dlatego
PokazPodsumowaniesprawdza oba kroki jeszcze raz i w razie błędu blokuje wysyłkę. - Po wysłaniu
Wyslijczyści pola i przywraca stan początkowy zakładek — formularz jest gotowy dla następnej osoby.
Sprawdź trzy rzeczy
- Przejdź do kroku 3, wróć do kroku 1 i wyczyść e-mail. Wróć do „Potwierdzenia” — wysyłka jest zablokowana, a znaczek ✔ przy kroku 1 zniknął.
- Usuń warunek z
e.Sourcei dopisz na końcuZakladki_SelectionChangedwierszMessageBox.Show("Zmiana zakładki");. W kroku 2 wybierz godzinę — komunikat się pojawi, choć zakładka się nie zmieniła. - Ustaw
TabStripPlacement="Left"naTabControli sprawdź, jak wygląda kreator z zakładkami z boku.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
TabControl | kontrolka | Pojemnik na zakładki; widać jedną naraz. |
TabItem | kontrolka | Zakładka: Header i jedno dziecko. |
SelectedIndex, SelectedItem | int, object | Aktywna zakładka — odczyt i przełączanie. |
TabItem.IsSelected | bool | Czy ta zakładka jest aktywna; true ją wybiera. |
SelectionChanged | zdarzenie | Zmiana zakładki — oraz zmiany list w środku. |
e.Source | object | Element, w którym zdarzenie się zaczęło. |
TabStripPlacement | wyliczenie | Top, Bottom, Left, Right. |
Items.Count | int | Liczba zakładek. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
dwa elementy bezpośrednio w TabItem | Błąd XAML — zawartość ustawiona więcej niż raz. Użyj panelu. |
SelectionChanged zakładek bez sprawdzenia e.Source | Metoda uruchamia się też po zmianie w ComboBox/ListBox w zakładce. |
if (sender != Zakladki) return; | Nic nie daje — sender to zawsze Zakladki. Sprawdzaj e.Source. |
Zakladki.SelectedIndex = 3; przy trzech zakładkach | Nic się nie dzieje albo wybór znika — numeracja od 0, ostatnia ma numer Items.Count - 1. |
| sprawdzanie danych tylko przy przycisku Dalej | Użytkownik może wrócić i zmienić dane, klikając nagłówki. Sprawdź jeszcze raz przed wysłaniem. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Trzy zakładki: „Długość” (cm ↔ cale), „Masa” (kg ↔ funty), „Temperatura” (°C ↔ °F). Każda zakładka ma pole, dwa przyciski kierunku i wynik. Tytuł okna pokazuje nazwę aktywnej zakładki: „Przelicznik — Masa”.
pokaż wskazówkę
- Tytuł zmieniaj w
SelectionChanged— z warunkiem nae.Source, choć tu nie ma list (przyda się, gdy je dodasz).
Okno z zakładkami „Dane”, „Oceny” i „Uwagi”. W „Ocenach” lista przedmiotów (ComboBox) i pole oceny z przyciskiem Dodaj — oceny trafiają do listy w tej samej zakładce. Nagłówek zakładki pokazuje liczbę ocen i średnią: „Oceny (5, śr. 4,20)”, a nagłówek „Uwagi” — liczbę uwag. Zmiana przedmiotu w liście nie może zmieniać tytułu okna ustawianego przy przełączaniu zakładek.
pokaż wskazówkę
- Średnią licz w polach klasy (suma, liczba), a nagłówek ustawiaj po każdym dodaniu.
Zakładki „Zupy”, „Dania główne”, „Desery”, „Napoje” — w każdej pola wyboru z cenami w Tag. Pod zakładkami (zawsze widoczne) zamówienie i suma, aktualizowane przy każdym zaznaczeniu w dowolnej zakładce. Nagłówki zakładek, w których coś zaznaczono, są pogrubione. Przycisk Zamów wymaga przynajmniej jednego dania głównego.
pokaż wskazówkę
- Przejdź pętlą po
Zakladki.Items(elementy rzutuj naTabItem), a w każdej po panelu zContentzakładki.
Kreator w czterech zakładkach z przyciskami Wstecz/Dalej: „Dane” (imię, nazwisko, PESEL — 11 cyfr), „Zdjęcie” (wybór jednego z trzech obrazów z zasobów projektu, podgląd), „Odbiór” (sekretariat / pocztą z adresem — pola adresu tylko przy poczcie), „Podsumowanie”. Pasek postępu pod zakładkami pokazuje numer kroku. Zakładki są dostępne tylko do kroku, który już osiągnięto.
pokaż wskazówkę
- Pole klasy
osiagnietyKrok; po każdym przejściu włączaj zakładki o numerze nie większym niż ono.
Okno ustawień programu do nauki słówek: zakładki z lewej strony z nagłówkami „znak + tekst” („🔤 Język”, „⏱ Sesja”, „🎨 Wygląd”). Na dole przyciski OK, Anuluj i Zastosuj; Zastosuj jest aktywny tylko po jakiejkolwiek zmianie. Zakładka ze zmienionymi, niezastosowanymi ustawieniami ma w nagłówku gwiazdkę („⏱ Sesja *”). Anuluj pyta o porzucenie zmian, jeśli jakieś są.
pokaż wskazówkę
- Dla każdej zakładki pamiętaj w
Tagjej nagłówek bez gwiazdki — łatwo go wtedy przywrócić.
Szkolny turniej siatkówki: zakładki „Grupa A”, „Grupa B”, „Grupa C”, budowane w kodzie w pętli (po 4 klasy w grupie, nazwy z tablicy). W każdej zakładce: dwie listy rozwijane z drużynami, pola na wynik w setach (2:0, 2:1…), przycisk Zapisz mecz i tabela grupy (klasa Druzyna: punkty, sety wygrane i przegrane), posortowana według punktów. Dodatkowa zakładka „Półfinały” odblokowuje się, gdy we wszystkich grupach rozegrano wszystkie mecze (6 w grupie), i pokazuje zwycięzców grup oraz najlepszą drużynę z drugich miejsc.
pokaż wskazówkę
- Zakładkę z kodu tworzysz jak każdy element:
new TabItem { Header = …, Content = panel }iZakladki.Items.Add(...). - Każdej zakładce nadaj w
Tagnumer grupy — jedna metodaClickobsłuży wszystkie przyciski.
Co trzeba zapamiętać
TabControlzawieraTabItemy; każdy maHeaderi jedno dziecko.SelectedIndex/SelectedItem/IsSelected— odczyt i przełączanie z kodu.- Wyłączoną zakładkę wybierze tylko kod — kreator najpierw ją włącza, potem przechodzi.
SelectionChangedprzychodzi też z list w zakładkach — sprawdzaje.Source.- Zawartość ukrytych zakładek istnieje — dane trzeba sprawdzić jeszcze raz przed wysłaniem.
Dokumentacja: Microsoft Learn — TabControl.