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

Border i ScrollViewer

Dwie kontrolki, które same nic nie wyświetlają, a zmieniają wygląd i wygodę okna. Border rysuje ramkę, tło i zaokrąglone rogi wokół innej kontrolki — z niej zrobisz karty, kafelki i paski. ScrollViewer dodaje przewijanie, gdy zawartość nie mieści się w oknie. Razem pozwalają zbudować listę, która rośnie w trakcie działania programu.

XAML C# Border CornerRadius ScrollViewer ScrollToEnd 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Otoczysz kontrolkę ramką z tłem, grubością linii i zaokrąglonymi rogami.
  • Wyjaśnisz, dlaczego Border i ScrollViewer mają tylko jedno dziecko i jak to obejść.
  • Zbudujesz kartę (ramka z kilkoma napisami) w XAML i w kodzie C#.
  • Dodasz przewijanie do listy i wyjaśnisz, dlaczego ScrollViewer w StackPanel nie przewija.
  • Przewiniesz listę z kodu do końca i sprawdzisz, czy użytkownik dotarł na dół.
  • Usuniesz wybraną kontrolkę z panelu.
TEORIA

Border — ramka wokół kontrolki

MainWindow.xaml
<Border Background="LightYellow"
        BorderBrush="Goldenrod" BorderThickness="2"
        CornerRadius="8" Padding="12" Margin="20">
    <TextBlock Text="Jutro wycieczka do Gdańska!" FontSize="16"/>
</Border>
Ramka–□✕
Jutro wycieczka do Gdańska!

ramka z tłem, grubością 2 i rogami zaokrąglonymi o 8

WłaściwośćPrzykładCo robi
BackgroundLightYellowTło wewnątrz ramki (także pod zaokrąglonymi rogami).
BorderBrushGoldenrodKolor linii ramki.
BorderThickness2 albo 0,0,0,1Grubość linii. Cztery liczby jak w Margin: lewa, góra, prawa, dół — 0,0,0,1 to sama kreska na dole.
CornerRadius8 albo 8,8,0,0Zaokrąglenie rogów. Cztery liczby: lewy górny, prawy górny, prawy dolny, lewy dolny.
Padding12Odstęp między linią ramki a zawartością.
Child—Zawartość ramki. W XAML to po prostu znacznik w środku.

Jedno dziecko

Border może mieć w środku tylko jedną kontrolkę. Dwie dadzą błąd The property 'Child’ is set more than once. Gdy w ramce ma być kilka napisów, wkładasz do niej panel — StackPanel albo Grid — a napisy do panelu.

MainWindow.xaml
<!-- karta: ramka → panel → kilka kontrolek -->
<Border BorderBrush="LightGray" BorderThickness="1" CornerRadius="6" Padding="10" Margin="10">
    <StackPanel>
        <TextBlock Text="Pendrive 64 GB" FontWeight="Bold"/>
        <TextBlock Text="39,99 zł" Foreground="Green"/>
        <Button Content="Do koszyka" Margin="0,8,0,0"/>
    </StackPanel>
</Border>

Gdy ramka stoi w StackPanel lub w komórce Grid, rozciąga się na całą szerokość. Żeby była tylko tak szeroka jak zawartość, daj jej HorizontalAlignment="Left" (albo Center, Right).

TEORIA

Border z kodu

W lekcji 03 dodawałeś do panelu napisy tworzone w kodzie. Kartę buduje się tak samo, od środka na zewnątrz: najpierw napisy, potem panel z napisami, na końcu ramka z panelem w Child.

MainWindow.xaml.cs
Border kafelek = new Border
{
    Background = Brushes.LightBlue,
    BorderBrush = Brushes.SteelBlue,
    BorderThickness = new Thickness(1),
    CornerRadius = new CornerRadius(6),     // osobny typ, nie liczba
    Padding = new Thickness(8),
    Margin = new Thickness(0, 0, 0, 6),
    Child = new TextBlock { Text = "Nowy kafelek" }
};
Lista.Children.Add(kafelek);

W XAML piszesz CornerRadius="6" i BorderThickness="1", a WPF sam zamienia tekst na właściwy typ. W kodzie musisz utworzyć obiekt: new CornerRadius(6), new Thickness(1).

TEORIA

ScrollViewer — przewijanie

ScrollViewer pokazuje zawartość w „okienku” i dodaje paski przewijania, gdy zawartość jest większa. Tak jak Border ma jedno dziecko — zwykle panel z listą:

