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

WrapPanel i UniformGrid

Dwa panele do układania wielu podobnych elementów. WrapPanel ustawia je jeden za drugim i przenosi do nowej linii, gdy zabraknie miejsca — jak słowa w akapicie. UniformGrid dzieli obszar na równe komórki i wypełnia je po kolei, bez numerowania wierszy i kolumn. Dzięki nim plansza, klawiatura czy sala kinowa to kilka linijek pętli.

XAML C# WrapPanel UniformGrid Rows / Columns 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Ułożysz elementy w WrapPanel, który przenosi je do nowej linii, i ustawisz im wspólny rozmiar.
  • Zbudujesz siatkę równych komórek w UniformGrid z Rows i Columns.
  • Przesuniesz pierwszy element UniformGrid o kilka komórek przez FirstColumn.
  • Wypełnisz oba panele w pętli i odczytasz stan elementów, przechodząc po Children.
  • Dobierzesz panel do zadania: StackPanel, WrapPanel, UniformGrid czy Grid.
TEORIA

WrapPanel — z zawijaniem

MainWindow.xaml
<WrapPanel Margin="10">
    <Button Content="matematyka" Margin="0,0,6,6" Padding="8,2"/>
    <Button Content="polski" Margin="0,0,6,6" Padding="8,2"/>
    <Button Content="programowanie" Margin="0,0,6,6" Padding="8,2"/>
    <Button Content="sieci" Margin="0,0,6,6" Padding="8,2"/>
    <Button Content="WF" Margin="0,0,6,6" Padding="8,2"/>
    <Button Content="angielski" Margin="0,0,6,6" Padding="8,2"/>
</WrapPanel>
Przedmioty–□✕
matematyka
polski
programowanie
sieci
WF
angielski

zwęź okno — przyciski przejdą do kolejnych linii

Każdy element ma swój naturalny rozmiar; panel ustawia je w linii od lewej do prawej, a gdy następny się nie mieści — zaczyna nową linię. Dodatkowe ustawienia:

WłaściwośćCo robi
Orientation="Vertical"Układa elementy w kolumnie z góry na dół, a gdy zabraknie wysokości — zaczyna nową kolumnę. Domyślnie Horizontal.
ItemWidth, ItemHeightKażdy element dostaje taki sam „slot”, np. ItemWidth="80". Elementy tworzą wtedy równe kolumny, choć nadal się zawijają.
TEORIA

UniformGrid — równe komórki

UniformGrid to uproszczony Grid: podajesz tylko liczbę wierszy i kolumn, a wszystkie komórki są równe. Elementy nie mają Grid.Row ani Grid.Column — trafiają do komórek po kolei, wierszami od lewej do prawej:

MainWindow.xaml
<UniformGrid Rows="2" Columns="3" Margin="10">
    <Button Content="1" Margin="3"/>
    <Button Content="2" Margin="3"/>
    <Button Content="3" Margin="3"/>
    <Button Content="4" Margin="3"/>
    <Button Content="5" Margin="3"/>
</UniformGrid>
1
2
3
4
5
pusta

5 przycisków w siatce 2 × 3 — ostatnia komórka zostaje pusta

UstawienieEfekt
tylko Columns="3"Wierszy jest tyle, ile potrzeba: 7 elementów → 3 wiersze.
tylko Rows="2"Kolumn jest tyle, ile potrzeba.
bez obuPanel sam tworzy siatkę „prawie kwadratową”: 9 elementów → 3 × 3.
FirstColumn="2"Pierwszy element trafia do kolumny 2 pierwszego wiersza, dwie pierwsze komórki zostają puste. Przydaje się w kalendarzu, gdy miesiąc nie zaczyna się w poniedziałek.

Komórki dzielą całe dostępne miejsce po równo i rosną razem z oknem. Jeśli elementów jest więcej niż komórek, nadmiarowe nie zostaną pokazane.

TEORIA

Wypełnianie z kodu

Oba panele świetnie współpracują z pętlą — dodajesz elementy przez Children.Add, a panel sam je rozmieszcza. Stan elementów sprawdzasz, przechodząc po Children w foreach:

MainWindow.xaml.cs
// w XAML: <UniformGrid x:Name="Plansza" Rows="4" Columns="4"/>
for (int i = 1; i <= 16; i++)
{
    Button pole = new Button { Content = i, Margin = new Thickness(2), Tag = "wolne" };
    pole.Click += Pole_Click;
    Plansza.Children.Add(pole);
}

