Canvas w WPF - pozycjonowanie, przeciąganie i wydajność

Wizualizacja w stylu wpf canvas z elementami do burzy mózgów. Deniz zaprasza Kim, Renatę i Vinko do współpracy.

Spis treści

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.

Niebieskie i pomarańczowe kwadraty tworzą diagram, obok napis WPF Diagram.

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.

FAQ - Najczęstsze pytania

Użyj właściwości dołączonych Canvas.Left i Canvas.Top, które określają odległość od lewego górnego rogu powierzchni. W C# możesz ustawić je metodami Canvas.SetLeft i Canvas.SetTop, a aktualne wartości odczytać przez Canvas.GetLeft i Canvas.GetTop.

Canvas sprawdza się w diagramach, mapach, planszach, whiteboardach i wizualizacjach, gdy położenie elementu wynika z danych lub działania użytkownika. Grid będzie lepszy w formularzach i układach, które mają dopasowywać się do dostępnego miejsca po zmianie rozmiaru okna.

Trzeba zapamiętać offset kursora względem elementu podczas MouseLeftButtonDown, pobierać pozycję myszy względem Canvas w MouseMove i ustawiać nowe wartości Canvas.Left oraz Canvas.Top. CaptureMouse pozwala odbierać ruch myszy także po opuszczeniu granic elementu.

Nie twórz osobnej kontrolki dla każdego prostego punktu lub fragmentu grafiki, ponieważ każde dziecko zwiększa koszt drzewa wizualnego, layoutu i renderowania. Dla dużej liczby prostych grafik rozważ DrawingVisual lub własne rysowanie przez DrawingContext, a przy płynnych animacjach także RenderTransform.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

wpf
xaml
canvas
inkcanvas
drawingvisual
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz