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

GridSplitter i Viewbox

Użytkownicy mają różne ekrany i różne potrzeby: jeden chce szerszej listy, drugi — większego podglądu. Zamiast zgadywać idealne proporcje, możesz pozwolić przeciągać granicę między częściami okna, tak jak w Eksploratorze plików. A treść, która ma być zawsze w całości widoczna — tablica wyników, zegar, plansza gry — może skalować się razem z oknem. Służą do tego dwie kontrolki z tej lekcji.

XAML C# GridSplitter Viewbox Stretch MinWidth 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Dodasz pasek, którym użytkownik zmienia szerokość kolumn siatki.
  • Dodasz taki sam pasek między wierszami.
  • Ograniczysz przeciąganie minimalnym rozmiarem kolumn i wierszy.
  • Zbudujesz treść, która skaluje się razem z oknem.
  • Dobierzesz sposób skalowania: z zachowaniem proporcji albo bez.
TEORIA

GridSplitter między kolumnami

GridSplitter to wąski pasek w Grid (lekcja 9). Przeciągnięcie go zmienia szerokość sąsiednich kolumn. Najprościej dać mu własną, wąską kolumnę Auto:

MainWindow.xaml
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200" MinWidth="120"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*" MinWidth="200"/>
    </Grid.ColumnDefinitions>

    <ListBox x:Name="Lista" Grid.Column="0"/>

    <GridSplitter Grid.Column="1" Width="5"
                  HorizontalAlignment="Center" VerticalAlignment="Stretch"/>

    <TextBox x:Name="Podglad" Grid.Column="2" TextWrapping="Wrap"/>
</Grid>
Notatki–□✕
Matematyka
Fizyka
Historia
⋮
Zasady dynamiki Newtona…

kursor nad paskiem zmienia się w strzałkę ⇔; przeciągnięcie przesuwa granicę

Trzy rzeczy decydują o tym, że pasek działa:

  • szerokość — Width="5", inaczej pasek ma szerokość zero i nie da się go chwycić;
  • wyrównanie — HorizontalAlignment="Center" (lub Left/Right) i VerticalAlignment="Stretch" mówią, że pasek jest pionowy i zmienia kolumny;
  • sąsiednie kolumny — pasek w kolumnie 1 zmienia kolumny 0 i 2.

MinWidth i MaxWidth kolumn ograniczają przeciąganie — lista nie zniknie całkiem, a podgląd zachowa co najmniej 200 pikseli.

TEORIA

GridSplitter między wierszami

Pasek poziomy działa tak samo, tylko role się zamieniają: wiersz Auto, Height zamiast Width, rozciągnięcie w poziomie:

MainWindow.xaml
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" MinHeight="100"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="120" MinHeight="50"/>
    </Grid.RowDefinitions>

    <TextBox x:Name="Kod" Grid.Row="0" AcceptsReturn="True"/>

    <GridSplitter Grid.Row="1" Height="5"
                  HorizontalAlignment="Stretch" VerticalAlignment="Center"/>

    <TextBox x:Name="Wynik" Grid.Row="2" IsReadOnly="True"/>
</Grid>

Pasek zmienia tylko bezpośrednich sąsiadów

Jeśli nad paskiem jest wiersz Auto z przyciskami, to on — a nie wiersz z treścią — zostanie rozciągnięty. Elementy, które mają się przesuwać razem, połącz w panel (np. DockPanel) w jednym wierszu. Tak zrobiono w przykładzie: przyciski są w tym samym wierszu co tablica wyników.

Właściwość ShowsPreview="True" sprawia, że podczas przeciągania widać tylko cień paska, a kolumny zmieniają się dopiero po puszczeniu przycisku myszy — przydaje się, gdy zawartość wolno się przerysowuje.

TEORIA

Viewbox

Viewbox ma jedno dziecko i skaluje je tak, żeby wypełniło dostępne miejsce — razem z czcionkami, grubością linii i odstępami. Dziecko projektuje się w stałym rozmiarze, a Viewbox je powiększa lub pomniejsza:

MainWindow.xaml
<Viewbox Margin="10">
    <StackPanel Width="200">
        <TextBlock x:Name="Godzina" Text="12:45" FontSize="60" HorizontalAlignment="Center"/>
        <TextBlock Text="do dzwonka: 5 min" FontSize="16" HorizontalAlignment="Center"/>
    </StackPanel>
</Viewbox>
Zegar–□✕
12:45
do dzwonka: 5 min
Zegar–□✕
12:45
do dzwonka: 5 min