// ile pól jest zaznaczonych?
int zaznaczone = 0;
foreach (Button pole in Plansza.Children)
{
    if ((string)pole.Tag == "zaznaczone")
    {
        zaznaczone++;
    }
}

Zapis foreach (Button pole in Plansza.Children) zakłada, że w panelu są same przyciski — każdy element jest rzutowany na Button. Gdyby był tam też napis, program zakończy się wyjątkiem InvalidCastException.

Do zapamiętania stanu elementu dobrze nadaje się Tag z krótkim napisem („wolne”, „zaznaczone”). Odczyt wymaga rzutowania: (string)pole.Tag.

TEORIA

Który panel wybrać

PanelUkładaTypowe użycie
StackPanelw jednej linii, bez zawijaniapionowa lista, rząd przycisków
WrapPanelw linii z zawijaniem, elementy różnej wielkościtagi, żetony, galeria miniatur, klawiatura ekranowa
UniformGridw równych komórkach, po koleiplansza gry, klawiatura numeryczna, kalendarz, sala
Gridw komórkach o różnych rozmiarach, z łączeniemukład całego okna, formularz
PRZYKŁAD

Wybór miejsc w kinie

Sala ma 5 rzędów po 8 miejsc. Przyciski miejsc tworzy pętla w konstruktorze i wkłada do UniformGrid. Kliknięcie wolnego miejsca je zaznacza (najwyżej 6), ponowne — odznacza. Wybrane miejsca pojawiają się pod salą jako zielone żetony w WrapPanel, a na dole widać cenę. Po rezerwacji miejsca stają się nieaktywne.

Kino „Sokół” — wybór miejsc–□✕
EKRAN
A1
A2
A3
A4
A5
A6
A7
A8
B1
B2
B3
B4
B5
B6
B7
B8
C1
C2
C3
C4
C5
C6
C7
C8
D1
D2
D3
D4
D5
D6
D7
D8
E1
E2
E3
E4
E5
E6
E7
E8
Wybrane miejsca:
C4C5C6
Miejsc: 3 × 25,00 zł = 75,00 zł
Rezerwuj

trzy miejsca wybrane, pięć wcześniej sprzedanych (szare)

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja12.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Kino „Sokół” — wybór miejsc" Height="480" Width="520"
        MinWidth="420" MinHeight="420"
        WindowStartupLocation="CenterScreen">

    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>   <!-- ekran -->
            <RowDefinition Height="*"/>      <!-- sala -->
            <RowDefinition Height="Auto"/>   <!-- wybrane miejsca -->
            <RowDefinition Height="Auto"/>   <!-- podsumowanie -->
        </Grid.RowDefinitions>

        <Border Grid.Row="0" Background="LightGray" CornerRadius="0,0,20,20"
                Padding="4" Margin="40,0,40,10">
            <TextBlock Text="EKRAN" HorizontalAlignment="Center" Foreground="DimGray"/>
        </Border>

        <!-- Sala: 5 rzędów po 8 miejsc, przyciski dodaje konstruktor -->
        <UniformGrid x:Name="Sala" Grid.Row="1" Rows="5" Columns="8"/>

        <!-- Wybrane miejsca jako „żetony”, zawijane do nowej linii -->
        <StackPanel Grid.Row="2" Margin="0,10,0,0">
            <TextBlock Text="Wybrane miejsca:" FontWeight="Bold"/>
            <WrapPanel x:Name="Wybrane" Margin="0,4,0,0"/>
        </StackPanel>

        <Grid Grid.Row="3" Margin="0,10,0,0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <TextBlock x:Name="Podsumowanie" VerticalAlignment="Center" FontSize="14"/>
            <Button x:Name="Rezerwuj" Grid.Column="1" Content="Rezerwuj"
                    Width="100" Height="30" Click="Rezerwuj_Click"/>
        </Grid>
    </Grid>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Lekcja12;

public partial class MainWindow : Window
{
    private const int Rzedy = 5;
    private const int MiejscWRzedzie = 8;
    private const int Limit = 6;
    private const decimal Cena = 25.00m;

    // miejsca sprzedane wcześniej
    private readonly string[] sprzedane = { "A4", "A5", "C1", "C2", "E7" };

