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

ToolBar i StatusBar

Menu z lekcji 35 zawiera wszystkie polecenia, ale najczęściej używane powinny być pod ręką — na pasku narzędzi pod menu. Z kolei na dole okna program może na bieżąco mówić, co się dzieje: pozycja kursora, liczba znaków, powiększenie. Tak wygląda Notatnik, Word czy Visual Studio. W tej lekcji zbudujesz edytor ogłoszeń z oboma paskami.

XAML C# ToolBar ToolBarTray ToggleButton StatusBar 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Dodasz pasek narzędzi z przyciskami, separatorami i listą rozwijaną.
  • Ułożysz kilka pasków narzędzi obok siebie.
  • Użyjesz przycisku, który pozostaje wciśnięty (ToggleButton).
  • Zbudujesz pasek stanu z kilkoma polami, także dosuniętymi do prawej.
  • Obsłużysz schowek i odczytasz pozycję kursora w polu tekstowym.
TEORIA

ToolBar i ToolBarTray

ToolBar to poziomy pasek, do którego wkłada się zwykłe kontrolki: przyciski, listy rozwijane, pola tekstowe. Pasek nadaje im płaski wygląd — przyciski nie mają ramki, dopóki nie najedziesz na nie myszą. Kilka pasków umieszcza się w ToolBarTray („tacy”), który ustawia je obok siebie:

MainWindow.xaml
<DockPanel>
    <ToolBarTray DockPanel.Dock="Top">
        <ToolBar>
            <Button Content="📄" ToolTip="Nowy" Click="Nowy_Click"/>
            <Button Content="💾" ToolTip="Zapisz" Click="Zapisz_Click"/>
            <Separator/>
            <Button Content="↶" ToolTip="Cofnij" Click="Cofnij_Click"/>
        </ToolBar>
        <ToolBar>
            <TextBlock Text="Klasa:" VerticalAlignment="Center"/>
            <ComboBox x:Name="Klasa" Width="70" SelectedIndex="0">
                <ComboBoxItem Content="1TI"/>
                <ComboBoxItem Content="2TI"/>
            </ComboBox>
        </ToolBar>
    </ToolBarTray>

    <TextBox x:Name="Tekst" AcceptsReturn="True"/>
</DockPanel>
Dziennik–□✕
📄💾↶ Klasa: 1TI ▾

dwa paski w jednej tacy; przycisk pod kursorem myszy dostaje ramkę, kropki z lewej to uchwyty do przesuwania

Paski w ToolBarTray można przeciągać za uchwyt i zmieniać ich kolejność. Gdy okno jest za wąskie, kontrolki, które się nie mieszczą, trafiają do rozwijanego menu ze strzałką na końcu paska.

Przyciski z samym znakiem są małe, ale bez podpisu niejasne — dlatego każdy ma ToolTip (dymek z lekcji 5). Kliknięcie obsługuje się zwykłym Click, często tą samą metodą co odpowiednia pozycja menu.

TEORIA

ToggleButton

ToggleButton wygląda jak przycisk, ale działa jak CheckBox: kliknięcie go wciska, kolejne — zwalnia. Ma te same IsChecked (typu bool?) oraz zdarzenia Click, Checked i Unchecked. To typowy przycisk „pogrubienie” albo „tryb nocny” na pasku narzędzi:

MainWindow.xaml
<ToggleButton x:Name="Pogrubienie" Content="B" FontWeight="Bold" Width="24"
              ToolTip="Pogrubienie" Click="Pogrubienie_Click"/>
MainWindow.xaml.cs
private void Pogrubienie_Click(object sender, RoutedEventArgs e)
{
    Tekst.FontWeight = Pogrubienie.IsChecked == true ? FontWeights.Bold : FontWeights.Normal;
}