ten sam XAML w małym i dużym oknie — wszystko rośnie w tych samych proporcjach

To co innego niż układ z * w Grid: tam kontrolki dostają więcej miejsca, ale czcionka zostaje ta sama. Viewbox powiększa wszystko jak lupa. Dlatego nie nadaje się do formularzy (ogromne pola tekstowe wyglądają źle), ale świetnie — do tablic, zegarów, plansz i ikon.

TEORIA

Stretch w Viewbox

Sposób skalowania wybiera się tak samo jak dla obrazu w lekcji 21:

WartośćDziałanie
Uniform (domyślna)Zachowuje proporcje; całość jest widoczna, po bokach może zostać wolne miejsce.
FillWypełnia całe miejsce, rozciągając nierówno — tekst bywa spłaszczony.
UniformToFillZachowuje proporcje i wypełnia całość; to, co wystaje, jest obcięte.
NoneBez skalowania.
MainWindow.xaml
<!-- tylko pomniejszanie: w dużym oknie zostaje naturalny rozmiar -->
<Viewbox Stretch="Uniform" StretchDirection="DownOnly">
    ...
</Viewbox>

StretchDirection ogranicza skalowanie: DownOnly tylko pomniejsza (treść zmieści się w małym oknie, ale w dużym nie urośnie), UpOnly tylko powiększa, Both (domyślnie) — w obie strony.

PRZYKŁAD

Panel sędziego turnieju klas

Szkolny turniej siatkówki. Z lewej lista meczów z bieżącym wynikiem w setach, z prawej tablica wyników, przyciski punktów i dziennik meczu. Pionowy pasek pozwala poszerzyć listę, poziomy — powiększyć dziennik kosztem tablicy. Tablica jest w Viewbox, więc zawsze wypełnia dostępne miejsce: po zmaksymalizowaniu okna na rzutniku wynik jest widoczny z końca sali gimnastycznej.

Turniej klas — panel sędziego–□✕
Mecze
1TI – 2TI 2:1 ✔
3TI – 3TE 1:0
2TE – 4TI 0:0
1TE – 4TE 0:0
3TI3TE
18:21
sety 1:0
+1 3TI+1 3TE
Punkt dla 3TE — 17:20
Punkt dla 3TI — 18:20
Punkt dla 3TE — 18:21

mecz 3TI–3TE w drugim secie; szare paski to dwa GridSplittery

Mecz.cs — pełny plik
namespace Lekcja37;

// Mecz siatkówki do dwóch wygranych setów; set do 25 punktów z przewagą dwóch
public class Mecz
{
    public string DruzynaA { get; set; }
    public string DruzynaB { get; set; }
    public int PunktyA { get; set; }
    public int PunktyB { get; set; }
    public int SetyA { get; set; }
    public int SetyB { get; set; }
    public string Dziennik { get; set; } = "";      // przebieg meczu, wiersz po wierszu

    public Mecz(string druzynaA, string druzynaB)
    {
        DruzynaA = druzynaA;
        DruzynaB = druzynaB;
    }

    public bool Zakonczony => SetyA == 2 || SetyB == 2;

    // Dodaje punkt i zwraca opis zdarzenia do dziennika meczu
    public string DodajPunkt(bool dlaA)
    {
        if (Zakonczony)
        {
            return "Mecz jest już zakończony.";
        }

        if (dlaA)
        {
            PunktyA++;
        }
        else
        {
            PunktyB++;
        }

        string opis = $"Punkt dla {(dlaA ? DruzynaA : DruzynaB)} — {PunktyA}:{PunktyB}";

        // koniec seta: co najmniej 25 punktów i przewaga dwóch
        if (PunktyA >= 25 && PunktyA - PunktyB >= 2)
        {
            SetyA++;
            opis += $"\n  Set dla {DruzynaA}! Sety {SetyA}:{SetyB}";
            PunktyA = 0;
            PunktyB = 0;
        }
        else if (PunktyB >= 25 && PunktyB - PunktyA >= 2)
        {
            SetyB++;
            opis += $"\n  Set dla {DruzynaB}! Sety {SetyA}:{SetyB}";
            PunktyA = 0;
            PunktyB = 0;
        }

        if (Zakonczony)
        {
            opis += $"\n  KONIEC MECZU — wygrywa {(SetyA == 2 ? DruzynaA : DruzynaB)}";
        }
        return opis;
    }

