Kontrolka Canvas – rysowanie i pozycjonowanie
Nauczysz się używać kontrolki Canvas w WPF – umieszczać elementy w dokładnych pozycjach, rysować kształty, obsługiwać mysz i tworzyć proste animacje.
Czym jest Canvas?
Canvas to specjalny kontener w WPF, który pozwala umieszczać elementy
w dokładnych pozycjach podanych w pikselach.
Elementy nie układają się automatycznie – Ty decydujesz gdzie każdy stoi.
Wyobraź sobie, że inne layouty (Grid, StackPanel)
to gotowe szafy z półkami – elementy trafiają na kolejne miejsca.
Canvas to pusta ściana – możesz powiesić coś dokładnie tam,
gdzie chcesz, podając odległość od lewej i od góry.
Porównanie z innymi layoutami
| Layout | Jak pozycjonuje? | Kiedy używać? |
|---|---|---|
StackPanel |
Jeden pod drugim / obok siebie | Proste listy, toolbary |
Grid |
Wiersze i kolumny | Formularze, okna aplikacji |
WrapPanel |
Automatycznie zalega w rzędach | Galerie, tagi |
Canvas |
Dokładna pozycja XY w pikselach | Gry, rysowanie, animacje |
Canvas nie skaluje się automatycznie jak Grid. Jeśli użytkownik zmieni rozmiar okna, elementy pozostają w tych samych pikselowych pozycjach. Canvas jest idealny do gier i rysowania, ale nie do tworzenia formularzy.
Canvas w XAML
Dodanie Canvas do okna wygląda tak samo jak każdy inny kontener. Różnica jest w tym, jak ustawiamy pozycję elementów wewnątrz niego.
<Window x:Class="MojaAplikacja.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Canvas – przykład" Width="500" Height="400"> <Canvas x:Name="moiCanvas" Background="LightGray"> <!-- Prostokąt w pozycji (50, 30) --> <Rectangle Canvas.Left="50" Canvas.Top="30" Width="100" Height="60" Fill="SteelBlue" /> <!-- Koło w pozycji (200, 80) --> <Ellipse Canvas.Left="200" Canvas.Top="80" Width="80" Height="80" Fill="Tomato" /> </Canvas> </Window>
Attached Properties – właściwości dołączone
W Canvas używamy specjalnego zapisu Canvas.Left i Canvas.Top.
To są tak zwane attached properties (właściwości dołączone) –
Canvas „dokłeja” do każdego dziecka informację o jego pozycji.
| Właściwość | Znaczenie | Przykład |
|---|---|---|
Canvas.Left | Odległość od lewej krawędzi | Canvas.Left="50" |
Canvas.Top | Odległość od górnej krawędzi | Canvas.Top="30" |
Canvas.Right | Odległość od prawej krawędzi | Canvas.Right="20" |
Canvas.Bottom | Odległość od dolnej krawędzi | Canvas.Bottom="10" |
Najczęściej używasz Left i Top razem – to wyznacza pozycję lewego górnego rogu elementu.
ZIndex – kolejność warstw
Gdy elementy na sobie leżą, domyślnie ten zdefiniowany później w XAML
jest widoczny na wierzchu. Możesz to zmienić właściwością Canvas.ZIndex.
<!-- Niebieski kwadrat – ZIndex 1, będzie pod czerwonym --> <Rectangle Canvas.Left="40" Canvas.Top="40" Width="120" Height="120" Fill="SteelBlue" Canvas.ZIndex="1" /> <!-- Czerwony kwadrat – ZIndex 2, będzie na wierzchu --> <Rectangle Canvas.Left="80" Canvas.Top="80" Width="120" Height="120" Fill="Tomato" Canvas.ZIndex="2" />
Im wyższa wartość ZIndex, tym element jest bliżej nas (na wierzchu).
Kształty wektorowe
Canvas współpracuje ze specjalnymi klasami kształtów z przestrzeni nazw
System.Windows.Shapes. Są to kształty wektorowe –
możesz je skalować bez utraty jakości.
Rectangle i Ellipse
<!-- Zwykły prostokąt --> <Rectangle Canvas.Left="20" Canvas.Top="20" Width="150" Height="80" Fill="SteelBlue" Stroke="DarkBlue" StrokeThickness="3" /> <!-- Prostokąt z zaokrąglonymi rogami --> <Rectangle Canvas.Left="20" Canvas.Top="120" Width="150" Height="80" Fill="MediumSeaGreen" RadiusX="15" RadiusY="15" /> <!-- Koło (Width == Height) --> <Ellipse Canvas.Left="200" Canvas.Top="20" Width="100" Height="100" Fill="Tomato" Stroke="DarkRed" StrokeThickness="2" /> <!-- Elipsa (różne Width i Height) --> <Ellipse Canvas.Left="200" Canvas.Top="140" Width="160" Height="60" Fill="Gold" />
Właściwości RadiusX i RadiusY zaokrąglają rogi prostokąta.
Im większa wartość, tym bardziej zaokrąglone rogi. Ustawienie obu na 5–20
to popularny sposób na nowoczesny wygląd przycisków i kart.
Line, Polyline, Polygon
<!-- Linia od punktu (10,10) do (200,100) --> <Line X1="10" Y1="10" X2="200" Y2="100" Stroke="Black" StrokeThickness="3" /> <!-- Linia przerywana --> <Line X1="10" Y1="150" X2="200" Y2="150" Stroke="Gray" StrokeThickness="2" StrokeDashArray="4 2" /> <!-- Łamana (otwarta – bez wypełnienia) --> <Polyline Points="20,180 80,120 140,160 200,100 260,140" Stroke="OrangeRed" StrokeThickness="3" Fill="Transparent" /> <!-- Wielokąt (zamknięty – z wypełnieniem) --> <Polygon Points="150,20 180,80 120,80" Fill="MediumPurple" Stroke="DarkViolet" StrokeThickness="2" />
| Kształt | Opis | Wymagane właściwości |
|---|---|---|
Line | Odcinek między dwoma punktami | X1, Y1, X2, Y2 |
Polyline | Łamana – otwarta linia wielopunktowa | Points |
Polygon | Wielokąt – zamknięty, można wypełnić | Points |
Rectangle | Prostokąt | Width, Height |
Ellipse | Elipsa lub koło | Width, Height |
Kolory i wypełnienia
Fill i Stroke
Każdy kształt ma dwie kluczowe właściwości wizualne:
Fill (wypełnienie) i Stroke (obramowanie).
| Właściwość | Co kontroluje | Przykłady wartości |
|---|---|---|
Fill | Wypełnienie wnętrza kształtu | "Red", "#FF5733", "Transparent" |
Stroke | Kolor linii obramowania | "Black", "DarkBlue" |
StrokeThickness | Grubość obramowania w pikselach | "1", "3", "5" |
StrokeDashArray | Wzór przerywania linii | "4 2" = 4px kreska, 2px przerwa |
Gradienty
Zamiast jednolitego koloru możesz użyć gradientu.
WPF obsługuje gradient liniowy (LinearGradientBrush)
i radialny (RadialGradientBrush).
<Rectangle Canvas.Left="20" Canvas.Top="20" Width="200" Height="100"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,0"> <!-- Od lewej --> <GradientStop Color="SteelBlue" Offset="0" /> <!-- Do prawej --> <GradientStop Color="MediumPurple" Offset="1" /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle>
Punkty podaje się jako wartości od 0 do 1.
(0,0) to lewy górny róg, (1,0) to prawy górny róg,
(0,1) to lewy dolny. StartPoint="0,0" EndPoint="1,0"
to gradient poziomy (lewa→prawa), a "0,0"→"0,1" to pionowy (góra→dół).
<Ellipse Canvas.Left="20" Canvas.Top="20" Width="120" Height="120"> <Ellipse.Fill> <RadialGradientBrush> <!-- Centrum – jasny kolor --> <GradientStop Color="Yellow" Offset="0" /> <!-- Krawędź – ciemny kolor --> <GradientStop Color="OrangeRed" Offset="1" /> </RadialGradientBrush> </Ellipse.Fill> </Ellipse>
Praca w C#
Możesz tworzyć i przesuwać elementy Canvas bezpośrednio z kodu C#. Jest to niezbędne w grach i animacjach, gdzie elementy muszą się poruszać.
Dodawanie elementów do Canvas
private void DodajKsztalty() { // 1. Tworzymy obiekt kształtu Rectangle kwadrat = new Rectangle() { Width = 100, Height = 100, Fill = Brushes.SteelBlue, Stroke = Brushes.DarkBlue, StrokeThickness = 2 }; // 2. Ustawiamy pozycję przez static methods Canvas Canvas.SetLeft(kwadrat, 50); // 50 px od lewej Canvas.SetTop(kwadrat, 30); // 30 px od góry // 3. Dodajemy do Canvas moiCanvas.Children.Add(kwadrat); } private void DodajKolo(double x, double y, Brush kolor) { Ellipse kolo = new Ellipse() { Width = 50, Height = 50, Fill = kolor }; Canvas.SetLeft(kolo, x); Canvas.SetTop(kolo, y); moiCanvas.Children.Add(kolo); }
Samo stworzenie obiektu new Rectangle() nie wyświetla go na ekranie.
Musisz jeszcze dodać go do canvas.Children.Add(...).
Bez tego kształt istnieje w pamięci, ale jest niewidoczny.
Przesuwanie elementów
Żeby przesunąć element, wywołujesz Canvas.SetLeft i Canvas.SetTop
z nową pozycją. WPF automatycznie przerysuje element w nowym miejscu.
Rectangle _kwadrat; // pole klasy – przechowujemy referencję double _x = 50; double _y = 50; private void Window_Loaded(object sender, RoutedEventArgs e) { _kwadrat = new Rectangle() { Width = 60, Height = 60, Fill = Brushes.Tomato }; Canvas.SetLeft(_kwadrat, _x); Canvas.SetTop(_kwadrat, _y); moiCanvas.Children.Add(_kwadrat); } private void PrzesunWPrawo() { _x = _x + 10; // zwiększamy X o 10 pikseli Canvas.SetLeft(_kwadrat, _x); // aktualizujemy pozycję }
Odczytywanie pozycji elementu
// Odczyt aktualnej pozycji double aktualneX = Canvas.GetLeft(_kwadrat); double aktualneY = Canvas.GetTop(_kwadrat); // Sprawdzamy czy wyszedł poza prawy brzeg (Canvas ma szerokość 400) if (aktualneX + _kwadrat.Width > moiCanvas.ActualWidth) { // Odbij – przesuń z powrotem Canvas.SetLeft(_kwadrat, 0); }
Usuwanie elementów
// Usuń konkretny element moiCanvas.Children.Remove(_kwadrat); // Usuń wszystko z Canvas moiCanvas.Children.Clear();
Obsługa myszy
Pobieranie pozycji kursora
Metoda e.GetPosition(canvas) zwraca pozycję kursora
względem podanego elementu. Użyj jej w zdarzeniach myszy,
aby wiedzieć gdzie użytkownik kliknął lub gdzie jest kursor.
<Canvas x:Name="moiCanvas" Background="LightGray" MouseLeftButtonDown="Canvas_MouseLeftButtonDown" MouseMove="Canvas_MouseMove"> </Canvas>
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // Pobieramy pozycję kliknięcia względem Canvas Point pozycja = e.GetPosition(moiCanvas); // Wyświetlamy współrzędne (np. w pasku tytułu) Title = $"Kliknięto: X={pozycja.X:F0}, Y={pozycja.Y:F0}"; // Rysujemy małe kółko w miejscu kliknięcia Ellipse punkt = new Ellipse() { Width = 12, Height = 12, Fill = Brushes.Red }; // Centrujemy kółko na klikniętym punkcie (odejmujemy połowę rozmiaru) Canvas.SetLeft(punkt, pozycja.X - 6); Canvas.SetTop(punkt, pozycja.Y - 6); moiCanvas.Children.Add(punkt); } private void Canvas_MouseMove(object sender, MouseEventArgs e) { Point pozycja = e.GetPosition(moiCanvas); Title = $"Kursor: X={pozycja.X:F0}, Y={pozycja.Y:F0}"; }
Przeciąganie elementów (Drag & Drop)
Przeciąganie to trzy etapy: złap (MouseDown), przesuń (MouseMove), puść (MouseUp).
<Canvas x:Name="moiCanvas" Background="LightGray"> <Rectangle x:Name="ruchomyKwadrat" Canvas.Left="100" Canvas.Top="100" Width="80" Height="80" Fill="SteelBlue" Cursor="Hand" MouseLeftButtonDown="Kwadrat_MouseDown" MouseMove="Kwadrat_MouseMove" MouseLeftButtonUp="Kwadrat_MouseUp" /> </Canvas>
bool _przeciagam = false; Point _przesunięcie; // różnica między kursorem a lewym-górnym rogiem elementu private void Kwadrat_MouseDown(object sender, MouseButtonEventArgs e) { _przeciagam = true; // Zapobiegamy "zgubieniu" myszy przy szybkim ruchu ruchomyKwadrat.CaptureMouse(); // Zapamiętujemy gdzie w obrębie elementu kliknęliśmy Point kursorNaCanvas = e.GetPosition(moiCanvas); double elemX = Canvas.GetLeft(ruchomyKwadrat); double elemY = Canvas.GetTop(ruchomyKwadrat); _przesunięcie = new Point(kursorNaCanvas.X - elemX, kursorNaCanvas.Y - elemY); } private void Kwadrat_MouseMove(object sender, MouseEventArgs e) { if (!_przeciagam) return; Point kursor = e.GetPosition(moiCanvas); // Nowa pozycja = pozycja kursora minus zapamiętane przesunięcie double noweX = kursor.X - _przesunięcie.X; double noweY = kursor.Y - _przesunięcie.Y; Canvas.SetLeft(ruchomyKwadrat, noweX); Canvas.SetTop(ruchomyKwadrat, noweY); } private void Kwadrat_MouseUp(object sender, MouseButtonEventArgs e) { _przeciagam = false; ruchomyKwadrat.ReleaseMouseCapture(); // oddajemy przechwycenie myszy }
Bez CaptureMouse(), gdy ruszysz myszą bardzo szybko,
kursor „ucieknie” z elementu i MouseMove przestanie działać –
element zatrzyma się w pół drogi. CaptureMouse() sprawia,
że element dostaje zdarzenia myszy nawet gdy kursor jest poza nim.
Zawsze pamiętaj o ReleaseMouseCapture() w MouseUp!
Animacja – DispatcherTimer
DispatcherTimer to zegar WPF, który co określony czas
wywołuje metodę. Używamy go do animacji – co kilkanaście milisekund
przesuwamy elementy, tworząc efekt płynnego ruchu.
Wyobraź sobie, że rysujesz animację klatkową w zeszycie.
DispatcherTimer to ktoś, kto co chwilę mówi „rysuj następną klatkę!”.
Ty wtedy przesuwasz element o kilka pikseli – i tak 60 razy na sekundę
powstaje płynna animacja.
Przykład: odbijająca się piłka
<Canvas x:Name="moiCanvas" Background="#1a1a2e"> <Ellipse x:Name="pilka" Width="40" Height="40" Fill="OrangeRed" Canvas.Left="100" Canvas.Top="100" /> </Canvas>
double _x = 100, _y = 100; // pozycja piłki double _vx = 4, _vy = 3; // prędkość: ile pikseli na klatkę DispatcherTimer _timer; private void Window_Loaded(object sender, RoutedEventArgs e) { _timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromMilliseconds(16); // ~60 klatek/sek _timer.Tick += Timer_Tick; _timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { // Przesuwamy piłkę _x += _vx; _y += _vy; // Sprawdzamy odbicie od ścian if (_x <= 0 || _x + pilka.Width >= moiCanvas.ActualWidth) _vx = -_vx; // odbij poziomo if (_y <= 0 || _y + pilka.Height >= moiCanvas.ActualHeight) _vy = -_vy; // odbij pionowo // Aktualizujemy pozycję na Canvas Canvas.SetLeft(pilka, _x); Canvas.SetTop(pilka, _y); }
| Interwał | Klatek/sek (FPS) | Zastosowanie |
|---|---|---|
16 ms | ~60 FPS | Płynne gry i animacje |
33 ms | ~30 FPS | Wystarczające dla większości gier |
100 ms | 10 FPS | Proste animacje, UI updates |
500 ms | 2 FPS | Odliczanie, tik-tok |
Projekt – gra „łap kółka”
Połączymy wszystko w małą grę: co sekundę pojawia się losowe kółko, gracz musi je kliknąć zanim zniknie. Za każde kliknięcie dostaje punkt.
<Grid> <Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <!-- Pasek z wynikiem --> <StackPanel Grid.Row="0" Orientation="Horizontal" Background="#222" VerticalAlignment="Stretch"> <TextBlock Text="Wynik: " Foreground="White" FontSize="18" Margin="10,0" VerticalAlignment="Center" /> <TextBlock x:Name="lblWynik" Text="0" Foreground="OrangeRed" FontSize="18" FontWeight="Bold" VerticalAlignment="Center" /> </StackPanel> <!-- Plansza gry --> <Canvas x:Name="moiCanvas" Grid.Row="1" Background="#1a1a2e" /> </Grid>
int _wynik = 0; Ellipse _aktualneKolo; DispatcherTimer _timer; Random _rnd = new Random(); private void Window_Loaded(object sender, RoutedEventArgs e) { _timer = new DispatcherTimer(); _timer.Interval = TimeSpan.FromSeconds(1); // nowe kółko co sekundę _timer.Tick += Timer_Tick; _timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { // Usuń poprzednie kółko jeśli istnieje if (_aktualneKolo != null) moiCanvas.Children.Remove(_aktualneKolo); // Losujemy rozmiar i pozycję nowego kółka int rozmiar = _rnd.Next(30, 80); double maxX = moiCanvas.ActualWidth - rozmiar; double maxY = moiCanvas.ActualHeight - rozmiar; double x = _rnd.Next(0, (int)maxX); double y = _rnd.Next(0, (int)maxY); // Losujemy kolor Brush[] kolory = { Brushes.OrangeRed, Brushes.LimeGreen, Brushes.Cyan, Brushes.Yellow, Brushes.HotPink }; Brush kolor = kolory[_rnd.Next(kolory.Length)]; // Tworzymy nowe kółko _aktualneKolo = new Ellipse() { Width = rozmiar, Height = rozmiar, Fill = kolor, Cursor = Cursors.Hand }; // Podpinamy obsługę kliknięcia _aktualneKolo.MouseLeftButtonDown += Kolo_Klikniete; Canvas.SetLeft(_aktualneKolo, x); Canvas.SetTop(_aktualneKolo, y); moiCanvas.Children.Add(_aktualneKolo); } private void Kolo_Klikniete(object sender, MouseButtonEventArgs e) { _wynik++; lblWynik.Text = _wynik.ToString(); // Usuń kliknięte kółko natychmiast moiCanvas.Children.Remove(_aktualneKolo); _aktualneKolo = null; // Zatrzymujemy propagację zdarzenia (nie przeniknie do Canvas) e.Handled = true; }
Timer co sekundę usuwa stare kółko i tworzy nowe w losowym miejscu.
Kliknięcie kółka (Kolo_Klikniete) zwiększa wynik i usuwa je od razu.
Właściwość e.Handled = true zatrzymuje zdarzenie – bez tego
kliknięcie „przeleciałoby” dalej do Canvas, co mogłoby powodować nieoczekiwane efekty.
Częste błędy i pułapki
❌ Błąd 1: Zapomniane Children.Add
❌ Źle
Rectangle r = new Rectangle(); Canvas.SetLeft(r, 50); // Zapomniałem dodać do Canvas! // Element nie pojawi się na ekranie
✅ Dobrze
Rectangle r = new Rectangle(); Canvas.SetLeft(r, 50); Canvas.SetTop(r, 30); moiCanvas.Children.Add(r); // ✅
❌ Błąd 2: ActualWidth = 0 przed załadowaniem okna
❌ Źle – w konstruktorze
public MainWindow()
{
InitializeComponent();
// ActualWidth = 0! Okno jeszcze niewidoczne
double x = moiCanvas.ActualWidth / 2;
}
✅ Dobrze – w Loaded
private void Window_Loaded(object s, RoutedEventArgs e)
{
// Tutaj ActualWidth ma prawidłową wartość
double x = moiCanvas.ActualWidth / 2;
}
❌ Błąd 3: Brak ReleaseMouseCapture
❌ Źle
private void MouseDown(...)
{
element.CaptureMouse();
// Brak ReleaseMouseCapture w MouseUp!
// Mysz "utknięta" przy elemencie
}
✅ Dobrze
private void MouseUp(...)
{
_przeciagam = false;
element.ReleaseMouseCapture(); // ✅
}
❌ Błąd 4: Modyfikacja kolekcji podczas iteracji
❌ Źle – wyjątek!
foreach (var elem in moiCanvas.Children)
{
// Błąd: nie można usuwać
// podczas iteracji foreach!
moiCanvas.Children.Remove(elem);
}
✅ Dobrze
// Usuń wszystko jednocześnie moiCanvas.Children.Clear(); // Lub usuń konkretny element moiCanvas.Children.Remove(konkretny);
Zadania do wykonania
Scena XAML – narysuj w XAML prostą scenę zawierającą:
- Niebieskie niebo (tło Canvas)
- Żółte słońce (Ellipse w prawym górnym rogu)
- Zieloną trawę (Rectangle na dole, szerokość = cały Canvas)
- Brązowy dom (Rectangle + trójkąt dach z Polygon)
Klikanie rysuje – po kliknięciu w Canvas dodawaj małe kółko (średnica 20px) w miejscu kliknięcia. Dodaj przycisk „Wyczyść” który usuwa wszystkie kółka.
Ruchomy pojazd – narysuj prostego samochodu (Rectangle + 2× Ellipse jako koła)
i animuj go za pomocą DispatcherTimer tak, by jechał od lewej do prawej.
Gdy wyjedzie poza krawędź, pojawia się z lewej strony od nowa.
Przeciągnij kwadraty – wyświetl 5 kolorowych kwadratów i umożliw ich przeciąganie myszą (Drag & Drop). Użytkownik powinien móc dowolnie układać je na planszy.
Gra Snake – zaimplementuj prostego węża:
- Wąż porusza się po siatce (np. 20×20 komórek, każda 20px)
- Strzałki klawiatury zmieniają kierunek
- Co sekundę pojawia się „jabłko” (czerwone kółko) w losowym miejscu
- Zjedzenie jabłka wydłuża węża o 1 i dodaje punkt
- Uderzenie w ścianę lub samego siebie kończy grę