Canvas — płótno ze współrzędnymi
Wszystkie dotychczasowe panele same decydowały, gdzie postawić kontrolkę: jedna pod drugą, w komórce siatki, przy krawędzi. W grach i rysunkach chcesz powiedzieć wprost: „postaw piłkę w punkcie (120, 80)”. Do tego służy Canvas — płótno, na którym każda kontrolka ma swoje współrzędne. Połączysz je z myszą, klawiaturą, zegarem i losowaniem z poprzednich lekcji.
Czego się dziś nauczysz
- Umieścisz kontrolkę na płótnie w podanym punkcie przez
Canvas.LeftiCanvas.Top. - Odczytasz i zmienisz pozycję z kodu metodami
Canvas.GetLeft/SetLeft. - Ustalisz, która kontrolka jest na wierzchu, przez
Panel.ZIndex. - Zatrzymasz obiekt na krawędziach płótna i odbijesz go od ścian.
- Sprawdzisz, czy dwa prostokąty na płótnie na siebie nachodzą.
Współrzędne na płótnie
<Canvas Width="400" Height="250" Background="AliceBlue">
<Border Canvas.Left="40" Canvas.Top="30" Width="80" Height="50" Background="Tomato"/>
<Border Canvas.Left="200" Canvas.Top="120" Width="60" Height="60"
Background="SteelBlue" CornerRadius="30"/>
<TextBlock Canvas.Right="10" Canvas.Bottom="10" Text="prawy dolny róg"/>
</Canvas>
każda kontrolka ma swój punkt — lewy górny róg kontrolki w podanych współrzędnych
Canvas.Left to odległość lewej krawędzi kontrolki od lewej krawędzi płótna, Canvas.Top — górnej od górnej. Oś Y rośnie w dół, jak w lekcji 25. Można też liczyć od prawej i dolnej krawędzi (Canvas.Right, Canvas.Bottom), ale nie mieszaj obu sposobów dla jednej kontrolki. Kontrolki na płótnie się nie rozciągają — podaj im Width i Height.
Pozycja z kodu
Canvas.Left to właściwość dołączona — jak Grid.Row. W kodzie czytasz i zmieniasz ją metodami klasy Canvas:
double x = Canvas.GetLeft(Gracz); // odczyt
double y = Canvas.GetTop(Gracz);
Canvas.SetLeft(Gracz, x + 10); // przesunięcie o 10 w prawo
Canvas.SetTop(Gracz, y - 5); // i o 5 w górę
// nowa kontrolka w punkcie (100, 50)
Border kafel = new Border { Width = 40, Height = 40, Background = Brushes.Gold };
Canvas.SetLeft(kafel, 100);
Canvas.SetTop(kafel, 50);
Plotno.Children.Add(kafel);
GetLeft bez ustawionej pozycji zwraca NaN
Jeżeli kontrolka nie ma ustawionego Canvas.Left, płótno rysuje ją przy lewej krawędzi, ale Canvas.GetLeft zwraca double.NaN — „nie liczba”. NaN + 10 to dalej NaN i kontrolka „nie chce się ruszyć”. Ustaw pozycję w XAML albo w kodzie, zanim zaczniesz ją zmieniać.
Kolejność rysowania
Kontrolki na płótnie mogą na siebie nachodzić. Domyślnie późniejsza w Children jest na wierzchu. Kolejność zmienisz bez przestawiania kontrolek właściwością Panel.ZIndex — większa liczba rysuje się wyżej:
<TextBlock Text="PAUZA" Panel.ZIndex="100" .../>
Panel.SetZIndex(Gracz, 10); // gracz ponad tłem i przeszkodami
ZIndex działa w każdym panelu (także w komórce Grid), ale najczęściej używa się go na płótnie.
Rozmiar płótna i krawędzie
Płótno nie ogranicza kontrolek — kontrolka z Canvas.Left="900" narysuje się poza płótnem, a nawet na innych częściach okna. ClipToBounds="True" obcina wszystko, co wychodzi poza płótno. Pilnowanie krawędzi należy do Ciebie:
double maxX = Plotno.ActualWidth - Gracz.Width; // najdalej w prawo
double maxY = Plotno.ActualHeight - Gracz.Height; // najniżej
double x = Canvas.GetLeft(Gracz) + dx;
x = Math.Max(0, Math.Min(x, maxX)); // „przytnij” do przedziału 0…maxX
Canvas.SetLeft(Gracz, x);
Gdy płótno ma stały Width/Height, możesz używać tych liczb. Gdy rośnie razem z oknem (stoi w wierszu * albo jako ostatnie dziecko DockPanel), korzystaj z ActualWidth/ActualHeight — rozmiaru po ułożeniu.
Kolizje
W grach co chwilę pytasz: „czy gracz dotknął monety?”. Najprostszy test traktuje obie kontrolki jak prostokąty. Dwa prostokąty nie nachodzą na siebie, jeśli jeden leży w całości na lewo, na prawo, nad albo pod drugim. W każdym innym przypadku — nachodzą:
private bool Koliduja(FrameworkElement a, FrameworkElement b)
{
double ax = Canvas.GetLeft(a), ay = Canvas.GetTop(a);
double bx = Canvas.GetLeft(b), by = Canvas.GetTop(b);
return ax < bx + b.Width && ax + a.Width > bx // nachodzą w poziomie
&& ay < by + b.Height && ay + a.Height > by; // i w pionie
}
FrameworkElement to wspólna klasa bazowa ramek, napisów, przycisków i obrazów — dzięki temu ta sama metoda działa dla każdej pary kontrolek.
Odbijające się piłki
Kliknięcie w płótno dodaje w tym miejscu piłkę w losowym kolorze, lecącą w losowym kierunku. Zegar 50 razy na sekundę przesuwa wszystkie piłki i odbija je od ścian. Prawy przycisk na piłce ją usuwa, Spacja wstrzymuje ruch (napis „PAUZA” jest zawsze na wierzchu dzięki ZIndex), a C czyści płótno. Płótno rośnie razem z oknem — piłki odbijają się od jego aktualnych krawędzi.
pięć piłek w ruchu — strzałki pokazują kierunki
using System.Windows.Controls;
namespace Lekcja29;
// Jedna piłka: kontrolka na płótnie + prędkość
public class Pilka
{
public Border Widok { get; }
public double Vx { get; set; } // przesunięcie w poziomie na jedno tyknięcie
public double Vy { get; set; } // przesunięcie w pionie
public Pilka(Border widok, double vx, double vy)
{
Widok = widok;
Vx = vx;
Vy = vy;
}
// pozycja czytana i zapisywana prosto na płótnie
public double X
{
get => Canvas.GetLeft(Widok);
set => Canvas.SetLeft(Widok, value);
}
public double Y
{
get => Canvas.GetTop(Widok);
set => Canvas.SetTop(Widok, value);
}
}
<Window x:Class="Lekcja29.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Odbijające się piłki" Height="480" Width="680"
WindowStartupLocation="CenterScreen" KeyDown="Window_KeyDown">
<DockPanel>
<Border DockPanel.Dock="Top" Background="#FF1F2937" Padding="10,6">
<DockPanel>
<TextBlock x:Name="Licznik" DockPanel.Dock="Right" Foreground="White"
VerticalAlignment="Center"/>
<TextBlock Foreground="White" VerticalAlignment="Center"
Text="Klik — nowa piłka • prawy klik na piłce — usuń • Spacja — pauza • C — wyczyść"/>
</DockPanel>
</Border>
<!-- płótno: współrzędne liczone od lewego górnego rogu -->
<Canvas x:Name="Plotno" Background="#FFF8FAFC" ClipToBounds="True"
MouseLeftButtonDown="Plotno_MouseLeftButtonDown">
<TextBlock x:Name="Pauza" Text="PAUZA" FontSize="48" FontWeight="Bold"
Foreground="#40000000" Canvas.Left="20" Canvas.Top="10"
Panel.ZIndex="100" Visibility="Collapsed"/>
</Canvas>
</DockPanel>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Lekcja29;
public partial class MainWindow : Window
{
private const double Srednica = 36;
private readonly Random los = new Random();
private readonly DispatcherTimer zegar = new DispatcherTimer();
private readonly List<Pilka> pilki = new List<Pilka>();
public MainWindow()
{
InitializeComponent();
zegar.Interval = TimeSpan.FromMilliseconds(20); // ok. 50 klatek na sekundę
zegar.Tick += Zegar_Tick;
zegar.Start();
PokazLicznik();
}
// Klik w płótno: nowa piłka w miejscu kursora
private void Plotno_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point p = e.GetPosition(Plotno);
Border widok = new Border
{
Width = Srednica,
Height = Srednica,
CornerRadius = new CornerRadius(Srednica / 2),
Background = new SolidColorBrush(Color.FromRgb(
(byte)los.Next(40, 220), (byte)los.Next(40, 220), (byte)los.Next(40, 220))),
BorderBrush = Brushes.Black,
BorderThickness = new Thickness(1)
};
widok.MouseRightButtonDown += Pilka_MouseRightButtonDown;
// prędkość: 2–6 pikseli na tyknięcie, w losowym kierunku
double vx = los.Next(2, 7) * (los.Next(2) == 0 ? -1 : 1);
double vy = los.Next(2, 7) * (los.Next(2) == 0 ? -1 : 1);
Pilka pilka = new Pilka(widok, vx, vy);
Plotno.Children.Add(widok);
pilka.X = p.X - Srednica / 2; // środek piłki pod kursorem
pilka.Y = p.Y - Srednica / 2;
pilki.Add(pilka);
PokazLicznik();
}
// Prawy klik na piłce: usunięcie
private void Pilka_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
Border widok = (Border)sender;
foreach (Pilka p in pilki)
{
if (p.Widok == widok)
{
pilki.Remove(p);
break; // po Remove nie wolno iść dalej pętlą
}
}
Plotno.Children.Remove(widok);
PokazLicznik();
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
zegar.IsEnabled = !zegar.IsEnabled;
Pauza.Visibility = zegar.IsEnabled ? Visibility.Collapsed : Visibility.Visible;
}
else if (e.Key == Key.C)
{
foreach (Pilka p in pilki)
{
Plotno.Children.Remove(p.Widok);
}
pilki.Clear();
PokazLicznik();
}
}
// Każde tyknięcie: przesuń wszystkie piłki, odbij od ścian
private void Zegar_Tick(object? sender, EventArgs e)
{
double szer = Plotno.ActualWidth;
double wys = Plotno.ActualHeight;
foreach (Pilka p in pilki)
{
p.X += p.Vx;
p.Y += p.Vy;
if (p.X < 0) // lewa ściana
{
p.X = 0;
p.Vx = -p.Vx;
}
else if (p.X > szer - Srednica) // prawa ściana
{
p.X = szer - Srednica;
p.Vx = -p.Vx;
}
if (p.Y < 0) // sufit
{
p.Y = 0;
p.Vy = -p.Vy;
}
else if (p.Y > wys - Srednica) // podłoga
{
p.Y = wys - Srednica;
p.Vy = -p.Vy;
}
}
}
private void PokazLicznik()
{
Licznik.Text = $"Piłek: {pilki.Count}";
}
}
Co dzieje się po kolei
- Każda piłka to obiekt klasy
Pilka(lekcja 23): kontrolka na płótnie i prędkość w obu osiach. Wszystkie piłki są wList<Pilka>. - Właściwości
XiYw klasiePilkanie przechowują liczby — ichgetisetczytają i ustawiają pozycję na płótnie. Dzięki temu w kodzie okna piszesz po prostup.X += p.Vx. - Płótno ma tło — inaczej kliknięcie w puste miejsce nie wywołałoby
MouseLeftButtonDown(lekcja 25).ClipToBoundsukrywa fragment piłki, która na chwilę wyjdzie za krawędź po zmniejszeniu okna. - Nowa piłka staje środkiem pod kursorem: od pozycji kursora odejmujemy połowę średnicy. Kolor i prędkość są losowe (lekcja 28);
los.Next(2) == 0 ? -1 : 1losuje kierunek. Zegar_Tickprzesuwa każdą piłkę o jej prędkość. Gdy piłka wyjdzie poza krawędź, wraca na krawędź, a odpowiednia składowa prędkości zmienia znak — to odbicie.- Granice liczone są z
ActualWidthiActualHeightpłótna, bo płótno rośnie razem z oknem. - Usuwając piłkę, szukamy jej obiektu po kontrolce i przerywamy pętlę
breakzaraz poRemove— dalsze przechodzenie po zmienionej liście skończyłoby się wyjątkiem. - Napis „PAUZA” ma
Panel.ZIndex="100", więc jest nad wszystkimi piłkami, choć dodano go do płótna jako pierwszy.
Sprawdź trzy rzeczy
- Dodaj kilkadziesiąt piłek i zmień rozmiar okna — piłki dopasują się do nowych krawędzi.
- Usuń
Panel.ZIndex="100"z napisu „PAUZA” i wstrzymaj grę nad piłką. Napis będzie pod piłkami. - Usuń z
Plotno_MouseLeftButtonDowndwie linie ustawiającepilka.Xipilka.Y. Piłki pojawią się w lewym górnym rogu i… nie ruszą się, boCanvas.GetLeftzwracaNaN.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
Canvas.Left, Canvas.Top | double | Właściwości dołączone — pozycja lewego górnego rogu kontrolki. |
Canvas.Right, Canvas.Bottom | double | Odległość od prawej / dolnej krawędzi płótna. |
Canvas.GetLeft(k), SetLeft(k, x), GetTop, SetTop | metody | Odczyt i zmiana pozycji z kodu; brak pozycji → NaN. |
Panel.ZIndex, Panel.SetZIndex(k, n) | int | Kolejność rysowania — większa liczba na wierzchu. |
ClipToBounds | bool | Obcina to, co wychodzi poza panel. |
FrameworkElement | klasa | Wspólna baza kontrolek z Width, Height, Margin. |
get => …; set => …; | właściwość | Właściwość, której odczyt i zapis wykonują kod. |
Math.Max(a, Math.Min(x, b)) | double | Przycięcie x do przedziału a…b. |
Zanim utkniesz
| Sytuacja | Problem |
|---|---|
| kontrolka „nie rusza się” | Brak początkowego Canvas.Left/Top — GetLeft zwraca NaN. |
Left="40" bez przedrostka | Błąd XAML — potrzebne Canvas.Left. |
ramka na płótnie bez Width/Height | Ma rozmiar zawartości — pusta jest niewidoczna. |
| płótno bez tła | Kliknięcia w puste miejsce nie są zgłaszane. |
krawędź liczona jako x > Plotno.ActualWidth | Obiekt wychodzi poza płótno o całą swoją szerokość. Odejmij szerokość obiektu. |
usuwanie z listy w pętli foreach bez break | InvalidOperationException: kolekcja została zmieniona. |
Plotno.Width płótna rozciąganego z oknem | NaN — rozmiar nie był ustawiony. Użyj ActualWidth. |
Zadania
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Obiekty na płótnie zrób z ramek, napisów i obrazów. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
Płótno 400 × 260 z postacią (niebieskie kółko 30 × 30) w lewym górnym rogu i metą (złoty kwadrat) w prawym dolnym. Strzałki przesuwają postać o 10 pikseli, a z Shift — o 30. Postać nie wychodzi poza płótno. Pod płótnem pozycja „x = 120, y = 40” i liczba ruchów. Po dotarciu do mety: „Meta w 34 ruchach!”, meta przenosi się w losowe miejsce, a licznik się zeruje.
pokaż wskazówkę
- Dotarcie do mety to kolizja dwóch prostokątów — skorzystaj z metody
Koliduja.
Kliknięcie w płótno przykleja w tym miejscu naklejkę — wybrany z listy symbol (★ ♥ ☀ ☂ ✿ ♫) w wybranym kolorze, wyśrodkowany pod kursorem. Kółko myszy nad naklejką ją powiększa lub zmniejsza (rozmiar czcionki 16–96), prawy przycisk usuwa, a dwuklik przenosi na wierzch. Pod płótnem: „Naklejek: 7”.
pokaż wskazówkę
- Rozmiar napisu znasz dopiero po ułożeniu — do środkowania użyj wymiarów przybliżonych (np. rozmiar czcionki) albo
ActualWidthpo dodaniu. - „Na wierzch”: pole z rosnącym licznikiem i
Panel.SetZIndex(naklejka, ++licznik).
Na płótnie pięć kolorowych kafelków z nazwami mebli (biurko, krzesło, szafa, łóżko, regał) — plan pokoju. Kafelek można przeciągnąć myszą: wciśnięcie lewego przycisku chwyta go, ruch przesuwa, puszczenie odkłada. Po odłożeniu pozycja „przyskakuje” do siatki co 20 pikseli. Przeciągany kafelek jest na wierzchu i półprzezroczysty. Kafelek nie może wyjść poza płótno.
pokaż wskazówkę
- Przy chwyceniu zapamiętaj przesunięcie kursora względem lewego górnego rogu kafelka — inaczej kafelek „skoczy” rogiem pod kursor.
kafel.CaptureMouse()sprawia, że kafelek dostaje ruch myszy nawet przy szybkim ruchu poza nim; wMouseLeftButtonUpwywołajReleaseMouseCapture().- Przyskok:
Math.Round(x / 20) * 20.
Gra na 30 sekund: okrągły cel pojawia się w losowym miejscu płótna i co 1,2 s przeskakuje gdzie indziej. Trafienie (kliknięcie celu) daje punkt i natychmiastowy przeskok; kliknięcie obok to pudło. Co 5 trafień cel robi się mniejszy, a przeskakuje szybciej. Nad płótnem: punkty, pudła, celność w procentach i pozostały czas. Kursor nad płótnem to celownik.
pokaż wskazówkę
- Kliknięcie celu „przechodzi” też do płótna. W metodzie celu ustaw
e.Handled = true, żeby płótno nie policzyło pudła. - Dwa zegary: jeden do przeskoków celu, drugi (albo różnica
DateTime) do czasu gry.
Dwóch graczy na jednym płótnie: czerwony sterowany W A S D, niebieski — strzałkami. Jeden goni („berek” — z napisem B na kółku). Gdy goniący dotknie uciekającego, role się zamieniają, uciekający dostaje 2 sekundy ochrony, a goniący punkt. Gracze ruszają się płynnie, dopóki klawisz jest wciśnięty — także obaj naraz. Gra trwa 60 sekund.
pokaż wskazówkę
- Płynny ruch obu graczy:
KeyDowniKeyUpustawiają polabool(np.wGore), a zegar co 20 ms przesuwa graczy według tych pól (lekcja 26).
Klasyczny Pong dla dwóch graczy: lewa paletka na W/S, prawa na strzałkach, piłka odbija się od górnej i dolnej krawędzi oraz od paletek. Piłka, która minie paletkę, daje punkt przeciwnikowi i wraca na środek z losowym kierunkiem. Po każdym odbiciu od paletki piłka przyspiesza o 5%, a kąt odbicia zależy od miejsca uderzenia w paletkę. Gra do 7 punktów; Spacja — start i pauza.
pokaż wskazówkę
- Kąt: zmierz, jak daleko od środka paletki uderzyła piłka (od −1 do 1), i ustaw
Vyproporcjonalnie. - Po odbiciu od paletki przesuń piłkę tuż przed paletkę — inaczej w następnym tyknięciu znów wykryjesz kolizję i piłka „utknie”.
Co trzeba zapamiętać
Canvasustawia kontrolki według współrzędnych:Canvas.Left,Canvas.Top, od lewego górnego rogu, y w dół.- Z kodu:
Canvas.GetLeft/SetLeft; bez ustawionej pozycjiGetLeftdajeNaN. Panel.ZIndex— kto na wierzchu;ClipToBounds— obcinanie do płótna.- Krawędzie pilnujesz sam: od rozmiaru płótna (
ActualWidth) odejmij rozmiar obiektu. - Kolizja dwóch prostokątów: nachodzą w poziomie i w pionie.
Dokumentacja: Microsoft Learn — klasa Canvas.