Zwykły TextBox ma jedną czcionkę dla całego tekstu, więc pogrubienie dotyczy całego ogłoszenia. Formatowanie pojedynczych słów wymaga innej kontrolki (RichTextBox), która wykracza poza ten kurs.

TEORIA

StatusBar

StatusBar to pasek na dole okna. Każde pole to StatusBarItem — ma jedno dziecko (tekst, panel, pasek postępu…), a Separator rysuje między polami pionową kreskę:

MainWindow.xaml
<StatusBar DockPanel.Dock="Bottom">
    <StatusBarItem>
        <TextBlock x:Name="Stan" Text="Gotowe"/>
    </StatusBarItem>
    <Separator/>
    <StatusBarItem x:Name="Uczniow" Content="Uczniów: 0"/>

    <!-- ostatnie pole przy prawej krawędzi -->
    <StatusBarItem HorizontalAlignment="Right">
        <ProgressBar x:Name="Postep" Width="100" Height="12"/>
    </StatusBarItem>
</StatusBar>
Dziennik–□✕
GotoweUczniów: 28

trzy pola: tekst, liczba uczniów i pasek postępu dosunięty do prawej

W środku StatusBar działa jak DockPanel z lekcji 13: pola ustawiają się od lewej, a ostatnie zajmuje resztę miejsca. Dlatego HorizontalAlignment="Right" na ostatnim polu dosuwa je do prawej krawędzi.

Kolejność w DockPanel okna

ToolBarTray (Top) i StatusBar (Bottom) muszą być w XAML przed główną zawartością — ostatni element DockPanel wypełnia środek.

TEORIA

Schowek i kursor w TextBox

MainWindow.xaml.cs
Tekst.Cut();        // wytnij zaznaczenie do schowka
Tekst.Copy();       // kopiuj zaznaczenie
Tekst.Paste();      // wklej w miejscu kursora
Tekst.Undo();       // cofnij ostatnią zmianę
Tekst.SelectAll();  // zaznacz wszystko

// numer wiersza (od 0), w którym stoi kursor, i początek tego wiersza
int wiersz = Tekst.GetLineIndexFromCharacterIndex(Tekst.CaretIndex);
int poczatek = Tekst.GetCharacterIndexFromLineIndex(wiersz);
int kolumna = Tekst.CaretIndex - poczatek;

int zaznaczono = Tekst.SelectionLength;    // ile znaków jest zaznaczonych

Pole tekstowe ma zdarzenie SelectionChanged, które zachodzi przy każdej zmianie zaznaczenia — także przy zwykłym przesunięciu kursora strzałką lub kliknięciem. To dobre miejsce na odświeżenie pozycji kursora w pasku stanu.

Przy TextWrapping="Wrap" „wiersz” oznacza wiersz widoczny na ekranie, więc długi akapit zawinięty na trzy linijki to trzy wiersze. Zanim pole zostanie pierwszy raz wyświetlone, GetLineIndexFromCharacterIndex zwraca -1 — trzeba to sprawdzić.

PRZYKŁAD

Ogłoszenie na tablicę szkolną

Edytor krótkich ogłoszeń wydrukowanych na kolorowej kartce. Pasek narzędzi ma dwie części: plik i schowek oraz wygląd (pogrubienie, kursywa, kolor kartki). Pasek stanu pokazuje pozycję kursora, liczbę znaków z paskiem limitu 300 znaków, ostrzeżenie o zbliżaniu się do limitu i — przy prawej krawędzi — suwak rozmiaru tekstu.

Ogłoszenie na tablicę szkolną–□✕
📄✂⧉📋 BIKartka: zielona ▾
Szkolne koło informatyczne

Spotkanie w czwartek o 15:00 w sali 14.
Zapraszamy!
Wiersz 4, kolumna 12 Znaki: 79/300 18 pt

