Przejdź do treści
School IT / Przestrzeń do nauki

Kształty: Rectangle, Ellipse, Line, Polygon

Do tej pory kółka i kwadraty udawałeś ramkami z zaokrąglonymi rogami. WPF ma do tego prawdziwe kształty: prostokąt, elipsę, odcinek, łamaną i wielokąt. Każdy ma wypełnienie, obrys i grubość linii, a na płótnie rysujesz nimi wykresy, plansze, ikony i postacie w grach. W tej lekcji narysujesz wykres ocen, który przebudowuje się przy każdym ruchu suwaka.

XAML C# Rectangle Ellipse Line Polygon 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Narysujesz prostokąt, elipsę, odcinek, łamaną i wielokąt w XAML.
  • Ustawisz wypełnienie, kolor i grubość obrysu oraz linię przerywaną.
  • Wyjaśnisz, czym różni się pozycja odcinka od pozycji prostokąta na płótnie.
  • Utworzysz kształty w kodzie i przeliczysz dane na współrzędne wykresu.
  • Wykorzystasz zdarzenia myszy na kształtach.
TEORIA

Fill, Stroke, StrokeThickness

Wszystkie kształty mają wspólne właściwości wyglądu:

WłaściwośćCo robi
FillWypełnienie (pędzel, jak Background). Bez niego wnętrze jest puste i nie reaguje na mysz.
StrokeKolor obrysu (linii).
StrokeThicknessGrubość obrysu, domyślnie 1.
StrokeDashArrayLinia przerywana: "4 2" — 4 jednostki kreski, 2 przerwy (w grubościach linii).
OpacityPrzezroczystość całego kształtu (lekcja 02).

Kształt bez Fill i bez Stroke jest niewidoczny. Kształty są w przestrzeni nazw System.Windows.Shapes — w XAML działają od razu, w kodzie potrzebne jest using System.Windows.Shapes;.

TEORIA

Rectangle i Ellipse

MainWindow.xaml
<Canvas Width="420" Height="160" Background="White">
    <Rectangle Canvas.Left="20" Canvas.Top="20" Width="120" Height="80"
               Fill="LightBlue" Stroke="Navy" StrokeThickness="2"/>
    <Rectangle Canvas.Left="160" Canvas.Top="20" Width="120" Height="80"
               Fill="Khaki" RadiusX="15" RadiusY="15"/>
    <Ellipse Canvas.Left="300" Canvas.Top="20" Width="100" Height="100"
             Fill="Tomato" Stroke="DarkRed" StrokeThickness="3" StrokeDashArray="3 2"/>
</Canvas>
Kształty–□✕

prostokąt z obrysem, zaokrąglony prostokąt, koło z przerywanym obrysem

Elipsa jest wpisana w prostokąt o podanych Width i Height — równe wymiary dają koło. Canvas.Left/Top wskazują lewy górny róg tego prostokąta, a nie środek koła. Żeby postawić koło o średnicy d środkiem w punkcie (x, y), ustaw Left = x − d/2, Top = y − d/2.

TEORIA

Line — odcinek

MainWindow.xaml
<Line X1="20" Y1="140" X2="400" Y2="20" Stroke="Black" StrokeThickness="2"/>

Odcinek nie potrzebuje Canvas.Left ani Width: punkty początku (X1, Y1) i końca (X2, Y2) są podane wprost we współrzędnych płótna. Fill nic nie zmienia — odcinek ma tylko Stroke. Końce linii zaokrąglisz przez StrokeStartLineCap="Round" i StrokeEndLineCap="Round".

TEORIA

Polyline i Polygon

MainWindow.xaml
<!-- łamana: wykres, ścieżka -->
<Polyline Points="20,120 80,60 140,90 200,30 260,70" Stroke="SteelBlue" StrokeThickness="3"/>

<!-- wielokąt: zamknięty, z wypełnieniem — trójkąt, dach, gwiazda, strzałka -->
<Polygon Points="300,120 350,30 400,120" Fill="IndianRed" Stroke="Black"/>
Łamana i wielokąt–□✕

