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

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.

XAML C# TabControl TabItem SelectedIndex e.Source 90 min
CEL LEKCJI

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.
TEORIA

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:

MainWindow.xaml
<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>
Dziennik klasy–□✕
Ogólne Uczniowie Statystyki
Nazwa klasy:
3TI

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.

TEORIA

Przełączanie z kodu

TabControl zachowuje się jak ListBox z lekcji 17 — ma SelectedIndex i SelectedItem:

MainWindow.xaml.cs
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.

TEORIA

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:

MainWindow.xaml.cs
// 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”.

TEORIA

Położenie i nagłówki

MainWindow.xaml
<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.

Ustawienia–□✕
⚙ Ustawienia 👤 Konto 🔔 Powiadomienia
…

TabStripPlacement="Left" — zakładki z lewej strony

PRZYKŁAD

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.

Dzień Otwarty — rejestracja–✕
1. Uczeń ✔ 2. Warsztaty ✔ 3. Potwierdzenie
Uczeń: Kacper Wiśniewski (SP nr 12)
Kontakt: wisniewscy@poczta.pl
Przyjazd: 11:00
Warsztaty:
   • Programowanie gier
   • Robotyka
Czas zajęć: 60 min
Wyrażam zgodę na przetwarzanie danych w celu organizacji Dnia Otwartego.
‹ WsteczWyślij

ostatni krok: dwa poprzednie mają znaczek ✔, przycisk „Dalej” zmienił się w „Wyślij”

MainWindow.xaml — pełny plik
<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>
MainWindow.xaml.cs — pełny plik
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 DockPanel pod zakładkami, więc są widoczne niezależnie od wybranej zakładki.
  • Dalej_Click sprawdza 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.
  • SprawdzKrok1 i SprawdzKrok2 zwracają bool, pokazują błąd w zakładce i ustawiają nagłówek ze znakiem ✔ albo bez niego.
  • Zakladki_SelectionChanged najpierw odrzuca zdarzenia z listy Godzina (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 PokazPodsumowanie sprawdza oba kroki jeszcze raz i w razie błędu blokuje wysyłkę.
  • Po wysłaniu Wyslij czyści pola i przywraca stan początkowy zakładek — formularz jest gotowy dla następnej osoby.

Sprawdź trzy rzeczy

  1. 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ął.
  2. Usuń warunek z e.Source i dopisz na końcu Zakladki_SelectionChanged wiersz MessageBox.Show("Zmiana zakładki");. W kroku 2 wybierz godzinę — komunikat się pojawi, choć zakładka się nie zmieniła.
  3. Ustaw TabStripPlacement="Left" na TabControl i sprawdź, jak wygląda kreator z zakładkami z boku.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
TabControlkontrolkaPojemnik na zakładki; widać jedną naraz.
TabItemkontrolkaZakładka: Header i jedno dziecko.
SelectedIndex, SelectedItemint, objectAktywna zakładka — odczyt i przełączanie.
TabItem.IsSelectedboolCzy ta zakładka jest aktywna; true ją wybiera.
SelectionChangedzdarzenieZmiana zakładki — oraz zmiany list w środku.
e.SourceobjectElement, w którym zdarzenie się zaczęło.
TabStripPlacementwyliczenieTop, Bottom, Left, Right.
Items.CountintLiczba zakładek.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
dwa elementy bezpośrednio w TabItemBłąd XAML — zawartość ustawiona więcej niż raz. Użyj panelu.
SelectionChanged zakładek bez sprawdzenia e.SourceMetoda 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ładkachNic się nie dzieje albo wybór znika — numeracja od 0, ostatnia ma numer Items.Count - 1.
sprawdzanie danych tylko przy przycisku DalejUżytkownik może wrócić i zmienić dane, klikając nagłówki. Sprawdź jeszcze raz przed wysłaniem.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Przelicznik jednostek★☆☆

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 na e.Source, choć tu nie ma list (przyda się, gdy je dodasz).
ZAD 2Karta ucznia★☆☆

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.
ZAD 3Menu restauracji szkolnej★★☆

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 na TabItem), a w każdej po panelu z Content zakładki.
ZAD 4Kreator zamówienia legitymacji★★☆

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.
ZAD 5Ustawienia z zakładkami z boku★★☆

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 Tag jej nagłówek bez gwiazdki — łatwo go wtedy przywrócić.
ZAD 6Turniej klas — tabela grup★★★

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 } i Zakladki.Items.Add(...).
  • Każdej zakładce nadaj w Tag numer grupy — jedna metoda Click obsłuży wszystkie przyciski.
PODSUMOWANIE

Co trzeba zapamiętać

  • TabControl zawiera TabItemy; każdy ma Header i 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.
  • SelectionChanged przychodzi też z list w zakładkach — sprawdzaj e.Source.
  • Zawartość ukrytych zakładek istnieje — dane trzeba sprawdzić jeszcze raz przed wysłaniem.

Dokumentacja: Microsoft Learn — TabControl.

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