wciśnięte „B” (pogrubienie), zielona kartka, kursor na końcu ostatniego wiersza

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja36.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ogłoszenie na tablicę szkolną" Width="620" Height="440"
        MinWidth="520" MinHeight="300" WindowStartupLocation="CenterScreen">

    <DockPanel>

        <!-- dwa paski narzędzi obok siebie w jednej „tacce” -->
        <ToolBarTray DockPanel.Dock="Top">
            <ToolBar>
                <Button Content="📄" ToolTip="Nowe ogłoszenie" Click="Nowe_Click"/>
                <Separator/>
                <Button Content="✂" ToolTip="Wytnij" Click="Wytnij_Click"/>
                <Button Content="⧉" ToolTip="Kopiuj" Click="Kopiuj_Click"/>
                <Button Content="📋" ToolTip="Wklej" Click="Wklej_Click"/>
            </ToolBar>
            <ToolBar>
                <ToggleButton x:Name="Pogrubienie" Content="B" FontWeight="Bold" Width="24"
                              ToolTip="Pogrubienie" Click="Format_Click"/>
                <ToggleButton x:Name="Kursywa" Content="I" FontStyle="Italic" Width="24"
                              ToolTip="Kursywa" Click="Format_Click"/>
                <Separator/>
                <TextBlock Text="Kartka:" VerticalAlignment="Center" Margin="4,0"/>
                <ComboBox x:Name="Kartka" Width="100" SelectedIndex="0"
                          SelectionChanged="Kartka_SelectionChanged">
                    <ComboBoxItem Content="żółta"/>
                    <ComboBoxItem Content="zielona"/>
                    <ComboBoxItem Content="różowa"/>
                    <ComboBoxItem Content="niebieska"/>
                </ComboBox>
            </ToolBar>
        </ToolBarTray>

        <!-- pasek stanu na dole okna -->
        <StatusBar DockPanel.Dock="Bottom">
            <StatusBarItem>
                <TextBlock x:Name="Pozycja" Text="Wiersz 1, kolumna 1"/>
            </StatusBarItem>
            <Separator/>
            <StatusBarItem>
                <StackPanel Orientation="Horizontal">
                    <TextBlock x:Name="Znaki" Width="110"/>
                    <ProgressBar x:Name="Limit" Width="80" Height="10" Maximum="300"/>
                </StackPanel>
            </StatusBarItem>
            <Separator/>
            <StatusBarItem x:Name="Uwaga"/>

            <!-- ostatni element dosunięty do prawej -->
            <StatusBarItem HorizontalAlignment="Right">
                <StackPanel Orientation="Horizontal">
                    <TextBlock x:Name="RozmiarOpis" Width="40"/>
                    <Slider x:Name="Rozmiar" Width="100" Minimum="12" Maximum="32" Value="18"
                            IsSnapToTickEnabled="True" TickFrequency="2"
                            ValueChanged="Rozmiar_ValueChanged"/>
                </StackPanel>
            </StatusBarItem>
        </StatusBar>

        <!-- kartka z ogłoszeniem -->
        <Border x:Name="Tlo" Margin="20" Padding="12" Background="#FFFFF59D"
                BorderBrush="#33000000" BorderThickness="1">
            <TextBox x:Name="Tekst" AcceptsReturn="True" TextWrapping="Wrap"
                     Background="Transparent" BorderThickness="0" FontSize="18"
                     VerticalScrollBarVisibility="Auto" MaxLength="300"
                     TextChanged="Tekst_TextChanged" SelectionChanged="Tekst_SelectionChanged"/>
        </Border>
    </DockPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Lekcja36;

public partial class MainWindow : Window
{
    // kolory kartek w tej samej kolejności co pozycje listy „Kartka”
    private Color[] kolory =
    {
        Color.FromRgb(255, 245, 157),   // żółta
        Color.FromRgb(197, 225, 165),   // zielona
        Color.FromRgb(248, 187, 208),   // różowa
        Color.FromRgb(179, 229, 252)    // niebieska
    };