MainWindow.xaml
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <TextBlock Text="Lista obecności" FontSize="18" Margin="10"/>

    <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
        <StackPanel x:Name="Lista" Margin="10"/>
    </ScrollViewer>
</Grid>
WartośćPasek przewijania
Autopojawia się tylko wtedy, gdy zawartość się nie mieści — zwykle najlepszy wybór
Visiblewidoczny zawsze; domyślna wartość dla pionowego paska
Hiddenniewidoczny, ale kółkiem myszy dalej da się przewijać
Disabledbrak przewijania w tym kierunku; domyślna wartość dla poziomego paska — dzięki temu tekst może się zawijać

ScrollViewer musi mieć ograniczoną wysokość

Pionowy StackPanel daje dzieciom tyle wysokości, ile chcą. ScrollViewer włożony do niego „chce” zmieścić całą listę, dostaje to miejsce i… nigdy nie przewija — lista wychodzi poza okno. Dlatego ScrollViewer stawiasz w wierszu Grid z gwiazdką albo dajesz mu stałe Height.

Pole TextBox nie potrzebuje osobnego ScrollViewer — ma przewijanie wbudowane (VerticalScrollBarVisibility z lekcji 07).

TEORIA

Przewijanie z kodu

MainWindow.xaml.cs
Przewijanie.ScrollToEnd();     // na sam dół — np. po dodaniu nowej wiadomości
Przewijanie.ScrollToHome();    // na samą górę

// zdarzenie ScrollChanged: przewinięto albo zmienił się rozmiar zawartości
private void Przewijanie_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
    bool naDole = Przewijanie.VerticalOffset >= Przewijanie.ScrollableHeight;
    Akceptuj.IsEnabled = naDole;
}

VerticalOffset mówi, o ile przewinięto od góry, a ScrollableHeight — o ile w ogóle da się przewinąć. Gdy są równe, użytkownik jest na dole. Gdy cała zawartość się mieści, obie wartości wynoszą 0.

TEORIA

Usuwanie z panelu

Znasz już Children.Add, Insert i Clear. Pojedynczą kontrolkę usuniesz na dwa sposoby:

MainWindow.xaml.cs
Lista.Children.Remove(kartka);   // usuwa wskazaną kontrolkę
Lista.Children.RemoveAt(0);      // usuwa kontrolkę o podanym numerze (0 = pierwsza)

Częsty wzór: przycisk „Usuń” wewnątrz karty zna swoją kartę, bo przy tworzeniu zapisujesz ją w jego Tag. W metodzie Click odczytujesz Tag, rzutujesz na Border i usuwasz z listy.

PRZYKŁAD

Tablica ogłoszeń klasy

Program wyświetla ogłoszenia klasy jako żółte karteczki z godziną dodania. Nowe ogłoszenie trafia na koniec listy, a lista przewija się sama, żeby było je widać. Nagłówek pokazuje liczbę ogłoszeń, a Wyczyść po potwierdzeniu zdejmuje wszystkie karteczki.

Tablica ogłoszeń–□✕
Tablica ogłoszeń 3TI
Ogłoszeń: 3
08:02
Witamy na tablicy klasy 3TI!
08:02
W piątek sprawdzian z programowania aplikacji desktopowych.
08:15
Kto ma ładowarkę USB-C? Sala 204.
 
Dodaj
Wyczyść

