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.
Czego się dziś nauczysz
- Otoczysz kontrolkę ramką z tłem, grubością linii i zaokrąglonymi rogami.
- Wyjaśnisz, dlaczego
BorderiScrollViewermają 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
ScrollViewerwStackPanelnie przewija. - Przewiniesz listę z kodu do końca i sprawdzisz, czy użytkownik dotarł na dół.
- Usuniesz wybraną kontrolkę z panelu.
Border — ramka wokół kontrolki
<Border Background="LightYellow"
BorderBrush="Goldenrod" BorderThickness="2"
CornerRadius="8" Padding="12" Margin="20">
<TextBlock Text="Jutro wycieczka do Gdańska!" FontSize="16"/>
</Border>
ramka z tłem, grubością 2 i rogami zaokrąglonymi o 8
| Właściwość | Przykład | Co robi |
|---|---|---|
Background | LightYellow | Tło wewnątrz ramki (także pod zaokrąglonymi rogami). |
BorderBrush | Goldenrod | Kolor linii ramki. |
BorderThickness | 2 albo 0,0,0,1 | Grubość linii. Cztery liczby jak w Margin: lewa, góra, prawa, dół — 0,0,0,1 to sama kreska na dole. |
CornerRadius | 8 albo 8,8,0,0 | Zaokrąglenie rogów. Cztery liczby: lewy górny, prawy górny, prawy dolny, lewy dolny. |
Padding | 12 | Odstę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.
<!-- 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).
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.
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).
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ą:
<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 |
|---|---|
Auto | pojawia się tylko wtedy, gdy zawartość się nie mieści — zwykle najlepszy wybór |
Visible | widoczny zawsze; domyślna wartość dla pionowego paska |
Hidden | niewidoczny, ale kółkiem myszy dalej da się przewijać |
Disabled | brak 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).
Przewijanie z kodu
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.
Usuwanie z panelu
Znasz już Children.Add, Insert i Clear. Pojedynczą kontrolkę usuniesz na dwa sposoby:
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.
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.
po dodaniu trzeciego ogłoszenia
<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>
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
Gridz trzema wierszami. Tylko środkowy, z listą, ma*— dzięki temuScrollViewerma ograniczoną wysokość i przewija. - Nagłówek to
Borderz ciemnym tłem iPadding. W środku jestStackPanel, bo ramka ma mieć dwa napisy. - Dolny pasek to
BorderzBorderThickness="0,1,0,0"— rysuje tylko cienką linię u góry, oddzielającą pasek od listy. W środku mała siatka: pole*i dwa przyciskiAuto. 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.
DodajOgloszeniebuduje karteczkę od środka: dwa napisy →StackPanel→Borderz panelem wChild. 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
- Dodaj kilka długich ogłoszeń, aż pojawi się pasek przewijania. Zmień szerokość okna — tekst w karteczkach zawija się na nowo.
- Zamień w
ScrollViewerwartośćAutonaVisible. Pasek jest widoczny także przy jednej karteczce (wyszarzony). - Usuń z
DodajOgloszenielinięPrzewijanie.ScrollToEnd();. Gdy lista jest długa, nowe ogłoszenie pojawia się „pod spodem” i trzeba je ręcznie przewinąć.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
Border.Child | kontrolka | Jedyna zawartość ramki. |
BorderBrush, BorderThickness | pędzel, Thickness | Kolor i grubość linii ramki (każdy bok osobno). |
CornerRadius | CornerRadius | Zaokrąglenie rogów; w kodzie new CornerRadius(6). |
VerticalScrollBarVisibility, HorizontalScrollBarVisibility | Auto, Visible, Hidden, Disabled | Kiedy pokazać pasek przewijania. |
ScrollToEnd(), ScrollToHome() | metody | Przewija na dół / na górę. |
VerticalOffset, ScrollableHeight | double | Ile przewinięto / ile da się przewinąć. |
ScrollChanged | zdarzenie | Po przewinięciu lub zmianie rozmiaru zawartości. Parametr ScrollChangedEventArgs. |
Children.Remove(k), Children.RemoveAt(i) | metody | Usuwa wskazaną kontrolkę / kontrolkę o numerze i. |
VerticalContentAlignment | Top, Center, … | Położenie treści w pionie wewnątrz kontrolki (np. tekstu w polu). |
Zanim utkniesz
| Zapis | Problem |
|---|---|
dwa napisy bezpośrednio w Border | Błąd: The property 'Child’ is set more than once. Włóż je do panelu. |
ScrollViewer w pionowym StackPanel | Nie przewija — lista wychodzi poza okno. Postaw go w wierszu *. |
CornerRadius = 6; w kodzie | CS0029 — potrzebny obiekt new CornerRadius(6). |
ramka z BorderBrush, ale bez BorderThickness | Linii nie widać — domyślna grubość to 0. |
| zaokrąglone rogi, a tło kwadratowe | Tło ustawiłeś na kontrolce w środku zamiast na Border. |
HorizontalScrollBarVisibility="Auto" i tekst z TextWrapping | Tekst przestaje się zawijać, bo w poziomie jest teraz nieskończenie dużo miejsca. |
Lista.Children.RemoveAt(0) przy pustej liście | Wyjątek ArgumentOutOfRangeException. Najpierw sprawdź Count. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
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.
pokaż wskazówkę
- Zewnętrzny
BorderzawieraGridz dwoma wierszami: pasek i treść. - Pasek dostaje
CornerRadiusz czterema liczbami — dolne rogi 0. - Datę trzymaj w polu typu
DateTime;AddYears(1)zwraca nową datę.
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
TextBlockzTextWrapping, a punkty rozdziel<LineBreak/>albo tekstem z\nustawionym w konstruktorze. - Raz włączony przycisk nie powinien się wyłączać, gdy użytkownik przewinie z powrotem do góry.
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 wswitch. - Licznik błędów prowadź w polu klasy — pamiętaj, żeby go zmniejszać, gdy usuwasz najstarszy wpis, który był błędem.
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
Tagprzycisku, a ramki kart nazwij, żeby zmieniać ich kolor. - Żeby z przycisku dojść do karty, możesz też zapisać w
Tagnazwę i sprawdzić ją wif/switch.
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→Gridz kolumnami*iAuto→ napis i przycisk (ustaw im kolumny przezGrid.SetColumn). - Przy tworzeniu zapisz kartę w
Tagprzycisku. W metodzieClick:(Border)przycisk.TagiChildren.Remove. - Żeby porównać treść z istniejącymi zadaniami, zapisz tekst zadania w
Tagkarty i przejdź pętlą poLista.Children.
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.
pokaż wskazówkę
- Dymek to
BorderzMaxWidthiHorizontalAlignmentustawionym naLeftalboRight. - 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.
Co trzeba zapamiętać
Borderrysuje tło, linię (BorderBrush+BorderThickness) i zaokrąglone rogi (CornerRadius) wokół jednej kontrolki.BorderiScrollViewermają jedno dziecko — kilka kontrolek wkładasz do panelu.- W kodzie:
new CornerRadius(…),new Thickness(…), a zawartość wChild. ScrollViewerprzewija tylko wtedy, gdy ma ograniczoną wysokość — postaw go w wierszu*.VerticalScrollBarVisibility="Auto"— pasek tylko wtedy, gdy jest potrzebny.ScrollToEnd()pokazuje najnowszy element.Children.RemoveiRemoveAtusuwają pojedynczą kontrolkę z panelu.
Dokumentacja: Microsoft Learn — Border, ScrollViewer.