Polyline nie łączy ostatniego punktu z pierwszym, Polygon — łączy

Punkty zapisujesz jako pary „x,y” oddzielone spacjami. Tak jak przy odcinku, to współrzędne płótna. Bardziej złożone kształty (łuki, krzywe) rysuje Path z poleceniami w języku ścieżek — poznasz go w bloku o wyglądzie.

TEORIA

Kształty z kodu

MainWindow.xaml.cs
using System.Windows.Shapes;

// prostokąt — pozycja przez Canvas
Rectangle r = new Rectangle { Width = 40, Height = 90, Fill = Brushes.Gold };
Canvas.SetLeft(r, 100);
Canvas.SetTop(r, 50);
Plotno.Children.Add(r);

// odcinek — pozycja w X1…Y2
Plotno.Children.Add(new Line { X1 = 0, Y1 = 140, X2 = 300, Y2 = 140, Stroke = Brushes.Gray });

// wielokąt — punkty jako kolekcja
Polygon trojkat = new Polygon { Fill = Brushes.Green };
trojkat.Points.Add(new Point(150, 10));
trojkat.Points.Add(new Point(200, 90));
trojkat.Points.Add(new Point(100, 90));
Plotno.Children.Add(trojkat);

// przerywana linia z kodu
Line srednia = new Line { X1 = 0, Y1 = 60, X2 = 300, Y2 = 60, Stroke = Brushes.Red,
                          StrokeDashArray = new DoubleCollection { 4, 3 } };

Wykres to przeliczanie danych na piksele. Jeśli największa wartość ma zająć wysokość H, to jedna jednostka danych to H / max pikseli. Pamiętaj, że oś Y rośnie w dół: słupek o wysokości h stojący na osi w y = dół ma górną krawędź w dół − h.

PRZYKŁAD

Wykres rozkładu ocen

Nauczyciel ustawia suwakami, ile było ocen 1–6 ze sprawdzianu. Wykres przebudowuje się przy każdym ruchu suwaka: osie ze strzałkami (Line + Polygon), kolorowe słupki (Rectangle) z liczbami nad nimi i czerwona przerywana linia średniej. Pole wyboru przełącza na wykres liniowy (Polyline z kropkami Ellipse).

Rozkład ocen ze sprawdzianu–✕
Liczba ocen
1:
1
2:
3
3:
7
4:
9
5:
5
6:
2
wykres liniowy
Ocen: 27
Średnia: 3,74
Niedostatecznych: 1
137952 123456 średnia 3,74

dane startowe: 27 ocen, średnia 3,74

MainWindow.xaml — pełny plik
<Window x:Class="Lekcja30.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Rozkład ocen ze sprawdzianu" SizeToContent="WidthAndHeight"
        ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">

    <StackPanel Orientation="Horizontal" Margin="15">
        <!-- dane: liczba ocen 1–6 ustawiana suwakami (tworzone w kodzie) -->
        <StackPanel Width="200" Margin="0,0,15,0">
            <TextBlock Text="Liczba ocen" FontWeight="Bold" Margin="0,0,0,6"/>
            <StackPanel x:Name="Suwaki"/>
            <CheckBox x:Name="Liniowy" Content="wykres liniowy" Margin="0,12,0,0"
                      Checked="Opcja_Zmiana" Unchecked="Opcja_Zmiana"/>
            <TextBlock x:Name="Podsumowanie" Margin="0,12,0,0" TextWrapping="Wrap"/>
        </StackPanel>

        <!-- wykres: kształty rysowane w kodzie od nowa po każdej zmianie -->
        <Canvas x:Name="Wykres" Width="440" Height="300" Background="White"/>
    </StackPanel>
</Window>
MainWindow.xaml.cs — pełny plik
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Lekcja30;

public partial class MainWindow : Window
{
    // obszar rysowania wewnątrz płótna (miejsce na opisy osi)
    private const double Lewo = 40, Dol = 260, Gora = 20, Prawo = 420;