po dodaniu trzeciego ogłoszenia

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja11.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Tablica ogłoszeń" Height="460" Width="420"
        MinWidth="320" MinHeight="300"
        WindowStartupLocation="CenterScreen">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>   <!-- nagłówek -->
            <RowDefinition Height="*"/>      <!-- przewijana lista -->
            <RowDefinition Height="Auto"/>   <!-- dodawanie -->
        </Grid.RowDefinitions>

        <!-- Nagłówek: ramka z tłem na całą szerokość -->
        <Border Grid.Row="0" Background="#FF1E3A8A" Padding="12,8">
            <StackPanel>
                <TextBlock Text="Tablica ogłoszeń 3TI" Foreground="White"
                           FontSize="18" FontWeight="Bold"/>
                <TextBlock x:Name="Licznik" Foreground="LightSteelBlue"/>
            </StackPanel>
        </Border>

        <!-- Lista ogłoszeń: StackPanel w ScrollViewer -->
        <ScrollViewer x:Name="Przewijanie" Grid.Row="1"
                      VerticalScrollBarVisibility="Auto" Background="#FFF3F4F6">
            <StackPanel x:Name="Lista" Margin="10"/>
        </ScrollViewer>

        <!-- Dolny pasek: pole + przyciski, oddzielony linią u góry -->
        <Border Grid.Row="2" BorderBrush="LightGray" BorderThickness="0,1,0,0" Padding="10">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <TextBox x:Name="Tresc" Grid.Column="0" MaxLength="200"
                         VerticalContentAlignment="Center"/>
                <Button Grid.Column="1" Content="Dodaj" Width="70" Height="28"
                        Margin="8,0,0,0" IsDefault="True" Click="Dodaj_Click"/>
                <Button Grid.Column="2" Content="Wyczyść" Width="70" Height="28"
                        Margin="8,0,0,0" Click="Wyczysc_Click"/>
            </Grid>
        </Border>
    </Grid>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Lekcja11;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        DodajOgloszenie("Witamy na tablicy klasy 3TI!");
        DodajOgloszenie("W piątek sprawdzian z programowania aplikacji desktopowych.");
    }

    private void Dodaj_Click(object sender, RoutedEventArgs e)
    {
        string tekst = Tresc.Text.Trim();

        if (tekst == "")
        {
            Tresc.Focus();
            return;
        }

        DodajOgloszenie(tekst);
        Tresc.Clear();
        Tresc.Focus();
    }

    private void Wyczysc_Click(object sender, RoutedEventArgs e)
    {
        if (Lista.Children.Count == 0)
        {
            return;
        }

        MessageBoxResult odp = MessageBox.Show("Usunąć wszystkie ogłoszenia?", "Tablica",
                                               MessageBoxButton.YesNo, MessageBoxImage.Question);
        if (odp == MessageBoxResult.Yes)
        {
            Lista.Children.Clear();
            PokazLicznik();
        }
    }

    // Buduje kartkę ogłoszenia: Border -> StackPanel -> dwa napisy
    private void DodajOgloszenie(string tekst)
    {
        TextBlock godzina = new TextBlock
        {
            Text = DateTime.Now.ToString("HH:mm"),
            Foreground = Brushes.Gray,
            FontSize = 11
        };

        TextBlock tresc = new TextBlock
        {
            Text = tekst,
            TextWrapping = TextWrapping.Wrap
        };

        StackPanel zawartosc = new StackPanel();
        zawartosc.Children.Add(godzina);
        zawartosc.Children.Add(tresc);

        Border kartka = new Border
        {
            Background = Brushes.LightYellow,
            BorderBrush = Brushes.Goldenrod,
            BorderThickness = new Thickness(1),
            CornerRadius = new CornerRadius(6),
            Padding = new Thickness(8),
            Margin = new Thickness(0, 0, 0, 8),
            Child = zawartosc            // Border ma jedno dziecko: Child
        };

        Lista.Children.Add(kartka);
        PokazLicznik();
        Przewijanie.ScrollToEnd();        // pokaż najnowsze ogłoszenie
    }

    private void PokazLicznik()
    {
        Licznik.Text = $"Ogłoszeń: {Lista.Children.Count}";
    }
}

Co dzieje się po kolei

  • Okno to Grid z trzema wierszami. Tylko środkowy, z listą, ma * — dzięki temu ScrollViewer ma ograniczoną wysokość i przewija.
  • Nagłówek to Border z ciemnym tłem i Padding. W środku jest StackPanel, bo ramka ma mieć dwa napisy.
  • Dolny pasek to Border z BorderThickness="0,1,0,0" — rysuje tylko cienką linię u góry, oddzielającą pasek od listy. W środku mała siatka: pole * i dwa przyciski Auto.
  • VerticalContentAlignment="Center" ustawia tekst na środku wysokości pola, które dopasowało się do przycisków.
  • Konstruktor dodaje dwa ogłoszenia startowe tą samą metodą, której używa przycisk — kod budowania karteczki jest w jednym miejscu.
  • DodajOgloszenie buduje karteczkę od środka: dwa napisy → StackPanel → Border z panelem w Child. Potem dodaje ją do listy i odświeża licznik.
  • Przewijanie.ScrollToEnd() przesuwa listę na dół, więc nowe ogłoszenie jest od razu widoczne.
  • Liczba ogłoszeń to po prostu Lista.Children.Count — nie trzeba osobnego pola z licznikiem.

Sprawdź trzy rzeczy

  1. Dodaj kilka długich ogłoszeń, aż pojawi się pasek przewijania. Zmień szerokość okna — tekst w karteczkach zawija się na nowo.
  2. Zamień w ScrollViewer wartość Auto na Visible. Pasek jest widoczny także przy jednej karteczce (wyszarzony).
  3. Usuń z DodajOgloszenie linię Przewijanie.ScrollToEnd();. Gdy lista jest długa, nowe ogłoszenie pojawia się „pod spodem” i trzeba je ręcznie przewinąć.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