    public MainWindow()
    {
        InitializeComponent();

        for (int r = 0; r < Rzedy; r++)
        {
            char litera = (char)('A' + r);           // 0 → 'A', 1 → 'B', …

            for (int m = 1; m <= MiejscWRzedzie; m++)
            {
                string nazwa = $"{litera}{m}";
                Button miejsce = new Button
                {
                    Content = nazwa,
                    Margin = new Thickness(3),
                    Tag = "wolne",
                    Background = Brushes.WhiteSmoke
                };
                miejsce.Click += Miejsce_Click;

                if (sprzedane.Contains(nazwa))
                {
                    OznaczJakoZajete(miejsce);
                }

                Sala.Children.Add(miejsce);           // UniformGrid sam wybiera komórkę
            }
        }

        Odswiez();
    }

    private void Miejsce_Click(object sender, RoutedEventArgs e)
    {
        Button miejsce = (Button)sender;

        if ((string)miejsce.Tag == "wybrane")
        {
            miejsce.Tag = "wolne";
            miejsce.Background = Brushes.WhiteSmoke;
        }
        else if (LiczbaWybranych() < Limit)
        {
            miejsce.Tag = "wybrane";
            miejsce.Background = Brushes.LightGreen;
        }
        else
        {
            MessageBox.Show($"Jedna rezerwacja to najwyżej {Limit} miejsc.", "Limit miejsc",
                            MessageBoxButton.OK, MessageBoxImage.Information);
        }

        Odswiez();
    }

    private void Rezerwuj_Click(object sender, RoutedEventArgs e)
    {
        int ile = LiczbaWybranych();
        MessageBoxResult odp = MessageBox.Show($"Zarezerwować {ile} miejsc za {ile * Cena:F2} zł?",
                                               "Rezerwacja", MessageBoxButton.YesNo,
                                               MessageBoxImage.Question);
        if (odp != MessageBoxResult.Yes)
        {
            return;
        }

        foreach (Button miejsce in Sala.Children)
        {
            if ((string)miejsce.Tag == "wybrane")
            {
                OznaczJakoZajete(miejsce);
            }
        }

        Odswiez();
    }

    // Przebudowuje żetony i podsumowanie na podstawie stanu przycisków
    private void Odswiez()
    {
        Wybrane.Children.Clear();

        foreach (Button miejsce in Sala.Children)
        {
            if ((string)miejsce.Tag == "wybrane")
            {
                Border zeton = new Border
                {
                    Background = Brushes.LightGreen,
                    CornerRadius = new CornerRadius(10),
                    Padding = new Thickness(10, 2, 10, 2),
                    Margin = new Thickness(0, 0, 6, 6),
                    Child = new TextBlock { Text = (string)miejsce.Content }
                };
                Wybrane.Children.Add(zeton);
            }
        }

        int ile = Wybrane.Children.Count;
        Podsumowanie.Text = ile == 0
            ? "Kliknij wolne miejsce, aby je wybrać."
            : $"Miejsc: {ile} × {Cena:F2} zł = {ile * Cena:F2} zł";
        Rezerwuj.IsEnabled = ile > 0;
    }

    private int LiczbaWybranych()
    {
        int ile = 0;
        foreach (Button miejsce in Sala.Children)
        {
            if ((string)miejsce.Tag == "wybrane")
            {
                ile++;
            }
        }
        return ile;
    }

    private void OznaczJakoZajete(Button miejsce)
    {
        miejsce.Tag = "zajete";
        miejsce.IsEnabled = false;      // nieaktywny przycisk WPF rysuje na szaro
    }
}

Co dzieje się po kolei

  • W XAML UniformGrid sali jest pusty — ma tylko Rows="5" i Columns="8". Stoi w wierszu *, więc przyciski rosną razem z oknem.
  • Konstruktor tworzy 40 przycisków w dwóch pętlach. (char)('A' + r) zamienia numer rzędu na literę: znaki to liczby, więc 'A’ + 1 to 'B’.
  • Przyciski są dodawane po kolei, a UniformGrid sam wypełnia nimi wiersze — nie ma żadnego Grid.SetRow.
  • sprzedane.Contains(nazwa) sprawdza, czy tablica zawiera nazwę miejsca. Sprzedane miejsca od razu stają się nieaktywne — WPF rysuje je na szaro i nie wywołuje dla nich Click.
  • Stan miejsca jest w Tag: „wolne”, „wybrane” albo „zajete”. Kolor tła tylko ten stan pokazuje.
  • Miejsce_Click odznacza wybrane miejsce, a wolne zaznacza tylko wtedy, gdy nie przekroczono limitu.
  • Odswiez buduje żetony od nowa: czyści WrapPanel i dodaje zaokrąglony Border dla każdego wybranego miejsca. Liczba żetonów to liczba wybranych miejsc.
  • Przycisk Rezerwuj jest aktywny tylko wtedy, gdy coś wybrano. Po potwierdzeniu wszystkie wybrane miejsca dostają stan „zajete”.

Sprawdź trzy rzeczy

  1. Wybierz 6 miejsc i zwęź okno. Żetony w WrapPanel przejdą do drugiej linii.
  2. Usuń z XAML Rows="5". Nic się nie zmieni — przy 8 kolumnach i 40 przyciskach panel sam policzy 5 wierszy.
  3. Zmień Columns="8" na Columns="10". Rzędy „rozjadą się”: A9 i A10 to w rzeczywistości B1 i B2, bo panel wypełnia komórki po kolei.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
WrapPanel.OrientationHorizontal, VerticalKierunek układania; zawija do nowej linii / kolumny.
ItemWidth, ItemHeightdoubleJednakowy slot dla każdego elementu WrapPanel.
UniformGrid.Rows, ColumnsintLiczba wierszy / kolumn; jedną z nich panel może policzyć sam.
FirstColumnintIle komórek pierwszego wiersza zostaje pustych.
foreach (Button b in Panel.Children)pętlaPrzejście po wszystkich elementach panelu z rzutowaniem na Button.
tablica.Contains(x)boolCzy tablica zawiera wartość x.
(char)('A' + n)charLitera przesunięta o n od 'A’.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Grid.Row="1" w elemencie UniformGridIgnorowane — kolejność wyznacza tylko pozycja w pliku / w Children.
więcej elementów niż Rows × ColumnsNadmiarowe elementy nie są widoczne. Zwiększ liczbę wierszy albo usuń Rows.
WrapPanel w poziomym StackPanelNie zawija — dostaje nieskończenie dużo szerokości. Postaw go w Grid albo w pionowym StackPanel.
foreach (Button b in Panel.Children), gdy w panelu jest też TextBlockInvalidCastException przy pierwszym napisie.
if (pole.Tag == "wolne")Ostrzeżenie CS0252 i porównanie referencji. Rzutuj: (string)pole.Tag == "wolne".
Background nieaktywnego przyciskuNie jest widoczny — WPF rysuje nieaktywny przycisk na szaro.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Elementy, których jest dużo, twórz w pętli. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Klawiatura ekranowa★☆☆

Kiosk w bibliotece nie ma fizycznej klawiatury. U góry pole tylko do odczytu, pod nim WrapPanel z przyciskami wszystkich liter polskiego alfabetu (32 litery, przyciski 40 × 40) utworzonymi w pętli z napisu "AĄBCĆDEĘFGHIJKLŁMNŃOÓPRSŚTUWYZŹŻ". Pod literami przyciski Spacja, ⌫ i Szukaj. Kliknięcie litery dopisuje ją do pola, Szukaj pokazuje „Szukam: …”.

pokaż wskazówkę
  • foreach (char litera in napis) przejdzie po wszystkich znakach.
  • Równe przyciski dostaniesz przez ItemWidth/ItemHeight panelu albo Width/Height przycisku.
ZAD 2Paleta kolorów★☆☆

UniformGrid 4 × 4 z szesnastoma kolorowymi przyciskami bez napisów (wybierz kolory z klasy Brushes). Kliknięcie koloru zmienia tło dużego prostokąta podglądu (Border z zaokrąglonymi rogami) obok palety, a pod podglądem pokazuje kod koloru, np. „#FF4682B4”. Wybrany przycisk palety ma grubszą, czarną ramkę.

pokaż wskazówkę
  • Pędzle zapisz w tablicy Brush[] i twórz przyciski w pętli; kliknięty kolor odczytasz z Background przycisku.
  • Kod koloru: Background.ToString(). Ramkę przycisku ustawisz przez BorderBrush i BorderThickness — przed zaznaczeniem nowego przywróć zwykłą ramkę wszystkim.
ZAD 3Kalendarz miesiąca★★☆

Kalendarz bieżącego miesiąca: u góry nazwa miesiąca i rok („październik 2026”) oraz przyciski ◀ i ▶ do zmiany miesiąca. Pod spodem wiersz z nazwami dni (Pn–Nd) i UniformGrid z 7 kolumnami, w którym są numery dni. Pierwszy dzień miesiąca stoi pod właściwym dniem tygodnia, soboty i niedziele są czerwone, a dzisiejszy dzień ma żółte tło.

