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.
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.
Fill, Stroke, StrokeThickness
Wszystkie kształty mają wspólne właściwości wyglądu:
| Właściwość | Co robi |
|---|---|
Fill | Wypełnienie (pędzel, jak Background). Bez niego wnętrze jest puste i nie reaguje na mysz. |
Stroke | Kolor obrysu (linii). |
StrokeThickness | Grubość obrysu, domyślnie 1. |
StrokeDashArray | Linia przerywana: "4 2" — 4 jednostki kreski, 2 przerwy (w grubościach linii). |
Opacity | Przezroczystość 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;.
Rectangle i Ellipse
<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>
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.
Line — odcinek
<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".
Polyline i Polygon
<!-- ł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"/>
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.
Kształty z kodu
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.
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).
Średnia: 3,74
Niedostatecznych: 1
dane startowe: 27 ocen, średnia 3,74
<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>
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.SetDockto odpowiednik atrybutuDockPanel.Dockw kodzie. - Numer oceny jest w
Tagsuwaka. Każda zmiana zapisuje wartość do tablicyilei wywołujeRysuj. Rysujzaczyna odChildren.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,Prawowyznaczają 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 wDol − 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.ZIndexkładzie kółka nad linią. - Średnia to
Σ(ocena × liczba) / liczba ocen. Pionowa przerywana linia stoi w miejscu średniej na osi ocen.
Sprawdź trzy rzeczy
- Ustaw wszystkie suwaki na 0. Program nie dzieli przez zero (
maxma co najmniej 1), a zamiast średniej pokazuje „Brak ocen”. - Usuń z
RysujlinięWykres.Children.Clear();i porusz suwakiem. Nowe kształty rysują się na starych. - Zmień w słupku
Canvas.SetTop(slupek, y)naCanvas.SetTop(slupek, Dol). Słupki „wiszą” pod osią — pozycja to górna krawędź, nie dolna.
Zestawienie elementów
| Element | Najważniejsze właściwości | Uwagi |
|---|---|---|
Rectangle | Width, Height, RadiusX, RadiusY | Pozycja przez Canvas.Left/Top. |
Ellipse | Width, Height | Wpisana w prostokąt; pozycja to jego róg. |
Line | X1, Y1, X2, Y2 | Współrzędne płótna; tylko Stroke. |
Polyline | Points | Łamana otwarta. |
Polygon | Points | Łamana zamknięta, z wypełnieniem. |
Fill, Stroke, StrokeThickness | pędzle, double | Wypełnienie, obrys, grubość. |
StrokeDashArray | "4 2" / new DoubleCollection { 4, 2 } | Linia przerywana. |
new Point(x, y), new PointCollection { … } | punkty | Punkty kształtów w kodzie. |
DockPanel.SetDock(k, Dock.Left) | metoda | Dokowanie kontrolki tworzonej w kodzie. |
Zanim utkniesz
| Sytuacja | Problem |
|---|---|
kształt bez Fill i Stroke | Niewidoczny. |
kliknięcie w środek koła bez Fill nie działa | Puste wnętrze nie łapie myszy. Daj Fill="Transparent". |
Canvas.Left dla Line | Przesuwa cały odcinek dodatkowo — współrzędne podaje się w X1…Y2. |
koło stawiane Canvas.Left = x, gdy x to środek | Koł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
Każde zadanie wykonaj w nowym projekcie Aplikacja WPF. Rysuj kształtami na płótnie. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.
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
ima kąti × 30°; w radianachkat = i * Math.PI / 6. Końce:x = sx + r * Math.Cos(kat),y = sy + r * Math.Sin(kat).
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 tylkoX2,Y2istniejących odcinków.
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.
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.Minwspółrzędnych startu i kursora, a szerokość —Math.Absich różnicy. - Ostatni kształt to
Children[Children.Count - 1].
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
Tagpionka 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.
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 wTick— inaczej dwa szybkie naciśnięcia w jednym tyknięciu pozwolą zawrócić „w miejscu”.
Co trzeba zapamiętać
- Kształty:
Rectangle,Ellipse,Line,Polyline,Polygon— przestrzeń nazwSystem.Windows.Shapes. - Wygląd:
Fill,Stroke,StrokeThickness,StrokeDashArray. RectangleiEllipsestawiasz przezCanvas.Left/Top(róg);Linei 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.