Border.ChildkontrolkaJedyna zawartość ramki.
BorderBrush, BorderThicknesspędzel, ThicknessKolor i grubość linii ramki (każdy bok osobno).
CornerRadiusCornerRadiusZaokrąglenie rogów; w kodzie new CornerRadius(6).
VerticalScrollBarVisibility, HorizontalScrollBarVisibilityAuto, Visible, Hidden, DisabledKiedy pokazać pasek przewijania.
ScrollToEnd(), ScrollToHome()metodyPrzewija na dół / na górę.
VerticalOffset, ScrollableHeightdoubleIle przewinięto / ile da się przewinąć.
ScrollChangedzdarzeniePo przewinięciu lub zmianie rozmiaru zawartości. Parametr ScrollChangedEventArgs.
Children.Remove(k), Children.RemoveAt(i)metodyUsuwa wskazaną kontrolkę / kontrolkę o numerze i.
VerticalContentAlignmentTop, Center, …Położenie treści w pionie wewnątrz kontrolki (np. tekstu w polu).
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
dwa napisy bezpośrednio w BorderBłąd: The property 'Child’ is set more than once. Włóż je do panelu.
ScrollViewer w pionowym StackPanelNie przewija — lista wychodzi poza okno. Postaw go w wierszu *.
CornerRadius = 6; w kodzieCS0029 — potrzebny obiekt new CornerRadius(6).
ramka z BorderBrush, ale bez BorderThicknessLinii nie widać — domyślna grubość to 0.
zaokrąglone rogi, a tło kwadratoweTło ustawiłeś na kontrolce w środku zamiast na Border.
HorizontalScrollBarVisibility="Auto" i tekst z TextWrappingTekst przestaje się zawijać, bo w poziomie jest teraz nieskończenie dużo miejsca.
Lista.Children.RemoveAt(0) przy pustej liścieWyjątek ArgumentOutOfRangeException. Najpierw sprawdź Count.
ZADANIA

Zadania

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

ZAD 1Legitymacja szkolna★☆☆

Zbuduj w XAML kartę legitymacji o wymiarach 340 × 210 z zaokrąglonymi rogami i cienką ramką: u góry pasek z nazwą szkoły (zaokrąglone tylko górne rogi), po lewej szary prostokąt 90 × 110 z napisem „FOTO”, po prawej imię, nazwisko, klasa, PESEL zastąpiony gwiazdkami i data ważności. Pod kartą przycisk Przedłuż ważność, który dopisuje rok do daty i zmienia kolor napisu z datą na zielony.

Legitymacja–□✕
ZSMI Lębork
FOTO
Jan Kowalski
klasa 3TI
PESEL: ***********
ważna do: 30.09.2027
Przedłuż ważność
pokaż wskazówkę
  • Zewnętrzny Border zawiera Grid z dwoma wierszami: pasek i treść.
  • Pasek dostaje CornerRadius z czterema liczbami — dolne rogi 0.
  • Datę trzymaj w polu typu DateTime; AddYears(1) zwraca nową datę.
ZAD 2Regulamin pracowni★★☆

Okno z długim regulaminem pracowni komputerowej (co najmniej 12 punktów, wymyśl je) w przewijanym obszarze. Pod nim napis „Przewiń do końca, aby zaakceptować” i nieaktywny przycisk Akceptuję. Przycisk włącza się dopiero, gdy użytkownik przewinie regulamin na sam dół, a napis zmienia się na „Dziękujemy za przeczytanie”. Kliknięcie przycisku pokazuje MessageBox i zamyka okno.

pokaż wskazówkę
  • Regulamin to jeden TextBlock z TextWrapping, a punkty rozdziel <LineBreak/> albo tekstem z \n ustawionym w konstruktorze.
  • Raz włączony przycisk nie powinien się wyłączać, gdy użytkownik przewinie z powrotem do góry.
ZAD 3Dziennik zdarzeń serwera★★☆

Symulator dziennika szkolnego serwera: przyciski Logowanie, Kopia zapasowa, Błąd dysku. Każdy dopisuje do przewijanej listy wpis „08:15:42  Kopia zapasowa zakończona” w ramce z grubą kolorową linią po lewej stronie: niebieską, zieloną albo czerwoną. Lista przewija się do najnowszego wpisu i przechowuje najwyżej 20 wpisów — przy 21. usuwa najstarszy. Nad listą: „Wpisy: 20, błędy: 3”.