    public MainWindow()
    {
        InitializeComponent();
        RozmiarOpis.Text = $"{Rozmiar.Value} pt";
        Tekst.Text = "Szkolne koło informatyczne\n\nSpotkanie w czwartek o 15:00 w sali 14.\nZapraszamy!";
        Tekst.Focus();
    }

    // ---------- pasek narzędzi ----------

    private void Nowe_Click(object sender, RoutedEventArgs e)
    {
        if (Tekst.Text != "")
        {
            MessageBoxResult odp = MessageBox.Show("Wyczyścić ogłoszenie?", "Nowe",
                MessageBoxButton.YesNo, MessageBoxImage.Question);
            if (odp != MessageBoxResult.Yes)
            {
                return;
            }
        }
        Tekst.Clear();
        Pogrubienie.IsChecked = false;
        Kursywa.IsChecked = false;
        Format_Click(sender, e);
        Tekst.Focus();
    }

    // TextBox ma gotowe metody schowka
    private void Wytnij_Click(object sender, RoutedEventArgs e)
    {
        Tekst.Cut();
        Tekst.Focus();
    }

    private void Kopiuj_Click(object sender, RoutedEventArgs e)
    {
        Tekst.Copy();
        Tekst.Focus();
    }

    private void Wklej_Click(object sender, RoutedEventArgs e)
    {
        Tekst.Paste();
        Tekst.Focus();
    }

    // ToggleButton pamięta stan wciśnięcia — jak CheckBox
    private void Format_Click(object sender, RoutedEventArgs e)
    {
        Tekst.FontWeight = Pogrubienie.IsChecked == true ? FontWeights.Bold : FontWeights.Normal;
        Tekst.FontStyle = Kursywa.IsChecked == true ? FontStyles.Italic : FontStyles.Normal;
        Tekst.Focus();
    }

    private void Kartka_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (Tlo != null && Kartka.SelectedIndex >= 0)
        {
            Tlo.Background = new SolidColorBrush(kolory[Kartka.SelectedIndex]);
        }
    }

    // ---------- pasek stanu ----------

    private void Tekst_TextChanged(object sender, TextChangedEventArgs e)
    {
        int ile = Tekst.Text.Length;
        Znaki.Text = $"Znaki: {ile}/{Tekst.MaxLength}";
        Limit.Value = ile;

        if (ile >= Tekst.MaxLength)
        {
            Uwaga.Content = "Osiągnięto limit znaków";
            Uwaga.Foreground = Brushes.Firebrick;
        }
        else if (ile > Tekst.MaxLength - 30)
        {
            Uwaga.Content = $"Zostało {Tekst.MaxLength - ile} znaków";
            Uwaga.Foreground = Brushes.DarkOrange;
        }
        else
        {
            Uwaga.Content = "";
        }
    }

    // SelectionChanged pola tekstowego zachodzi także przy każdym ruchu kursora
    private void Tekst_SelectionChanged(object sender, RoutedEventArgs e)
    {
        int wiersz = Tekst.GetLineIndexFromCharacterIndex(Tekst.CaretIndex);
        if (wiersz < 0)
        {
            return;     // pole nie jest jeszcze wyświetlone
        }
        int poczatekWiersza = Tekst.GetCharacterIndexFromLineIndex(wiersz);
        int kolumna = Tekst.CaretIndex - poczatekWiersza;

        Pozycja.Text = $"Wiersz {wiersz + 1}, kolumna {kolumna + 1}";
        if (Tekst.SelectionLength > 0)
        {
            Pozycja.Text += $" (zaznaczono {Tekst.SelectionLength})";
        }
    }

    private void Rozmiar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        if (Tekst == null)
        {
            return;     // zdarzenie z czasu wczytywania XAML
        }
        Tekst.FontSize = Rozmiar.Value;
        RozmiarOpis.Text = $"{Rozmiar.Value} pt";
    }
}