    public override string ToString()
    {
        string stan = Zakonczony ? "  ✔" : "";
        return $"{DruzynaA} – {DruzynaB}   {SetyA}:{SetyB}{stan}";
    }
}
MainWindow.xaml — pełny plik
<Window x:Class="Lekcja37.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Turniej klas — panel sędziego" Width="900" Height="560"
        MinWidth="600" MinHeight="400" WindowStartupLocation="CenterScreen">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="230" MinWidth="150"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*" MinWidth="300"/>
        </Grid.ColumnDefinitions>

        <!-- lewa część: lista meczów -->
        <DockPanel Grid.Column="0" Margin="10">
            <TextBlock DockPanel.Dock="Top" Text="Mecze" FontWeight="Bold" Margin="0,0,0,6"/>
            <ListBox x:Name="Mecze" SelectionChanged="Mecze_SelectionChanged"/>
        </DockPanel>

        <!-- pionowy pasek do przeciągania między kolumnami -->
        <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Center"
                      VerticalAlignment="Stretch" Background="#FFD0D7DE"/>

        <!-- prawa część: tablica, przyciski i dziennik -->
        <Grid Grid.Column="2" Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" MinHeight="160"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="110" MinHeight="50"/>
            </Grid.RowDefinitions>

            <DockPanel Grid.Row="0">
                <UniformGrid x:Name="Przyciski" DockPanel.Dock="Bottom" Columns="2" Margin="0,8,0,8">
                    <Button x:Name="PunktA" Content="+1 A" Height="34" Margin="0,0,4,0" Click="Punkt_Click"/>
                    <Button x:Name="PunktB" Content="+1 B" Height="34" Margin="4,0,0,0" Click="Punkt_Click"/>
                </UniformGrid>

                <!-- tablica wyników skaluje się do dostępnego miejsca -->
                <Border Background="#FF1E293B" CornerRadius="8" Padding="10">
                    <Viewbox Stretch="Uniform">
                        <Grid Width="400" Height="180">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition/>
                                <ColumnDefinition Width="40"/>
                                <ColumnDefinition/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="40"/>
                                <RowDefinition/>
                                <RowDefinition Height="36"/>
                            </Grid.RowDefinitions>

                            <TextBlock x:Name="NazwaA" Grid.Column="0" Foreground="White"
                                       FontSize="22" HorizontalAlignment="Center"/>
                            <TextBlock x:Name="NazwaB" Grid.Column="2" Foreground="White"
                                       FontSize="22" HorizontalAlignment="Center"/>

                            <TextBlock x:Name="PunktyA" Grid.Row="1" Grid.Column="0" Text="0"
                                       Foreground="#FFFBBF24" FontSize="80" FontWeight="Bold"
                                       HorizontalAlignment="Center" VerticalAlignment="Center"/>
                            <TextBlock Grid.Row="1" Grid.Column="1" Text=":" Foreground="White"
                                       FontSize="60" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                            <TextBlock x:Name="PunktyB" Grid.Row="1" Grid.Column="2" Text="0"
                                       Foreground="#FFFBBF24" FontSize="80" FontWeight="Bold"
                                       HorizontalAlignment="Center" VerticalAlignment="Center"/>

                            <TextBlock x:Name="Sety" Grid.Row="2" Grid.ColumnSpan="3"
                                       Foreground="#FF94A3B8" FontSize="20" HorizontalAlignment="Center"/>
                        </Grid>
                    </Viewbox>
                </Border>
            </DockPanel>

            <!-- poziomy pasek do przeciągania między tablicą a dziennikiem -->
            <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"
                          VerticalAlignment="Center" Background="#FFD0D7DE"/>

            <TextBox x:Name="Dziennik" Grid.Row="2" IsReadOnly="True" TextWrapping="Wrap"
                     VerticalScrollBarVisibility="Auto" FontFamily="Consolas" Margin="0,4,0,0"/>
        </Grid>
    </Grid>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;

namespace Lekcja37;

public partial class MainWindow : Window
{
    private List<Mecz> mecze = new List<Mecz>();

    public MainWindow()
    {
        InitializeComponent();

        mecze.Add(new Mecz("1TI", "2TI"));
        mecze.Add(new Mecz("3TI", "3TE"));
        mecze.Add(new Mecz("2TE", "4TI"));
        mecze.Add(new Mecz("1TE", "4TE"));

        OdswiezListe(mecze[0]);
    }