Kalendarz–□✕
◀
październik 2026
▶
PnWtŚrCzPtSoNd 1234 567891011 12131415161718 …
pokaż wskazówkę
  • Pierwszy dzień: new DateTime(rok, miesiac, 1), liczba dni: DateTime.DaysInMonth(rok, miesiac).
  • DayOfWeek liczy od niedzieli (0). Przesunięcie dla tygodnia od poniedziałku: ((int)dzien.DayOfWeek + 6) % 7 — to wartość dla FirstColumn.
  • Nazwę miesiąca da ToString("MMMM yyyy"). Przy zmianie miesiąca wyczyść panel i zbuduj go od nowa; AddMonths(1) sam przejdzie przez koniec roku.
ZAD 4Tagi do notatki★★☆

Pole „Nowy tag” i przycisk Dodaj (Enter). Każdy tag pojawia się jako żeton „#wpf ✕” w WrapPanel. Kliknięcie żetonu usuwa go. Tag zapisuje się małymi literami, bez spacji (spacje zamieniają się na myślniki), a znak # dodaje program. Nie można dodać tagu pustego ani takiego, który już jest. Najwyżej 10 tagów — potem pole i przycisk się blokują, a po usunięciu tagu odblokowują.

pokaż wskazówkę
  • Najprościej, gdy żeton jest zwykłym przyciskiem z napisem „#wpf ✕” i Padding. Kliknięty przycisk usuniesz przez Children.Remove((Button)sender). Zaokrąglony przycisk zrobisz w lekcji o szablonach.
  • Replace(' ', '-') i ToLower() przygotują tekst tagu.
ZAD 5Plan sali egzaminacyjnej★★☆

Egzaminator wpisuje liczbę stanowisk w rzędzie (2–8) i liczbę rzędów (2–8), a program rysuje plan sali w UniformGrid. Na egzaminie zdający nie mogą siedzieć obok siebie, więc w każdym rzędzie co drugie stanowisko jest wolne, a w kolejnych rzędach wzór jest przesunięty („szachownica”). Zajęte stanowiska to jasnoniebieskie ramki z numerem zdającego (1, 2, 3…), wolne — szare ramki z napisem „—”. Pod planem: „Maksymalnie zdających: 18”.

pokaż wskazówkę
  • Ustaw Rows i Columns panelu z kodu, potem wyczyść go i wypełnij w dwóch pętlach.
  • Szachownica: stanowisko (w, k) jest zajęte, gdy (w + k) % 2 == 0.
ZAD 6Gra „Zgaś światła”★★★

Plansza 5 × 5 przycisków-lampek: zapalona jest żółta, zgaszona — ciemnoszara. Kliknięcie lampki przełącza ją i jej sąsiadów z góry, z dołu, z lewej i z prawej (lampki na brzegu mają mniej sąsiadów). Celem jest zgaszenie wszystkich. Na start zapal lampki według wzoru wpisanego w kodzie, np. środkowy krzyż. Pod planszą: „Ruchy: 7”. Po zgaszeniu wszystkich pokaż „Wygrana w 7 ruchach!” i zablokuj planszę. Przycisk Od nowa przywraca wzór startowy.

Zgaś światła–□✕
Ruchy: 0
Od nowa
pokaż wskazówkę
  • W Tag każdej lampki zapisz jej numer 0–24. Wiersz to numer / 5, kolumna — numer % 5.
  • Stan lampek trzymaj w tablicy bool[25], a kolory ustawiaj w osobnej metodzie na podstawie tablicy. Lampkę o numerze n znajdziesz jako (Button)Plansza.Children[n].
  • Metoda „przełącz (wiersz, kolumna)” niech nic nie robi, gdy współrzędne wychodzą poza planszę — wtedy brzegi obsłużą się same.
PODSUMOWANIE

Co trzeba zapamiętać

  • WrapPanel układa elementy w linii i zawija je, gdy zabraknie miejsca. ItemWidth/ItemHeight dają równe sloty.
  • UniformGrid dzieli obszar na równe komórki i wypełnia je po kolei — bez Grid.Row i Grid.Column.
  • Wystarczy podać Rows albo Columns; FirstColumn zostawia puste komórki na początku.
  • Oba panele najlepiej wypełniać w pętli, a stan elementów odczytywać w foreach po Children.
  • Stan elementu (wolne, wybrane…) zapisuj w Tag, a kolor niech go tylko pokazuje.

Dokumentacja: Microsoft Learn — WrapPanel, UniformGrid.

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