Projektujesz edytor diagramów, prostą planszę do gry albo panel, na którym elementy mają pojawiać się w konkretnych miejscach? Canvas w WPF daje pełną kontrolę nad współrzędnymi, ale wymaga świadomego podejścia do układu, obsługi myszy i wydajności. Pokażę, jak działa, kiedy warto go użyć, jak pozycjonować elementy w XAML i C#, a także gdzie lepiej wybrać inne rozwiązanie.
Canvas daje precyzję, lecz nie zastępuje responsywnego układu interfejsu
- Pozycjonowanie odbywa się za pomocą właściwości Canvas.Left, Canvas.Top, Canvas.Right i Canvas.Bottom.
- Najlepsze zastosowania to diagramy, mapy, plansze, wizualizacje i elementy poruszające się po określonych współrzędnych.
- Canvas nie układa automatycznie dzieci tak jak Grid, StackPanel czy DockPanel.
- Interakcje można obsługiwać przez zdarzenia myszy, hit testing oraz właściwość Canvas.ZIndex.
- Wydajność zależy głównie od liczby elementów wizualnych i kosztu całego drzewa UI.

Czym jest Canvas w WPF i kiedy ma sens
Canvas jest panelem układu należącym do Windows Presentation Foundation. Jego zadanie jest proste: tworzy obszar, w którym można umieścić elementy interfejsu za pomocą konkretnych współrzędnych. W przeciwieństwie do Grid czy StackPanel nie próbuje samodzielnie zdecydować, gdzie powinno znaleźć się dziecko.
To ważne rozróżnienie. Canvas nie jest wyłącznie „płótnem do rysowania”. Może zawierać przyciski, teksty, obrazy, kontrolki użytkownika oraz obiekty graficzne, takie jak Rectangle, Ellipse, Line czy Path. W praktyce traktuję go jako warstwę współrzędnych, na której aplikacja może świadomie rozmieszczać elementy.
Najlepiej sprawdza się tam, gdzie położenie obiektu wynika z danych albo działania użytkownika. Dobrymi przykładami są:
- edytory diagramów i schematów,
- mapy pomieszczeń, magazynów lub sieci,
- plansze do gier i proste sceny 2D,
- wykresy wymagające własnego rozmieszczania punktów,
- interfejsy typu whiteboard,
- nakładki i znaczniki umieszczane nad obrazem.
Nie używałbym go natomiast jako głównego panelu całego okna biznesowej aplikacji. Formularz z etykietami, polami i przyciskami znacznie lepiej zbudować przy pomocy Grid. Po zmianie rozmiaru okna elementy umieszczone absolutnie mogą zostać w starych miejscach, a użytkownik zobaczy puste obszary albo nachodzące na siebie kontrolki.
Jak ustawiać elementy na powierzchni
Pozycję dziecka określają tak zwane właściwości dołączone. Są to właściwości przechowywane przez element nadrzędny, ale ustawiane bezpośrednio na jego dzieciach. W przypadku Canvas najczęściej używa się Canvas.Left i Canvas.Top.
Współrzędne liczone są od lewego górnego rogu Canvas. Wartość Canvas.Left="80" oznacza odsunięcie elementu o 80 jednostek od lewej krawędzi, a Canvas.Top="60" o 60 jednostek od góry. WPF używa jednostek niezależnych od urządzenia, więc nie należy mylić ich bezpośrednio z fizycznymi pikselami monitora.
To samo można zrobić w C#. Przydaje się, gdy elementy powstają na podstawie danych, są dodawane dynamicznie albo użytkownik tworzy je podczas działania programu.
var rectangle = new Rectangle
{
Width = 160,
Height = 90,
Fill = Brushes.SteelBlue
};
Canvas.SetLeft(rectangle, 80);
Canvas.SetTop(rectangle, 60);
DesignSurface.Children.Add(rectangle);Do odczytania aktualnego położenia służą metody Canvas.GetLeft i Canvas.GetTop. Jeżeli właściwość nie została ustawiona, może zwrócić wartość NaN, dlatego przy odczycie danych warto zastosować wartość domyślną.
double left = Canvas.GetLeft(rectangle);
if (double.IsNaN(left))
{
left = 0;
}Canvas obsługuje również Canvas.Right i Canvas.Bottom. Są przydatne wtedy, gdy wygodniej określić odległość od prawej albo dolnej krawędzi. W jednym elemencie nie mieszam bez potrzeby wszystkich czterech właściwości. Czytelniejszy kod zwykle powstaje wtedy, gdy pozycję definiuję konsekwentnie przez lewy i górny offset.
Jak zrobić przesuwanie elementów myszą
Najczęstszy praktyczny przypadek to przeciąganie obiektu po powierzchni. Potrzebujemy zapamiętać punkt, w którym użytkownik rozpoczął ruch, pobrać pozycję kursora względem Canvas, a potem ustawiać nowe wartości Canvas.Left i Canvas.Top.
private UIElement? draggedElement;
private Point mouseOffset;
private bool isDragging;
private void Element_MouseLeftButtonDown(
object sender,
MouseButtonEventArgs e)
{
draggedElement = (UIElement)sender;
Point elementPosition = new Point(
Canvas.GetLeft(draggedElement),
Canvas.GetTop(draggedElement));
Point mousePosition = e.GetPosition(DesignSurface);
mouseOffset = new Point(
mousePosition.X - elementPosition.X,
mousePosition.Y - elementPosition.Y);
isDragging = true;
draggedElement.CaptureMouse();
e.Handled = true;
}
private void Element_MouseMove(
object sender,
MouseEventArgs e)
{
if (!isDragging || draggedElement is null)
{
return;
}
Point position = e.GetPosition(DesignSurface);
Canvas.SetLeft(
draggedElement,
position.X - mouseOffset.X);
Canvas.SetTop(
draggedElement,
position.Y - mouseOffset.Y);
}
private void Element_MouseLeftButtonUp(
object sender,
MouseButtonEventArgs e)
{
if (draggedElement is null)
{
return;
}
isDragging = false;
draggedElement.ReleaseMouseCapture();
draggedElement = null;
}W XAML trzeba podpiąć zdarzenia do przesuwanego elementu:
Istotne jest zapamiętanie offsetu kursora względem lewego górnego rogu obiektu. Bez tego prosty przykład często powoduje „przeskok” prostokąta tak, aby jego róg znalazł się dokładnie pod kursorem. CaptureMouse dodatkowo sprawia, że aplikacja nadal otrzymuje ruch myszy, nawet gdy kursor na chwilę opuści granice elementu.
Przy większym edytorze nie trzymałbym stanu wyłącznie w kontrolkach. Lepiej przechowywać pozycję w modelu, na przykład jako X i Y, a interfejs połączyć z nim przez binding. Dzięki temu zapis diagramu, cofanie zmian i obsługa MVVM nie zamienią się w serię odczytów z właściwości wizualnych.
Canvas jako przestrzeń do rysowania i interakcji
Elementy typu Rectangle i Ellipse dziedziczą po Shape, więc mają między innymi obsługę zdarzeń, pędzle i obrysy. To wygodne rozwiązanie dla kilkudziesięciu lub kilkuset obiektów, które użytkownik może zaznaczać, przesuwać albo edytować.
Gdy obiektów jest dużo, sytuacja wygląda inaczej. Każdy element wizualny powiększa drzewo interfejsu i może uczestniczyć w pomiarze, rozmieszczaniu oraz obsłudze zdarzeń. Dla statycznych grafik lepsze mogą być DrawingVisual, Drawing albo własny element rysujący przez DrawingContext. Zwykle zużywają mniej zasobów niż duża liczba niezależnych kontrolek, ale oferują mniej gotowej interakcji.
Do sterowania kolejnością nakładania elementów służy Panel.ZIndex. Większa wartość oznacza wyższą warstwę.
Wizualny znacznik pojawi się nad obrazem, ponieważ ma wyższy indeks. Samo dodanie elementu później do kolekcji często daje podobny efekt, ale nie opierałbym logiki aplikacji na kolejności dodawania. Jawne ustawienie Panel.ZIndex jest łatwiejsze do utrzymania.
Jeśli użytkownik ma rysować odręcznie piórem lub myszą, rozważyłbym InkCanvas. Jeśli trzeba wyświetlać dużą liczbę prostych kształtów, sprawdziłbym warstwę DrawingVisual. Canvas jest dobrym gospodarzem układu, ale nie zawsze powinien być miejscem, w którym każdy piksel lub każdy punkt staje się osobnym elementem UI.
Kiedy wybrać Canvas, a kiedy inny panel
Najwięcej problemów bierze się z użycia Canvas do zadania, które wymaga automatycznego dopasowania. Poniższe zestawienie pomaga szybko wybrać właściwą warstwę interfejsu.
| Rozwiązanie | Najlepsze zastosowanie | Główne ograniczenie |
|---|---|---|
| Canvas | Diagramy, mapy, plansze i elementy o konkretnych współrzędnych | Brak automatycznego układania i słaba reakcja na zmianę rozmiaru |
| Grid | Formularze, ekrany ustawień i układy wierszowo-kolumnowe | Więcej logiki układu niż w Canvas |
| StackPanel | Listy kontrolek ułożone pionowo lub poziomo | Nie nadaje się do swobodnego rozmieszczania elementów |
| InkCanvas | Odroczne notatki, podpisy i rysowanie piórem | Nie jest zamiennikiem pełnego edytora diagramów |
| DrawingVisual | Duża liczba prostych, głównie statycznych grafik | Trudniejsza obsługa zaznaczania i interakcji |
Moja praktyczna zasada jest prosta. Jeżeli położenie elementu opisujesz zdaniem „ma być 120 jednostek od lewej i 80 od góry”, Canvas prawdopodobnie pasuje. Jeżeli opis brzmi „ma zająć dostępne miejsce i zachować margines przy zmianie szerokości okna”, zacząłbym od Grid albo innego panelu automatycznego.
Wydajność i błędy, które szybko bolą
Canvas bywa szybki, ponieważ ma mniej reguł układu niż bardziej rozbudowane panele. Nie oznacza to jednak, że jest automatycznie wydajny w każdej sytuacji. Przy każdym dziecku nadal istnieje koszt drzewa wizualnego, layoutu, renderowania i obsługi zdarzeń.
Nie twórz setek kontrolek bez potrzeby
Jeśli każdy punkt wykresu stanie się osobnym Ellipse, a każdy fragment siatki osobnym Line, aplikacja może zacząć reagować z opóźnieniem. Przy prostych, masowych grafikach lepiej narysować wiele elementów w jednym obiekcie wizualnym. Mniej elementów w drzewie UI często daje większą różnicę niż drobne optymalizacje w kodzie C#.
Nie używaj UpdateLayout jako lekarstwa
Ręczne wywoływanie UpdateLayout() po każdej zmianie pozycji zwykle pogarsza sytuację. System WPF sam wykonuje potrzebne przebiegi pomiaru i rozmieszczania, a wymuszanie ich w pętli może niepotrzebnie obciążyć interfejs.
Do animacji rozważ transformację
Jeżeli element przesuwa się płynnie przez wiele klatek, często lepiej użyć RenderTransform niż wielokrotnie zmieniać Canvas.Left i Canvas.Top. Transformacja zmienia sposób renderowania elementu, a nie jego podstawową pozycję w układzie. Przy prostych animacjach różnica może być niewielka, ale przy wielu obiektach potrafi stać się zauważalna.
Przeczytaj również: Skróty klawiaturowe w CMD - szybka obsługa konsoli
Pamiętaj o skalowaniu i przewijaniu
Duża plansza powinna zwykle znaleźć się wewnątrz ScrollViewer. Jeżeli potrzebujesz powiększania, wygodnym rozwiązaniem jest opakowanie Canvas w ScaleTransform. Trzeba wtedy przeliczać pozycję myszy z układu ekranu na układ dokumentu, bo współrzędne kursora nie będą już odpowiadały współrzędnym elementów jeden do jednego.
W projekcie z obsługą DPI nie zakładałbym, że rozmiar elementu widziany na ekranie zawsze odpowiada liczbie fizycznych pikseli. WPF pracuje w jednostkach niezależnych od urządzenia, dlatego warto testować aplikację przy różnych skalach systemowych, na przykład 100%, 125% i 150%.
Jak zaplanować Canvas, żeby nie utrudnić sobie rozwoju
Najpierw ustal, czy pozycje elementów są częścią danych. W edytorze diagramów odpowiedź brzmi zazwyczaj tak. Wtedy model powinien przechowywać identyfikator, współrzędne, rozmiar, typ elementu i ewentualne połączenia, a Canvas ma jedynie pokazywać aktualny stan.
Drugą decyzją jest sposób zaznaczania. Dla prostego prototypu wystarczy zmiana obrysu klikniętego kształtu. W większej aplikacji przydadzą się osobna warstwa zaznaczenia, uchwyty do zmiany rozmiaru oraz jawnie zarządzany z-index. Rozdzielenie tych odpowiedzialności ogranicza liczbę wyjątków w kodzie zdarzeń.
Trzecia sprawa to granice obszaru roboczego. Sam Canvas pozwala ustawić element poza widocznym obszarem, więc przy przeciąganiu trzeba samodzielnie zdecydować, czy obiekt może wyjechać poza planszę. Najczęściej ograniczam wartości X i Y do zakresu wynikającego z rozmiaru powierzchni oraz wymiarów elementu.
W aplikacjach produkcyjnych dodałbym jeszcze zapis dokumentu, cofanie operacji i testy przy zmianie rozmiaru okna. To właśnie te elementy odróżniają działający przykład od narzędzia, z którego da się wygodnie korzystać przez kilka godzin.
Canvas sprawdza się wtedy, gdy współrzędne są częścią funkcji aplikacji
Canvas jest bardzo dobrym wyborem dla interfejsów, w których użytkownik lub dane decydują o dokładnym położeniu obiektów. Pozwala szybko zbudować planszę, diagram albo warstwę znaczników, a właściwości dołączone upraszczają zarówno XAML, jak i kod C#.
Nie traktowałbym go jednak jako uniwersalnego zamiennika Grid. Gdy najważniejsze są responsywność, wyrównanie i dopasowanie do dostępnego miejsca, automatyczne panele będą bezpieczniejsze. Gdy liczy się duża liczba grafik, warto zejść poziom niżej i użyć mechanizmów DrawingVisual lub własnego renderowania.
Najrozsądniejszy start to mały prototyp z jednym Canvas, kilkoma elementami i obsługą przeciągania. Jeśli już na tym etapie model danych przechowuje pozycje, a logika interakcji nie jest przyklejona na stałe do kontrolek, dalszy rozwój edytora lub wizualizacji będzie znacznie prostszy.