    private readonly int[] ile = { 1, 3, 7, 9, 5, 2 };      // ile ocen 1, 2, …, 6 (dane startowe)
    private readonly Brush[] kolory =
    {
        Brushes.IndianRed, Brushes.Orange, Brushes.Gold,
        Brushes.YellowGreen, Brushes.MediumSeaGreen, Brushes.SteelBlue
    };

    public MainWindow()
    {
        InitializeComponent();

        for (int i = 0; i < 6; i++)
        {
            DockPanel wiersz = new DockPanel { Margin = new Thickness(0, 2, 0, 2) };
            TextBlock opis = new TextBlock { Text = $"{i + 1}:", Width = 20 };
            TextBlock wartosc = new TextBlock { Width = 24, TextAlignment = TextAlignment.Right };
            Slider suwak = new Slider
            {
                Minimum = 0, Maximum = 15, Value = ile[i],
                TickFrequency = 1, IsSnapToTickEnabled = true, Tag = i
            };
            suwak.ValueChanged += Suwak_ValueChanged;

            DockPanel.SetDock(opis, Dock.Left);
            DockPanel.SetDock(wartosc, Dock.Right);
            wiersz.Children.Add(opis);
            wiersz.Children.Add(wartosc);
            wiersz.Children.Add(suwak);             // ostatni wypełnia środek
            Suwaki.Children.Add(wiersz);
        }

        Rysuj();
    }

    private void Suwak_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        Slider suwak = (Slider)sender;
        ile[(int)suwak.Tag] = (int)suwak.Value;
        Rysuj();
    }

    private void Opcja_Zmiana(object sender, RoutedEventArgs e)
    {
        Rysuj();
    }

    private void Rysuj()
    {
        Wykres.Children.Clear();

        int max = 1;                                   // najwyższy słupek (co najmniej 1)
        int liczba = 0;                                // ile ocen razem
        double srednia = 0;
        for (int i = 0; i < 6; i++)
        {
            max = Math.Max(max, ile[i]);
            liczba += ile[i];
            srednia += (i + 1) * ile[i];               // na razie suma ocen
        }
        double skala = (Dol - Gora) / max;             // ile pikseli na jedną ocenę
        double szer = (Prawo - Lewo) / 6;              // szerokość miejsca na jeden słupek

        // 1. Osie ze strzałkami
        Wykres.Children.Add(new Line { X1 = Lewo, Y1 = Dol, X2 = Prawo + 10, Y2 = Dol, Stroke = Brushes.Black, StrokeThickness = 1.5 });
        Wykres.Children.Add(new Line { X1 = Lewo, Y1 = Dol, X2 = Lewo, Y2 = Gora - 10, Stroke = Brushes.Black, StrokeThickness = 1.5 });
        Wykres.Children.Add(new Polygon
        {
            Points = new PointCollection { new Point(Prawo + 16, Dol), new Point(Prawo + 6, Dol - 5), new Point(Prawo + 6, Dol + 5) },
            Fill = Brushes.Black
        });
        Wykres.Children.Add(new Polygon
        {
            Points = new PointCollection { new Point(Lewo, Gora - 16), new Point(Lewo - 5, Gora - 6), new Point(Lewo + 5, Gora - 6) },
            Fill = Brushes.Black
        });

        // 2. Słupki albo punkty linii
        Polyline linia = new Polyline { Stroke = Brushes.SteelBlue, StrokeThickness = 2 };

        for (int i = 0; i < 6; i++)
        {
            double wys = ile[i] * skala;
            double x = Lewo + i * szer;
            double y = Dol - wys;                      // oś Y rośnie w dół — odejmujemy

            if (Liniowy.IsChecked == true)
            {
                double sx = x + szer / 2;              // środek miejsca na słupek
                linia.Points.Add(new Point(sx, y));
                Ellipse punkt = new Ellipse { Width = 10, Height = 10, Fill = kolory[i], Stroke = Brushes.Black };
                Canvas.SetLeft(punkt, sx - 5);
                Canvas.SetTop(punkt, y - 5);
                Panel.SetZIndex(punkt, 1);             // punkty nad linią
                Wykres.Children.Add(punkt);
            }
            else
            {
                Rectangle slupek = new Rectangle
                {
                    Width = szer - 16,
                    Height = wys,
                    Fill = kolory[i],
                    Stroke = Brushes.Black,
                    StrokeThickness = 1,
                    RadiusX = 3,
                    RadiusY = 3
                };
                Canvas.SetLeft(slupek, x + 8);
                Canvas.SetTop(slupek, y);
                Wykres.Children.Add(slupek);
            }

            Napis($"{ile[i]}", x + szer / 2 - 6, y - 20);       // wartość nad słupkiem
            Napis($"{i + 1}", x + szer / 2 - 4, Dol + 4);       // ocena pod osią
        }

        if (Liniowy.IsChecked == true)
        {
            Wykres.Children.Add(linia);
        }

        // 3. Średnia jako przerywana linia pionowa na osi ocen
        if (liczba > 0)
        {
            srednia /= liczba;
            double xs = Lewo + (srednia - 0.5) * szer;           // pozycja średniej na osi ocen
            Wykres.Children.Add(new Line
            {
                X1 = xs, Y1 = Gora, X2 = xs, Y2 = Dol,
                Stroke = Brushes.Crimson, StrokeThickness = 2,
                StrokeDashArray = new DoubleCollection { 4, 3 }  // 4 kreska, 3 przerwa
            });
            Napis($"średnia {srednia:F2}", xs + 4, Gora);
        }

        Podsumowanie.Text = liczba == 0
            ? "Brak ocen."
            : $"Ocen: {liczba}\nŚrednia: {srednia:F2}\nNiedostatecznych: {ile[0]}";

        // wartości przy suwakach
        for (int i = 0; i < 6; i++)
        {
            DockPanel wiersz = (DockPanel)Suwaki.Children[i];
            ((TextBlock)wiersz.Children[1]).Text = $"{ile[i]}";
        }
    }

    private void Napis(string tekst, double x, double y)
    {
        TextBlock napis = new TextBlock { Text = tekst, FontSize = 12 };
        Canvas.SetLeft(napis, x);
        Canvas.SetTop(napis, y);
        Wykres.Children.Add(napis);
    }
}