pokaż wskazówkę
  • Kolorowa linia po lewej to BorderThickness = new Thickness(5, 0, 0, 0).
  • Wszystkie trzy przyciski mogą wywoływać jedną metodę. Rodzaj wpisu zapisz w Tag (np. "ok", "info", "blad"), a kolor i tekst dobierz w switch.
  • Licznik błędów prowadź w polu klasy — pamiętaj, żeby go zmniejszać, gdy usuwasz najstarszy wpis, który był błędem.
ZAD 4Sklepik szkolny★★☆

Cztery produkty jako karty w XAML (nazwa, cena, przycisk Do koszyka): drożdżówka 3,50 zł, woda 2,20 zł, kanapka 6,90 zł, sok 3,80 zł. Karty stoją w siatce 2 × 2. Każde kliknięcie dodaje produkt do koszyka, a pod kartami widać „Koszyk: 5 szt., razem 19,30 zł”. Karta produktu, który jest w koszyku, ma zieloną ramkę grubości 2. Przycisk Opróżnij koszyk zeruje wszystko i przywraca szare ramki.

pokaż wskazówkę
  • Cenę zapisz w Tag przycisku, a ramki kart nazwij, żeby zmieniać ich kolor.
  • Żeby z przycisku dojść do karty, możesz też zapisać w Tag nazwę i sprawdzić ją w if/switch.
ZAD 5Lista zadań domowych★★★

Pole tekstowe i przycisk Dodaj (Enter). Każde zadanie to karta z treścią po lewej i przyciskiem ✓ Gotowe po prawej. Kliknięcie „Gotowe” usuwa tylko tę kartę. Nad listą: „Do zrobienia: 3”. Gdy lista jest pusta, zamiast niej widać napis „Brak zadań — wolne popołudnie!”. Nie można dodać pustego zadania ani zadania, które już jest na liście (bez względu na wielkość liter).

pokaż wskazówkę
  • Karta: Border → Grid z kolumnami * i Auto → napis i przycisk (ustaw im kolumny przez Grid.SetColumn).
  • Przy tworzeniu zapisz kartę w Tag przycisku. W metodzie Click: (Border)przycisk.Tag i Children.Remove.
  • Żeby porównać treść z istniejącymi zadaniami, zapisz tekst zadania w Tag karty i przejdź pętlą po Lista.Children.
ZAD 6Czat dwóch osób★★★

Okno czatu Ali i Bartka. Na dole dwa wiersze: „Ala:” z polem i przyciskiem Wyślij oraz „Bartek:” z polem i przyciskiem. Wiadomości to dymki w przewijanej liście: Ali przy lewej krawędzi na jasnozielonym tle, Bartka przy prawej na jasnoniebieskim. Dymek ma zaokrąglone rogi (z wyjątkiem jednego, od strony nadawcy), szerokość najwyżej 250 i pod tekstem godzinę. Lista przewija się do najnowszej wiadomości. Dwie kolejne wiadomości tej samej osoby mają mniejszy odstęp niż wiadomości różnych osób.

Czat–□✕
Masz zadanie z WPF?
16:02
Mam, ale zad 6 jeszcze nie.
16:03
Dymki są trudne 😅
16:03
pokaż wskazówkę
  • Dymek to Border z MaxWidth i HorizontalAlignment ustawionym na Left albo Right.
  • Obie osoby mogą korzystać z jednej metody budującej dymek z parametrami: tekst, czy to Ala.
  • Kto pisał ostatni, zapamiętaj w polu klasy — od tego zależy górny margines nowego dymka.
PODSUMOWANIE

Co trzeba zapamiętać

  • Border rysuje tło, linię (BorderBrush + BorderThickness) i zaokrąglone rogi (CornerRadius) wokół jednej kontrolki.
  • Border i ScrollViewer mają jedno dziecko — kilka kontrolek wkładasz do panelu.
  • W kodzie: new CornerRadius(…), new Thickness(…), a zawartość w Child.
  • ScrollViewer przewija tylko wtedy, gdy ma ograniczoną wysokość — postaw go w wierszu *.
  • VerticalScrollBarVisibility="Auto" — pasek tylko wtedy, gdy jest potrzebny. ScrollToEnd() pokazuje najnowszy element.
  • Children.Remove i RemoveAt usuwają pojedynczą kontrolkę z panelu.

Dokumentacja: Microsoft Learn — Border, ScrollViewer.

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