Co dzieje się po kolei

  • Okno to DockPanel: taca z paskami u góry, pasek stanu na dole, kartka (Border z polem tekstowym) w środku.
  • Przyciski schowka wywołują gotowe metody pola tekstowego i oddają mu fokus — po kliknięciu paska narzędzi można od razu pisać dalej.
  • Oba przyciski ToggleButton obsługuje jedna metoda Format_Click, która ustawia czcionkę na podstawie stanu obu przycisków.
  • Kolory kartek są w tablicy kolory, w tej samej kolejności co pozycje listy — numer wybranej pozycji jest numerem koloru.
  • Tekst_TextChanged aktualizuje licznik, pasek limitu i ostrzeżenie. Samo ograniczenie do 300 znaków zapewnia MaxLength pola.
  • Tekst_SelectionChanged wylicza wiersz i kolumnę kursora oraz dopisuje liczbę zaznaczonych znaków.
  • Suwak w ostatnim polu paska stanu zmienia rozmiar czcionki; osłona Tekst == null chroni przed zdarzeniem z czasu wczytywania XAML.

Sprawdź trzy rzeczy

  1. Zwężaj okno. Gdy kontrolki przestaną się mieścić, na końcu paska narzędzi pojawi się strzałka z ukrytymi przyciskami.
  2. Przeciągnij drugi pasek narzędzi za kropkowany uchwyt przed pierwszy.
  3. Przenieś w XAML StatusBar za Border. Pasek stanu zajmie środek okna, a kartka ułoży się z lewej.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
ToolBarkontrolkaPasek narzędzi z kontrolkami w płaskim stylu.
ToolBarTraykontrolkaTaca na kilka pasków; pozwala je przesuwać.
ToggleButtonkontrolkaPrzycisk wciskany i zwalniany; IsChecked typu bool?.
StatusBar, StatusBarItemkontrolkiPasek stanu i jego pola.
SeparatorkontrolkaKreska między grupami przycisków lub polami stanu.
Cut, Copy, Paste, Undo, SelectAllmetody TextBoxSchowek, cofanie, zaznaczenie.
GetLineIndexFromCharacterIndexmetodaNumer wiersza (od 0) dla pozycji znaku; -1 przed wyświetleniem.
GetCharacterIndexFromLineIndexmetodaPozycja pierwszego znaku wiersza.
SelectionLengthintLiczba zaznaczonych znaków.
TextBox.SelectionChangedzdarzenieZmiana zaznaczenia lub ruch kursora.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
StatusBar w XAML po głównej zawartości DockPanelPasek stanu wypełnia środek. Elementy z Dock muszą być przed ostatnim.
GetCharacterIndexFromLineIndex(-1)ArgumentOutOfRangeException przy starcie — sprawdź, czy wiersz nie jest ujemny.
przycisk paska bez ToolTipSam znak bywa niezrozumiały; egzaminator też tego nie zgadnie.
if (Pogrubienie.IsChecked)CS0266 — ToggleButton.IsChecked to bool?, porównaj z true.
po kliknięciu przycisku paska nie da się pisaćFokus został na przycisku — wywołaj Tekst.Focus().
ZADANIA

Zadania

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

ZAD 1Licznik słów do wypracowania★☆☆

Wielowierszowe pole i pasek stanu z polami: liczba słów, liczba znaków (ze spacjami i bez), liczba akapitów, szacowany czas czytania (200 słów na minutę). Przy prawej krawędzi pole „Cel: 250 słów” z paskiem postępu, który robi się zielony po osiągnięciu celu.

pokaż wskazówkę
  • Słowa: Split po spacjach i znakach nowej linii z StringSplitOptions.RemoveEmptyEntries.
ZAD 2Paleta rysownika★☆☆