Co dzieje się po kolei

  • Sześć wierszy z suwakami powstaje w pętli. Każdy wiersz to DockPanel: numer oceny przy lewej krawędzi, wartość przy prawej, suwak w środku. DockPanel.SetDock to odpowiednik atrybutu DockPanel.Dock w kodzie.
  • Numer oceny jest w Tag suwaka. Każda zmiana zapisuje wartość do tablicy ile i wywołuje Rysuj.
  • Rysuj zaczyna od Children.Clear() i rysuje wszystko od nowa. Przy kilkudziesięciu kształtach to szybkie i dużo prostsze niż zmienianie istniejących.
  • Stałe Lewo, Dol, Gora, Prawo wyznaczają obszar wykresu w płótnie, zostawiając miejsce na opisy. Skala to liczba pikseli na jedną ocenę: wysokość obszaru / największa liczba ocen.
  • Osie to dwa odcinki, a groty strzałek — trójkąty (Polygon) z punktami policzonymi od końców osi.
  • Słupek ma wysokość ile[i] × skala, a jego górna krawędź jest w Dol − wysokość, bo oś Y rośnie w dół.
  • W trybie liniowym ten sam punkt (środek słupka) trafia do Polyline, a w punkcie stawiamy kółko — przesunięte o połowę średnicy, żeby środek koła był w punkcie. ZIndex kładzie kółka nad linią.
  • Średnia to Σ(ocena × liczba) / liczba ocen. Pionowa przerywana linia stoi w miejscu średniej na osi ocen.