    private void Mecze_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        Pokaz();
    }

    // Jedna metoda dla obu przycisków — który kliknięto, mówi sender
    private void Punkt_Click(object sender, RoutedEventArgs e)
    {
        Mecz? m = Mecze.SelectedItem as Mecz;
        if (m == null)
        {
            return;
        }

        string opis = m.DodajPunkt(sender == PunktA);
        m.Dziennik += opis + "\n";

        // wynik w ToString się zmienił — lista musi pokazać mecze od nowa
        OdswiezListe(m);
    }

    private void OdswiezListe(Mecz wybrany)
    {
        Mecze.Items.Clear();
        foreach (Mecz m in mecze)
        {
            Mecze.Items.Add(m);
        }
        Mecze.SelectedItem = wybrany;     // to wywoła SelectionChanged, a ono Pokaz()
    }

    private void Pokaz()
    {
        Mecz? m = Mecze.SelectedItem as Mecz;
        if (m == null)
        {
            return;
        }

        NazwaA.Text = m.DruzynaA;
        NazwaB.Text = m.DruzynaB;
        PunktyA.Text = $"{m.PunktyA}";
        PunktyB.Text = $"{m.PunktyB}";
        Sety.Text = m.Zakonczony ? $"KONIEC — sety {m.SetyA}:{m.SetyB}" : $"sety {m.SetyA}:{m.SetyB}";

        PunktA.Content = $"+1 {m.DruzynaA}";
        PunktB.Content = $"+1 {m.DruzynaB}";
        Przyciski.IsEnabled = !m.Zakonczony;

        Dziennik.Text = m.Dziennik;
        Dziennik.ScrollToEnd();
    }
}

Co dzieje się po kolei

  • Zewnętrzny Grid ma trzy kolumny: listę (230 px, co najmniej 150), pasek (Auto) i resztę (*, co najmniej 300).
  • Prawa część to drugi Grid z trzema wierszami: tablica z przyciskami, pasek poziomy i dziennik. Przyciski są w DockPanel razem z tablicą, żeby pasek zmieniał wiersz tablicy, a nie wiersz przycisków.
  • Wnętrze tablicy ma stały rozmiar 400 × 180 i czcionki do 80 punktów. Viewbox skaluje je do miejsca, które zostało w ramce.
  • Reguły siatkówki są w klasie Mecz: DodajPunkt dolicza punkt, wykrywa koniec seta (25 punktów i dwa punkty przewagi) i koniec meczu, a opis zdarzenia zwraca do dziennika.
  • Oba przyciski obsługuje Punkt_Click; porównanie sender == PunktA mówi, która drużyna zdobyła punkt.
  • Po każdym punkcie lista jest budowana od nowa, bo zmienił się tekst z ToString — jak w lekcji 23.

Sprawdź trzy rzeczy

  1. Przeciągnij pionowy pasek maksymalnie w lewo. Lista zatrzyma się na 150 pikselach.
  2. Zmień w Viewbox Stretch na Fill i zwęź okno. Cyfry zrobią się wysokie i chude.
  3. Usuń Width="5" z pionowego GridSplitter. Pasek zniknie i nie będzie czego przeciągać.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
GridSplitterkontrolkaPasek zmieniający rozmiar sąsiednich kolumn lub wierszy Grid.
HorizontalAlignment, VerticalAlignmentwyrównanieKierunek paska: Stretch w pionie — zmienia kolumny, w poziomie — wiersze.
ShowsPreviewboolPodgląd przeciągania; zmiana po puszczeniu myszy.
MinWidth, MaxWidth, MinHeightdoubleNa ColumnDefinition/RowDefinition — granice przeciągania.
ViewboxkontrolkaSkaluje jedno dziecko do dostępnego miejsca.
StretchwyliczenieUniform, Fill, UniformToFill, None.
StretchDirectionwyliczenieBoth, DownOnly, UpOnly.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
GridSplitter bez Width/HeightPasek ma rozmiar 0 — niewidoczny i nie do chwycenia.
pasek pionowy z HorizontalAlignment="Stretch"Pasek wypełnia kolumnę i zmienia wiersze zamiast kolumn. Dla kolumn: Center + VerticalAlignment="Stretch".
pasek obok wiersza Auto z przyciskamiPrzeciąganie rozciąga wiersz przycisków. Połącz go z treścią w jednym panelu.
kolumny bez MinWidthKolumnę można „zgnieść” do zera i użytkownik nie wie, gdzie się podziała.
formularz w ViewboxPola i przyciski rosną razem z czcionką — wygląda to nienaturalnie. Viewbox jest do tablic i grafiki.
wnętrze Viewbox bez stałego rozmiaruTreść „pływa” — skala zmienia się przy każdej zmianie tekstu. Nadaj wnętrzu Width/Height.
ZADANIA

Zadania

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

ZAD 1Zegar na rzutnik★☆☆

