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.
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.
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:
<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>
Fizyka
Historia
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"(lubLeft/Right) iVerticalAlignment="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.
GridSplitter między wierszami
Pasek poziomy działa tak samo, tylko role się zamieniają: wiersz Auto, Height zamiast Width, rozciągnięcie w poziomie:
<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.
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:
<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>
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.
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. |
Fill | Wypełnia całe miejsce, rozciągając nierówno — tekst bywa spłaszczony. |
UniformToFill | Zachowuje proporcje i wypełnia całość; to, co wystaje, jest obcięte. |
None | Bez skalowania. |
<!-- 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.
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.
mecz 3TI–3TE w drugim secie; szare paski to dwa GridSplittery
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}";
}
}
<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>
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
Gridma trzy kolumny: listę (230 px, co najmniej 150), pasek (Auto) i resztę (*, co najmniej 300). - Prawa część to drugi
Gridz trzema wierszami: tablica z przyciskami, pasek poziomy i dziennik. Przyciski są wDockPanelrazem 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.
Viewboxskaluje je do miejsca, które zostało w ramce. - Reguły siatkówki są w klasie
Mecz:DodajPunktdolicza 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ównaniesender == PunktAmó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
- Przeciągnij pionowy pasek maksymalnie w lewo. Lista zatrzyma się na 150 pikselach.
- Zmień w
ViewboxStretchnaFilli zwęź okno. Cyfry zrobią się wysokie i chude. - Usuń
Width="5"z pionowegoGridSplitter. Pasek zniknie i nie będzie czego przeciągać.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
GridSplitter | kontrolka | Pasek zmieniający rozmiar sąsiednich kolumn lub wierszy Grid. |
HorizontalAlignment, VerticalAlignment | wyrównanie | Kierunek paska: Stretch w pionie — zmienia kolumny, w poziomie — wiersze. |
ShowsPreview | bool | Podgląd przeciągania; zmiana po puszczeniu myszy. |
MinWidth, MaxWidth, MinHeight | double | Na ColumnDefinition/RowDefinition — granice przeciągania. |
Viewbox | kontrolka | Skaluje jedno dziecko do dostępnego miejsca. |
Stretch | wyliczenie | Uniform, Fill, UniformToFill, None. |
StretchDirection | wyliczenie | Both, DownOnly, UpOnly. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
GridSplitter bez Width/Height | Pasek 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 przyciskami | Przeciąganie rozciąga wiersz przycisków. Połącz go z treścią w jednym panelu. |
kolumny bez MinWidth | Kolumnę można „zgnieść” do zera i użytkownik nie wie, gdzie się podziała. |
formularz w Viewbox | Pola i przyciski rosną razem z czcionką — wygląda to nienaturalnie. Viewbox jest do tablic i grafiki. |
wnętrze Viewbox bez stałego rozmiaru | Treść „pływa” — skala zmienia się przy każdej zmianie tekstu. Nadaj wnętrzu Width/Height. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
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
MouseLeftButtonDownsprawdźe.ClickCount == 2.
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
Pojeciez polami nazwa, opis, notatka. Notatkę zapisuj wTextChangedpola notatek do zaznaczonego obiektu.
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.
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.
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 zDateTime.Now.TimeOfDay.
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, ustawnew GridLength(0)i schowaj pasek przezVisibility. Pamiętaj, żeMinWidthnie pozwoli zejść do zera — wyzeruj je też.
Co trzeba zapamiętać
GridSplitterw osobnej kolumnie/wierszuAutozmienia rozmiar bezpośrednich sąsiadów.- Pasek musi mieć szerokość (lub wysokość) i odpowiednie wyrównanie:
Stretchwzdłuż paska. MinWidth/MinHeightna definicjach kolumn i wierszy ograniczają przeciąganie.Viewboxskaluje całe dziecko — czcionki też; wnętrze projektuj w stałym rozmiarze.Stretch="Uniform"zachowuje proporcje;StretchDirectionogranicza kierunek skalowania.
Dokumentacja: Microsoft Learn — GridSplitter, Viewbox.