Sprawdź trzy rzeczy

  1. Ustaw wszystkie suwaki na 0. Program nie dzieli przez zero (max ma co najmniej 1), a zamiast średniej pokazuje „Brak ocen”.
  2. Usuń z Rysuj linię Wykres.Children.Clear(); i porusz suwakiem. Nowe kształty rysują się na starych.
  3. Zmień w słupku Canvas.SetTop(slupek, y) na Canvas.SetTop(slupek, Dol). Słupki „wiszą” pod osią — pozycja to górna krawędź, nie dolna.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementNajważniejsze właściwościUwagi
RectangleWidth, Height, RadiusX, RadiusYPozycja przez Canvas.Left/Top.
EllipseWidth, HeightWpisana w prostokąt; pozycja to jego róg.
LineX1, Y1, X2, Y2Współrzędne płótna; tylko Stroke.
PolylinePointsŁamana otwarta.
PolygonPointsŁamana zamknięta, z wypełnieniem.
Fill, Stroke, StrokeThicknesspędzle, doubleWypełnienie, obrys, grubość.
StrokeDashArray"4 2" / new DoubleCollection { 4, 2 }Linia przerywana.
new Point(x, y), new PointCollection { … }punktyPunkty kształtów w kodzie.
DockPanel.SetDock(k, Dock.Left)metodaDokowanie kontrolki tworzonej w kodzie.
CZĘSTE BŁĘDY

Zanim utkniesz

SytuacjaProblem
kształt bez Fill i StrokeNiewidoczny.
kliknięcie w środek koła bez Fill nie działaPuste wnętrze nie łapie myszy. Daj Fill="Transparent".
Canvas.Left dla LinePrzesuwa cały odcinek dodatkowo — współrzędne podaje się w X1…Y2.
koło stawiane Canvas.Left = x, gdy x to środekKoło jest przesunięte w prawo i w dół o promień.
Points="20 120, 80 60"Niepoprawny zapis — pary „x,y” oddziela spacja: "20,120 80,60".
brak using System.Windows.Shapes;CS0246 — nie znaleziono Rectangle, Line.
wykres rysowany „do góry nogami”Oś Y rośnie w dół: użyj dół − wartość × skala.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Rysuj kształtami na płótnie. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Domek ze słońcem★☆☆

Narysuj w XAML na płótnie 500 × 350: trawę (prostokąt na dole), dom (prostokąt), dach (trójkąt), drzwi z klamką (prostokąt z zaokrąglonymi rogami i małe koło), dwa okna ze szprosami (prostokąty i odcinki) oraz słońce (koło). Promienie słońca — 12 odcinków — dodaj w pętli w kodzie. Kliknięcie w słońce przełącza dzień i noc: tło zmienia się na granatowe, słońce na księżyc (jasnoszare koło), a w oknach zapala się żółte światło.

pokaż wskazówkę
  • Promień nr i ma kąt i × 30°; w radianach kat = i * Math.PI / 6. Końce: x = sx + r * Math.Cos(kat), y = sy + r * Math.Sin(kat).
ZAD 2Zegar analogowy★★☆

Tarcza zegara: koło z obrysem, 12 kresek godzinowych (grubszych) i 48 minutowych (cieńszych), cyfry 12, 3, 6, 9. Trzy wskazówki — odcinki o różnej długości, grubości i kolorze, z zaokrąglonymi końcami — pokazują bieżący czas i przesuwają się co sekundę (zegar z lekcji 27). Wskazówka godzinowa przesuwa się płynnie między godzinami, a sekundowa jest czerwona.

pokaż wskazówkę
  • Kąt mierzony od „godziny 12” zgodnie z ruchem wskazówek: x = sx + r·sin(kąt), y = sy − r·cos(kąt).
  • Godzinowa: (godzina % 12 + minuta / 60.0) × 30°. Przy każdym tyknięciu zmieniaj tylko X2, Y2 istniejących odcinków.
ZAD 3Wykres temperatur tygodnia★★☆