Rozbuduj rysowanie myszą z lekcji 25 albo 29 o pasek narzędzi: przyciski kolorów (kwadraty w kolorze, z dymkiem z nazwą), dwa ToggleButtony „pióro” i „gumka” (wciśnięty może być tylko jeden), lista grubości i przycisk „wyczyść”. Pasek stanu: współrzędne kursora i aktualne narzędzie.

pokaż wskazówkę
  • Kliknięcie jednego ToggleButton ustawia drugiemu IsChecked = false.
ZAD 3Przeglądarka zdjęć klasy★★☆

Okno z obrazem (kilka obrazów w zasobach projektu). Pasek narzędzi: ◀ ▶ (poprzedni/następny, wyłączone na końcach), „pokaz slajdów” jako ToggleButton (co 3 s następne zdjęcie, DispatcherTimer), lista sposobu dopasowania (Stretch). Pasek stanu: nazwa pliku, „zdjęcie 3 z 8”, a przy prawej krawędzi pasek postępu do kolejnego slajdu.

pokaż wskazówkę
  • Timer co 100 ms: zwiększa pasek postępu i po 30 krokach przechodzi do następnego zdjęcia.
ZAD 4Kasa sklepiku szkolnego★★☆

Siatka przycisków produktów (drożdżówka, woda, kanapka…) i lista paragonu. Pasek narzędzi: „nowy paragon”, „usuń pozycję”, ToggleButton „zniżka 10%” dla nauczycieli, pole „wpłacono” z przyciskiem „reszta”. Pasek stanu: liczba pozycji, suma, godzina (zegar na DispatcherTimer) i — przy prawej — liczba paragonów od początku dnia.

pokaż wskazówkę
  • Produkty zapisz w List obiektów klasy Pozycja (nazwa, cena) — suma to pętla po liście.
ZAD 5Szyfrator wiadomości★★☆

Dwa pola: tekst jawny i zaszyfrowany. Pasek narzędzi: lista szyfrów (Cezar, ROT13, odwrócenie tekstu), pole przesunięcia dla Cezara (aktywne tylko dla niego), przyciski „szyfruj ↓” i „odszyfruj ↑”, „kopiuj wynik”, „zamień pola”. Pasek stanu: liczba znaków w obu polach i komunikat ostatniej operacji („Zaszyfrowano 42 znaki szyfrem Cezara, przesunięcie 3”).

pokaż wskazówkę
  • Polskie litery zostaw bez zmian — szyfruj tylko A–Z i a–z.
ZAD 6Mini-edytor z menu, paskami i zapisem stanu★★★

Połącz lekcje 35 i 36: edytor notatek z menu (Plik, Edycja, Widok), paskiem narzędzi i paskiem stanu. Te same polecenia są w menu i na pasku i wywołują te same metody. W menu „Widok” pozycje zaznaczane „Pasek narzędzi” i „Pasek stanu” ukrywają i pokazują paski. Przycisk „pogrubienie” na pasku i pozycja „Pogrubienie” w menu muszą zawsze pokazywać ten sam stan. Pasek stanu: wiersz i kolumna, liczba słów, informacja „zmieniono” po każdej zmianie od ostatniego „Nowy”.

pokaż wskazówkę
  • Jedna metoda UstawPogrubienie(bool) zmienia czcionkę oraz IsChecked przycisku i pozycji menu — wywołują ją oba miejsca.
PODSUMOWANIE

Co trzeba zapamiętać

  • ToolBar w ToolBarTray u góry, StatusBar na dole — oba przed główną zawartością DockPanel.
  • Przyciski paska narzędzi potrzebują ToolTip; po kliknięciu oddaj fokus polu roboczemu.
  • ToggleButton to przycisk z IsChecked jak w CheckBox.
  • Ostatnie pole paska stanu z HorizontalAlignment="Right" trafia do prawej krawędzi.
  • TextBox ma metody schowka, SelectionChanged i metody do wyliczenia wiersza kursora.

Dokumentacja: Microsoft Learn — ToolBar, StatusBar.

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