Okno z dużym zegarem (godzina, minuty, sekundy), datą z nazwą dnia tygodnia i informacją „do końca lekcji: 12 min” (dzwonki zapisane w tablicy). Całość w Viewbox, aktualizowana przez DispatcherTimer. Dwukrotne kliknięcie w zegar przełącza ciemny i jasny motyw.

pokaż wskazówkę
  • Dwukrotne kliknięcie: w MouseLeftButtonDown sprawdź e.ClickCount == 2.
ZAD 2Słownik pojęć INF.04★☆☆

Lista pojęć (np. „klasa”, „obiekt”, „hermetyzacja”, „zdarzenie”) z lewej, pionowy GridSplitter, z prawej nazwa pojęcia dużą czcionką i wyjaśnienie. Pod wyjaśnieniem poziomy GridSplitter i pole „moje notatki” zapamiętywane osobno dla każdego pojęcia. Kolumny mają rozsądne minimalne szerokości.

pokaż wskazówkę
  • Klasa Pojecie z polami nazwa, opis, notatka. Notatkę zapisuj w TextChanged pola notatek do zaznaczonego obiektu.
ZAD 3Plansza kółko i krzyżyk, która rośnie★★☆

Gra w kółko i krzyżyk dla dwóch graczy: plansza 3 × 3 z przycisków w UniformGrid o stałym rozmiarze 300 × 300, umieszczona w Viewbox. Z prawej (za GridSplitterem) panel z wynikiem meczu do trzech wygranych, historią partii i przyciskiem „nowa partia”. Wygrywająca linia podświetla się na zielono.

pokaż wskazówkę
  • Osiem linii wygrywających zapisz w tablicy int[8, 3] numerów pól 0–8 i sprawdzaj w pętli.
ZAD 4Porównywarka dwóch tekstów★★☆

Dwa wielowierszowe pola obok siebie z pionowym paskiem między nimi i przyciskiem „Porównaj”. Pod nimi poziomy pasek i wynik: liczba wierszy w obu tekstach, numery wierszy, które się różnią, i pierwszy różniący się znak w każdym z nich. Przycisk „Wyrównaj” przywraca obie kolumny do równej szerokości.

pokaż wskazówkę
  • Szerokość kolumny z kodu: Kolumna.Width = new GridLength(1, GridUnitType.Star) dla obu kolumn — jak w lekcji 26.
ZAD 5Tablica odjazdów spod szkoły★★☆

Ekran informacyjny na korytarz: tablica 6 najbliższych odjazdów autobusów (linia, kierunek, „za 4 min”) z rozkładu zapisanego w kodzie, odświeżana co 10 sekund. Tablica w Viewbox (czarne tło, żółty tekst), odjazd za mniej niż 3 minuty miga. Na dole, za poziomym paskiem, panel administratora do dodawania kursów, który można „zwinąć” paskiem prawie do zera.

pokaż wskazówkę
  • Godziny odjazdów jako TimeSpan; porównuj z DateTime.Now.TimeOfDay.
ZAD 6Menedżer plików na niby★★★

Okno w stylu Eksploratora: drzewo folderów udawane listą z wcięciami („📁 Dokumenty”, „ 📁 Szkoła”…), pionowy pasek, lista plików wybranego folderu (nazwa, rozmiar, typ — dane w klasach Folder i Plik), drugi pionowy pasek i panel podglądu z ikoną typu pliku w Viewbox. W menu „Widok” pozycja zaznaczana „Panel podglądu” ukrywa trzecią kolumnę razem z paskiem i przywraca je z poprzednią szerokością.

pokaż wskazówkę
  • Ukrycie kolumny: zapamiętaj jej Width, ustaw new GridLength(0) i schowaj pasek przez Visibility. Pamiętaj, że MinWidth nie pozwoli zejść do zera — wyzeruj je też.
PODSUMOWANIE

Co trzeba zapamiętać

  • GridSplitter w osobnej kolumnie/wierszu Auto zmienia rozmiar bezpośrednich sąsiadów.
  • Pasek musi mieć szerokość (lub wysokość) i odpowiednie wyrównanie: Stretch wzdłuż paska.
  • MinWidth/MinHeight na definicjach kolumn i wierszy ograniczają przeciąganie.
  • Viewbox skaluje całe dziecko — czcionki też; wnętrze projektuj w stałym rozmiarze.
  • Stretch="Uniform" zachowuje proporcje; StretchDirection ogranicza kierunek skalowania.

Dokumentacja: Microsoft Learn — GridSplitter, Viewbox.

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