Siedem pól na temperatury (pon.–niedz., mogą być ujemne). Przycisk Rysuj tworzy wykres liniowy z poziomymi liniami pomocniczymi co 5 °C (szare, przerywane) i opisami osi. Oś 0 °C jest grubsza. Punkty powyżej zera są czerwone, poniżej — niebieskie. Najwyższą i najniższą temperaturę oznacz trójkątem ▲/▼ nad/pod punktem i podpisem. Zakres osi dobiera się do danych (zaokrąglony do pełnych 5 °C).

pokaż wskazówkę
  • Gdy zakres zaczyna się od wartości ujemnej, piksel dla temperatury t: y = dół − (t − min) × skala.
ZAD 4Rysowanie myszą★★☆

Prosty edytor: przyciski opcji narzędzia (prostokąt, elipsa, odcinek), lista kolorów i suwak grubości. Wciśnięcie lewego przycisku na płótnie zaczyna kształt, ruch myszy go rozciąga (widać podgląd), puszczenie kończy. Rysowanie od prawej do lewej albo od dołu do góry też działa. Ctrl + Z usuwa ostatni kształt, przycisk Wyczyść — wszystkie.

pokaż wskazówkę
  • Zapamiętaj punkt startu. Lewy górny róg prostokąta to Math.Min współrzędnych startu i kursora, a szerokość — Math.Abs ich różnicy.
  • Ostatni kształt to Children[Children.Count - 1].
ZAD 5Warcaby — plansza★★☆

Plansza 8 × 8 z kwadratów (jasne i ciemne pola) rysowana w pętli i 24 pionki (koła z obrysem: 12 białych, 12 czarnych) na ciemnych polach trzech pierwszych i trzech ostatnich rzędów. Kliknięcie pionka go zaznacza (żółty obrys), kliknięcie pustego ciemnego pola przenosi tam zaznaczony pionek. Zasad gry nie trzeba sprawdzać — tylko na zmianę ruszają białe i czarne. Napis „Ruch: białe”.

pokaż wskazówkę
  • W Tag pionka zapisz jego kolor; pole planszy wyliczysz z pozycji kliknięcia: kolumna = (int)(p.X / bok).
  • Kliknięcie pionka „przechodzi” do płótna — użyj e.Handled = true.
ZAD 6Wąż★★★

Klasyczna gra „Snake” na planszy 20 × 15 pól po 20 pikseli. Wąż to lista kwadratów; zegar co 150 ms przesuwa go o jedno pole w bieżącym kierunku (strzałki zmieniają kierunek, ale nie na przeciwny). Jabłko (czerwone koło) pojawia się w losowym wolnym polu; zjedzone wydłuża węża o jedno pole i dodaje punkt, a co 5 punktów gra przyspiesza. Uderzenie w ścianę albo we własny ogon kończy grę. Spacja — pauza, Enter — nowa gra, najlepszy wynik sesji.

pokaż wskazówkę
  • Węża trzymaj jako List<Point> w polach planszy (nie w pikselach). Ruch: wstaw nową głowę na początek listy i usuń ostatni element — chyba że wąż właśnie zjadł jabłko.
  • Zmianę kierunku zapamiętaj w KeyDown, a wykonaj w Tick — inaczej dwa szybkie naciśnięcia w jednym tyknięciu pozwolą zawrócić „w miejscu”.
PODSUMOWANIE

Co trzeba zapamiętać

  • Kształty: Rectangle, Ellipse, Line, Polyline, Polygon — przestrzeń nazw System.Windows.Shapes.
  • Wygląd: Fill, Stroke, StrokeThickness, StrokeDashArray.
  • Rectangle i Ellipse stawiasz przez Canvas.Left/Top (róg); Line i wielokąty — przez współrzędne punktów.
  • Wykres = dane × skala; oś Y rośnie w dół, więc y = dół − wartość × skala.
  • Rysowanie od nowa (Children.Clear() + pętla) jest najprostszym sposobem odświeżania rysunku.

Dokumentacja: Microsoft Learn — kształty i rysowanie w WPF.

Postęp zapisuje się w tej przeglądarce.