PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Wielokąt foremny i gwiazdka Kreślenie wielokąta Kreślenie gwiazdki Program w Visual C# Parametry domyślne Program w Visual C# (flaga USA) Poprzedni przykład Następny przykład Program w C++ Kontakt

Wielokąt foremny i gwiazdka

Zadanie rysowania wielokąta foremnego lub gwiazdki równora­miennej stanowi szcze­gólny przypadek zaga­dnienia ryso­wania linii łamanej występu­jącego w grafice kompute­rowej. Na przykład ryso­wanie krzywej, nawet bardzo złożonej, sprowadza się do naryso­wania łamanej o odpo­wiednio dużej liczbie wierz­chołków leżących na tej krzywej. Technika rysowania łamanej o wierz­chołkach P0,P1,...,Pn polega na rysowaniu kolejnych odcinków PkPk+1 dla k=0,1,...,n-1. W przy­padku łamanej zamkniętej wierz­chołek początkowy jest również końcowym: P0=Pn.

W środowisku Visual C# podstawowymi narzędziami do ryso­wania linii łamanych i wielokątów są następu­jące metody klasy Graphics:

Używanie ich wiąże się z wyborem obiektów klasy:

Kreślenie wielokąta

Wierzchołki wielokąta foremnego są równomiernie rozło­żone na okręgu (rys.). Jeżeli R jest promie­niem tego okręgu, pq są współrzę­dnymi jego środka S oraz promień łączący punkt SP0 jest równo­legły do osi x, to współ­rzędne xkyk wierz­chołków Pk wielo­kąta można dla k=0,1,...,n wyzna­czyć następująco:

gdzie

Jedna z przeciążonych wersji metody DrawLine klasy Graphics wymaga podania, oprócz pióra, czterech argu­mentów typu int określa­jących współ­rzędne punktu początko­wego i końco­wego rysowa­nego odcinka. Zatem algo­rytm kreślenia wielo­kąta można z użyciem tej metody zaprogra­mować następująco:

void Wielokat(Graphics g, int n, int p, int q, double R, Pen pen)
{
    double alpha = 2 * Math.PI / n, fi;
    int x1 = (int)R + p, y1 = q, x2, y2;
    for (int k = 1; k <= n; k++)
    {
        fi = k * alpha;
        x2 = (int)(R * Math.Cos(fi)) + p;
        y2 = q - (int)(R * Math.Sin(fi));
        g.DrawLine(pen, x1, y1, x2, y2);
        x1 = x2;
        y1 = y2;
    }
}

Wartościami zmiennych x1 i y1 oraz x2 i y2 są współ­rzędne dwóch sąsie­dnich wierz­chołków wielokąta rozpatry­wanych w kolejnym kroku itera­cyjnym. Zmiana znaku (minus zamiast plus) w wyra­żeniu przypisy­wanemu zmiennej y2 wynika z odwro­tnego skiero­wania osi y układu współrzę­dnych powierz­chni rysowania.

Najczęściej stosowanym sposobem uzyskania obiektu Graphics jest metoda obsługi zdarzenia Paint okna. Poniższy listing zawiera przykła­dowy kod takiej metody rysującej kontury trzech pięciokątów foremnych. Pierwszym parametrem metody jest obiekt sender klasy object, dla którego zaszło zdarzenie, drugim jest obiekt e klasy PaintEventArgs, którego jedną ze składowych jest obiekt Graphics.

void Form1_Paint(object sender, PaintEventArgs e)
{
    Wielokat(e.Graphics, 5, 360, 140, 120, Pens.Blue);
    Pen pen = new Pen(Color.Magenta, 10);
    Wielokat(e.Graphics, 5, 130, 140, 120, pen);
    pen.StartCap = pen.EndCap = System.Drawing.Drawing2D.LineCap.Round;
    Wielokat(e.Graphics, 5, 590, 140, 120, pen);
    pen.Dispose();
}

Metoda Form1_Paint rysuje środkowy kontur dostępnym w systemie niebie­skim piórem o szero­kości 1 piksela, zaś lewy i prawy definio­wanym fiole­towym piórem o szero­kości 10 pikseli (rys.). Przypi­sując właści­wościom StartCupEndCap obiektu pen wartości typu wylicze­niowego LineCap, można dla punktów początko­wych i końco­wych odcinków stosować efekt końcówek, które popra­wiają lub wygła­dzają miejsca łączenia się odcinków ze sobą. Lewy pięciokąt jest rysowany piórem o domyślnych końcówkach Flat (płaskie), prawy piórem o końcówkach Round (okrągłe). Pędzle są tak bitmapy zasobami niezarzą­dzanymi, toteż aby uniknąć wycieku pamięci, utworzony w metodzie Form1_Paint obiekt pen jest po wykorzy­staniu w niej niszczony za pomocą metody Destroy.

Dużą niedogodnością sprecyzowanej powyżej metody Wielokat jest brak możli­wości wypeł­nienia wnętrza wielo­kąta. Kłopotu można uniknąć, stosując metody DrawPolygonFillPolygon zamiast DrawLine. Obie wymagają przeka­zania współ­rzędnych wierz­chołków wielo­kąta w tablicy punktów (punkt początkowy nie musi się pokrywać z końcowym). Oto przykład ich użycia w nowej wersji metody kreślenia wielo­kąta foremnego:

void Wielokat(Graphics g, int n, int p, int q, double R, Pen pen, Brush brush)
{
    double alpha = 2 * Math.PI / n, fi;
    Point[] P = new Point[n];
    for (int k = 0; k < n; k++)
    {
        fi = k * alpha;
        P[k].X = (int)(R * Math.Cos(fi)) + p;
        P[k].Y = q - (int)(R * Math.Sin(fi));
    }
    if (brush != null)
        g.FillPolygon(brush, P);
    g.DrawPolygon(pen, P);
}

Tym razem najpierw są obliczane współrzędne wszystkich wierz­chołków wielo­kąta (oprócz osta­tniego, który pokrywa się z począ­tkowym). Wyzna­czane wartości są zapamię­tywane w polach X i Y struktur Point (punkt), z których składa się n elemen­towa tablica P. Dopiero na koniec tablica ta zostaje przeka­zana w drugim argu­mencie metodom FillPolygonDrawPolygon. Warto zwrócić uwagę na kolejność ich wywoły­wania: wypeł­nianie wielo­kąta poprzedza ryso­wanie jego konturu (podobnie jak w przy­padku malowania pokoju, najpierw maluje się ściany, a potem pasek podsufi­towy). Wybieg ten ma na celu uniemożli­wienie częścio­wego zamalowy­wania konturu. Poniższa metoda obsługi zdarzenia Paint okna ilustruje wykorzy­stanie nowej metody Wielokat.

void Form1_Paint(object sender, PaintEventArgs e)
{
    Wielokat(e.Graphics, 5, 360, 140, 120, Pens.Blue, Brushes.LightPink);
    Pen pen = new Pen(Color.Magenta, 10);
    Wielokat(e.Graphics, 5, 130, 140, 120, pen, Brushes.Cyan);
    pen.LineJoin = System.Drawing.Drawing2D.LineJoin.Round;
    Wielokat(e.Graphics, 5, 590, 140, 120, pen, null);
    pen.Dispose();
}

Podobnie jak poprzednio, metoda Form1_Paint rysuje trzy pięciokąty, ale dodatkowo dwa z nich wypełnia pędzlami wybranymi z szero­kiego zestawu pędzli dostępnych w systemie (rys.). Niewypeł­niony pięciokąt (argument brush równy null) jest rysowany piórem, którego właści­wość LineJoin (łączenie linii) ma wartość Round (okrągłe). Domyślną wartością LineJoin jest Miter (ostre lub obcięte – w zależ­ności od kąta zała­mania).

Kreślenie gwiazdki

Nietrudno jest przejść od rysowania wielokąta foremnego do gwiazdki równora­miennej, która ma 2n wierz­chołków leżących naprze­miennie na dwóch koncentry­cznych okręgach o środku S i promie­niach R, r (rys.). Zakładając ponownie, że promień R łączący środek S z punktem P0 jest równo­legły do osi x, można wyznaczyć współ­rzędne xkyk tych wierz­chołków dla k=0,1,...,2n z następu­jących równości:

w których

Bazując na nich, można algorytm rysowania gwiazdki równora­miennej zaprogra­mować z użyciem metody DrawLine następująco:

void Gwiazdka(Graphics g, int n, int p, int q, double R, double r, Pen pen)
{
    double alpha2 = Math.PI / n, fi, ro;
    int x1 = (int)R + p, y1 = q, x2, y2;
    for (int k = 1; k <= 2*n; k++)
    {
        fi = k * alpha2;
        ro = (k % 2 == 0) ? R : r;
        x2 = (int)(ro * Math.Cos(fi)) + p;
        y2 = q - (int)(ro * Math.Sin(fi));
        g.DrawLine(pen, x1, y1, x2, y2);
        x1 = x2;
        y1 = y2;
    }
}

Tak samo jak w przypadku wielokątów, znamienną wadą tej metody jest brak możli­wości wypeł­niania wnętrza gwiazdki. Po raz drugi kluczem do skonstruo­wania lepszej metody jest przechowy­wanie współ­rzędnych wierz­chołków gwiazdki w tablicy elementów typu Point i użycie metod DrawPolygonFillPolygon klasy Graphics zamiast DrawLine. Nowa wersja metody kreślenia gwiazdki może wyglądać następująco:

void Gwiazdka(Graphics g, int n, int p, int q, double R, double r, Pen pen, Brush brush)
{
    double alpha2 = Math.PI / n, fi, ro;
    Point[] P = new Point[2 * n];
    for (int k = 0; k < 2 * n; k++)
    {
        fi = k * alpha2;
        ro = (k % 2 == 0) ? R : r;
        P[k].X = (int)(ro * Math.Cos(fi)) + p;
        P[k].Y = q - (int)(ro * Math.Sin(fi));
    }
    if (brush != null)
        g.FillPolygon(brush, P);
    g.DrawPolygon(pen, P);
}

Program w C#

Zależnie od ustawień dokonywanych na bieżąco przez użytko­wnika program ma rysować wielokąt foremny lub gwiazdkę równora­mienną o określonej liczbie boków lub ramion wypełnioną lub nie. Komuni­kacja programu z użytko­wnikiem ma się odbywać za pośredni­ctwem inter­fejsu grafi­cznego (GUI).

Rozpoczynamy od utworzenia nowego projektu apli­kacji okien­kowej, np. o nazwie Gwiazdka. Następnie po zmianie właści­wości BackColor formu­larza na Window (białe tło w standar­dowym schemacie kolorów Windows) wybieramy w grupie konte­nery przybor­nika kontrolkę Panel (panel) i wsta­wiamy ją do formu­larza. Jej właściwość BackColor ustawiamy na Control (jasno­szara kontrolka), zaś Dock (dokowanie) na Bottom (dół, zob. rys. poniżej), po czym zwężamy ją, by przybrała kształt paska o wyso­kości około 40 pikseli. Kontrolka ta posłuży jako pojemnik na inne kontrolki używane do stero­wania programem.

Budujemy zatem prosty interfejs graficzny, wstawiając do utworzo­nego na dole okna panela kilka typowych kontrolek z przybor­nika. Zaczynamy od umieszczenia od jego lewej strony dwóch przyci­sków radiowych RadioButton, których właści­wości Text zamie­niamy na &Wielokąt&Gwiazdka. Znak amper­sand (&) oznacza, że występu­jąca za nim litera zostanie podkre­ślona, co w trakcie wykony­wania aplikacji umożliwi używanie skrótów klawiatu­rowych. I tak, gdy użytko­wnik naciśnie Alt+W, naryso­wany zostanie wielokąt, a gdy Alt+G, gwiazdka. Oba przyciski współpra­cują ze sobą, gdyż należą do tego samego konte­nera – włączenie pierwszego powoduje wyłą­czenie drugiego, i odwrotnie.

Kontynuując wizualne projektowanie aplikacji, wstawiamy do panela etykietę Label, której właści­wość Text zmie­niamy na n:, oraz pole edycji NumericUpDown, które jest powiązane z dwoma przyci­skami strzałek (góra i dół) ułatwia­jącymi edycję liczby. Pole to posłuży do określania liczby boków wielokąta lub ramion gwiazdki. Jego wartość bieżącą określa właści­wość Value, którą ustawiamy na 5. Przyjmujemy tym samym, że na początku wyświe­tlany będzie pięciokąt lub gwiazdka pięciora­mienna. Następnie na panelu umieszczamy pole wyboru CheckBox pozwala­jące na ustalenie, czy figura ta ma być wypeł­niana, czy nie. Jego właści­wości Text przypi­sujemy wartość Wypełnienie. Na koniec po prawej stronie panela wstawiamy przycisk Button, który ma zamykać okno i kończyć wykonanie programu. Jego właści­wość Text ustawiamy na &Zamknij, a Anchor na Top, Right (zob. rys. poniżej), która oznacza, że ma on zacho­wywać stałą odle­głość od górnego i prawego brzegu panela, gdy ten będzie zmieniał swoje rozmiary.

Zaprojektowany formularz jest przedsta­wiony na poniż­szym rysunku. Pomimo że do tej pory nie napisa­liśmy ani jednej linijki kodu, plik Form1.Designer.cs został rozbu­dowany o kod źródłowy odpowie­dzialny za utwo­rzenie umieszczo­nych na formu­larzu kontrolek. Dla każdego wstawia­nego w ten sposób kompo­nentu środowisko Visual C# definiuje zmienną, której przypisuje refe­rencję pozwala­jącą na odwoły­wanie się do utworzo­nego obiektu danej klasy. Mechanizm nazy­wania tworzo­nych przez środo­wisko elementów programu polega na doda­waniu kolejnego numeru do nazwy wywo­dzącej się od nazwy klasy. W rozpatry­wanym przypadku nazwami kontrolek są: panel1, radioButton1, radioButton2, label1, numericUpDown1, checkBox1button1. Należy pamiętać, że wygene­rowaną nazwę kompo­nentu powinno się zmieniać tylko poprzez ustawienie właści­wości Name. Progra­miści często kierują się wygodni­ctwem i pozostają przy nazwach domyśl­nych. Czy jest to tylko wygodni­ctwo? Może jednak nazwy domyślne są sugestywne?

Możemy wreszcie przejść do zaprogramowania funkcjonal­ności zaprojekto­wanego inter­fejsu grafi­cznego. Zaczniemy od zdefinio­wania metody obsługi zdarzenia CheckedChanged kontrolki radioButton1, które nastąpi, gdy użytko­wnik przestawi stan kontrolki, dokonując wyboru pomiędzy wielo­kątem a gwiazdką. Pusty szkielet metody genero­wany przez środo­wisko Visual C# możemy uzyskać na kilka sposobów:

Stan kontrolki określa właściwość Checked (true – kontrolka wybrana, false – nie). W przy­padku wybrania wielokąta należy zmienić tytuł okna na Wielokąt i skorygować wartość właści­wości Minimum kontrolki numericUpDown1 na 3, co oznacza, że użytkownik będzie mógł zmieniać liczbę boków wielokąta w zakresie od 3 do 100 (wartość domyślna właści­wości Maximum). Na koniec trzeba wymusić przemalo­wanie obszaru robo­czego okna, wywołując metodę Invalidate:

private void radioButton1_CheckedChanged(object sender, EventArgs e)
{
    if (radioButton1.Checked)
    {
        Text = "Wielokąt";
        numericUpDown1.Minimum = 3;
        Invalidate();
    }
}

Metoda obsługi zdarzenia CheckedChanged kontrolki radioButton2 precyzująca, jakie operacje należy wykonać w przy­padku wybrania gwiazdki, jest bardzo podobna. Tytułem okna będzie wtedy napis Gwiazdka, a mini­malną liczbą ramion gwiazdki 2:

private void radioButton2_CheckedChanged(object sender, EventArgs e)
{
    if (radioButton2.Checked)
    {
        Text = "Gwiazdka";
        numericUpDown1.Minimum = 2;
        Invalidate();
    }
}

Kolejnym zdarzeniem wymagającym zaprogramo­wania jest ValueChanged kontrolki numericUpDown1, które zachodzi, gdy użytko­wnik zmieni liczbę boków wielo­kąta lub ramion gwiazdki. Wówczas należy jedynie wymusić przemalo­wanie obszaru robo­czego okna:

private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
    Invalidate();
}

Nie możemy jeszcze przeoczyć dwóch zdarzeń wymaga­jących podobnej obsługi: CheckedChanged kontrolki checkBox1Resize formularza Form1. Pierwsze zachodzi, gdy użytko­wnik zmieni opcję dotyczącą wypeł­niania rysowanej figury, drugie, gdy zmieni rozmiar okna. W obu przypad­kach należy tylko przema­lować obszar roboczy okna, toteż jednemu i drugiemu zdarzeniu możemy przypisać tę samą, znaną już metodę numericUpDown1­_ValueChanged, wybie­rając ją z listy metod obsługi zdarzeń w oknie Właściwości kontrolki (rys.) i formularza.

Metoda obsługi zdarzenia Click przycisku button1, które zostaje wyzwolone, gdy użytko­wnik kliknie na przycisku myszką, wybierze go przy użyciu klawia­tury i naciśnie Enter bądź użyje skrótu klawiszo­wego Alt+Z, ma zamknąć okno i zakoń­czyć wykonanie apli­kacji. Jej pusty szkielet wygene­rowany przez środo­wisko Visual C# należy jedynie uzupełnić wywoła­niem metody Close formularza:

private void button1_Click(object sender, EventArgs e)
{
    Close();
}

Przejdźmy w końcu do rysowania wielokąta foremnego lub gwiazdki równora­miennej. Rozpoczy­namy od umieszczenia w klasie Form1 dwóch omówionych metod WielokatGwiazdka, poprze­dzając ich nagłówki modyfika­torem private. Zależnie od wartości właści­wości Checked (true lub false) kontrolki radioButton1 pierwszą lub drugą z tych metod wywołu­jemy w meto­dzie obsługi zdarzenia Paint formu­larza:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    int p = ClientSize.Width / 2;
    int q = (ClientSize.Height - panel1.Height) / 2;
    double R = 0.9 * Math.Min(p, q);
    int n = (int)numericUpDown1.Value;
    Brush brush = checkBox1.Checked ? Brushes.Pink : null;
    if (radioButton1.Checked)
        Wielokat(e.Graphics, n, p, q, R, Pens.Blue, brush);
    else
        Gwiazdka(e.Graphics, n, p, q, R, 0.4 * R, Pens.Blue, brush);
}

Najpierw jednak wyznaczamy wartości zmiennych p, qR określa­jące współ­rzędne środka i promień rysowanej figury tak, by pasowała do rozmiaru obszaru robo­czego okna. Warto zauważyć, że w oblicze­niach wartości q uwzglę­dniona jest wysokość panela umieszczo­nego na dole tego obszaru, a przy wyzna­czaniu liczby boków wielokąta lub ramion gwiazdki wymagane jest rzutowanie wartości typu decimal właści­wości Value kontrolki numericUpDown1 na wartość typu int. W rezul­tacie otrzymu­jemy następu­jący plik źródłowy Form1.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Gwiazdka
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Wielokat(Graphics g, int n, int p, int q, double R, Pen pen, Brush brush)
        {
            double alpha = 2 * Math.PI / n, fi;
            Point[] P = new Point[n];
            for (int k = 0; k < n; k++)
            {
                fi = k * alpha;
                P[k].X = (int)(R * Math.Cos(fi)) + p;
                P[k].Y = q - (int)(R * Math.Sin(fi));
            }
            if (brush != null)
                g.FillPolygon(brush, P);
            g.DrawPolygon(pen, P);
        }

        private void Gwiazdka(Graphics g, int n, int p, int q, double R, double r, Pen pen,
                              Brush brush)
        {
            double alpha2 = Math.PI / n, fi, ro;
            Point[] P = new Point[2 * n];
            for (int k = 0; k < 2 * n; k++)
            {
                fi = k * alpha2;
                ro = (k % 2 == 0) ? R : r;
                P[k].X = (int)(ro * Math.Cos(fi)) + p;
                P[k].Y = q - (int)(ro * Math.Sin(fi));
            }
            if (brush != null)
                g.FillPolygon(brush, P);
            g.DrawPolygon(pen, P);
        }

        private void radioButton1_CheckedChanged(object sender, EventArgs e)
        {
            if (radioButton1.Checked)
            {
                Text = "Wielokąt";
                numericUpDown1.Minimum = 3;
                Invalidate();
            }
        }

        private void radioButton2_CheckedChanged(object sender, EventArgs e)
        {
            if (radioButton2.Checked)
            {
                Text = "Gwiazdka";
                numericUpDown1.Minimum = 2;
                Invalidate();
            }
        }

        private void numericUpDown1_ValueChanged(object sender, EventArgs e)
        {
            Invalidate();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            int p = ClientSize.Width / 2;
            int q = (ClientSize.Height - panel1.Height) / 2;
            double R = 0.9 * Math.Min(p, q);
            int n = (int)numericUpDown1.Value;
            Brush brush = checkBox1.Checked ? Brushes.Pink : null;
            if (radioButton1.Checked)
                Wielokat(e.Graphics, n, p, q, R, Pens.Blue, brush);
            else
                Gwiazdka(e.Graphics, n, p, q, R, 0.4 * R, Pens.Blue, brush);
        }
    }
}

A oto przykładowy wynik wykonania programu:

Parametry domyślne

W języku C# można (od wersji 4.0, 2011 r.) definiować metody o domyślnych parame­trach (argu­mentach). Wartości takich parame­trów są określane bezpo­średnio w nagłówku metody. Parametry domyślne mogą być umieszczane tylko z prawej strony listy parametrów (jeśli któryś z para­metrów ma wartość domyślną, to wszystkie występu­jące po nim też muszą mieć wartości domyślne). W wywo­łaniu metody można nie podawać argumentów odpowiada­jących parame­trom domyślnym, wówczas przyjęte zostają dla nich wartości domyślne.

Zaprezentowane w punkcie Kreślenie gwiazdki niniejszej strony dwie metody rysowania gwiazdki równora­miennej nie są uniwer­salne, gdyż nie przewi­dują obrotu (promień SP0 jest zawsze równo­legły do osi x). Warto zatem je zmodyfi­kować, wprowa­dzając dodatkowy parametr określa­jący kąt obrotu wierz­chołków gwiazdki względem jej środka (rys.).

W udoskonalonej wersji metody Gwiazdka uwzględnia­jącej wypeł­nienie konturu (listing poniżej) dodatkowy parametr omega określa kąt obrotu gwiazdki (wyrażony w stopniach ze względu na wygodę korzy­stania). Ma on wartość domyśl­ną 0, co pozwala na używanie metody w dotych­czasowy sposób – bez poda­wania kąta obrotu, gdy ten nie występuje. Dodatkowo zmieniono charakter parametru brush, nadając mu wartość domyślną null. Uwzglę­dnienie kąta obrotu wymagało korekty obliczeń współ­rzędnych wierz­chołków gwiazdki obejmu­jącej przeli­czenie miary kąta ze stopni na radiany i modyfi­kację wyrażenia przypisy­wanego zmiennej fi wewnątrz pętli for.

void Gwiazdka(Graphics g, int n, int p, int q, double R, double r, Pen pen,
              Brush brush = null, int omega = 0)
{
    double alpha2 = Math.PI / n, omegar = omega * Math.PI / 180, fi, ro;
    Point[] P = new Point[2 * n];
    for (int k = 0; k < 2 * n; k++)
    {
        fi = k * alpha2 + omegar;
        ro = (k % 2 == 0) ? R : r;
        P[k].X = (int)(ro * Math.Cos(fi)) + p;
        P[k].Y = q - (int)(ro * Math.Sin(fi));
    }
    if (brush != null)
        g.FillPolygon(brush, P);
    g.DrawPolygon(pen, P);
}

Jak pokazuje poniższa metoda obsługi zdarzenia Paint formu­larza, nową metodę kreślenia gwiazdki można wywołać z siedmioma, ośmioma lub dziewię­cioma argumentami.

void Form1_Paint(object sender, PaintEventArgs e)
{
    Gwiazdka(e.Graphics, 5, 350, 140, 120, 50, Pens.Blue);
    Pen pen = new Pen(Color.OrangeRed, 5);
    Gwiazdka(e.Graphics, 5, 125, 140, 120, 50, pen, Brushes.Cyan);
    Gwiazdka(e.Graphics, 5, 590, 150, 120, 50, pen, Brushes.Moccasin, 90);
    pen.Color = Color.Olive;
    Gwiazdka(e.Graphics, 5, 240, 335, 120, 50, pen, null, 180);
    Gwiazdka(e.Graphics, 5, 515, 330, 120, 50, pen, omega:-90);
    pen.Dispose();
}

Metoda rysuje pięć gwiazdek pięcioramiennych (rys.). Środkowa gwiazdka w pierw­szym rzędzie jest rysowana bez wypeł­nienia i obrotu, dwie pozostałe są wypeł­nione, przy czym lewa nie jest obrócona, zaś prawa obrócona o 90o przeciwnie do ruchu wskazówek zegara (kąt dodatni). Dwie niewypeł­nione gwiazdki w drugim rzędzie są obrócone, pierwsza o 180o, druga o –90o, tj. o 90o zgodnie z ruchem wskazówek zegara (kąt ujemny). Warto zauważyć, że pomi­nięcie wartości domyślnej null w ostatnim wywo­łaniu metody Gwiazdka wymagało sprecyzo­wania, że ósmy argument nie dotyczy pędzla, lecz kąta obrotu.

Program w C# (flaga USA)

Flaga Stanów Zjednoczonych Ameryki składa się z trzy­nastu równej wysokości poziomych pasów na przemian czerwo­nych i białych (czerwony na początku i na końcu) symboli­zujących 13 pierwo­tnych kolonii. W lewym górnym rogu flagi znajduje się niebieski kanton z pięćdzie­sięcioma pięciora­miennymi białymi gwiazdami symboli­zującymi 50 stanów. Są one rozmie­szczone w dzie­więciu poziomych rzędach po 6 (na górze i dole) i 5 gwiazd przemiennie.

Flaga i jej elementy mają w prawie USA ściśle określone proporcje. Zachowując je, można rozmiar flagi i jej poszcze­gólnych elementów wyrazić w pikselach za pomocą szero­kości pasa następująco:

  P               – szerokość pasa
  A = 13 * P      – szerokość flagi
  B = 1.9 * A     – długość flagi
  C = 7 * P       – szerokość kantonu
  D = 0.76 * A    – długość kantonu
  G = 0.0308 * A  – promień gwiazdy

Jest oczywiste, że wartość P powinna być tak dobrana, aby prostokąt BxA flagi mieścił się z niezbyt dużym margi­nesem w obszarze roboczym okna. Rozsądnym wydaje się wyzna­czenie jej w nastę­pujący sposób:

P = (int)Math.Min(ClientSize.Height, ClientSize.Width / 1.9) / 14;

Użycie liczby 14 zamiast 13 zapobiega zbyt małym margi­nesom lub ich zniknięciu. Obliczenia wszystkich parametrów flagi wygodnie jest umieścić w metodzie Form1_Load obsługi zdarzenia Load formu­larza, które jest wyzwalane, gdy okno zostało załadowane do pamięci i ma być po raz pierwszy wyświe­tlone na ekranie.

W metodzie Form1_Paint obsługi zdarzenia Paint formu­larza pośrodku obszaru roboczego okna rysowana jest flaga. Do zapeł­nienia jej trzyna­stoma pasami o rozmiarze BxP, naprze­miennie czerwonym i białym, a także utwo­rzenia w jej lewym górnym rogu niebieskiego kantonu o rozmiarze DxC, używana jest metoda FillRectangle obiektu Graphics, która wypełnia określonym pędzlem prostokąt o zadanym lewym górnym rogu i obu bokach. Na koniec rysowanych jest 50 białych gwiazdek obróco­nych o 90o. Zaprogra­mowanie tej operacji ułatwia siatka dzieląca kanton na 12x10 jedna­kowych prosto­kątów (rys.).

Środki gwiazdek znajdują się w węzłach siatki. I tak, dla grupy 30 gwiazdek rozlokowanych w 6 rzędach po 5 w każdym (rys. powyżej z lewej) współ­rzędne ich środków wyrażają się wzorami:

zaś dla grupy 20 gwiazdek ustawionych w 5 rzędach po 4 w każdym (rys. powyżej z prawej) wzorami:

Na koniec należy jeszcze zdefiniować metodę Form1_Resize obsługi zdarzenia Resize formu­larza, które zachodzi przy każdej zmianie rozmiaru okna. Jej działanie sprowadza się do ponownego obli­czenia para­metrów flagi i wymu­szenia przema­lowania obszaru robo­czego okna, czyli wywołania metod Form1_LoadInvalidate. Rozwa­żania te prowadzą do przedsta­wionego na poni­ższym listingu kodu źródło­wego programu w C#.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace FlagaUSA
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Gwiazdka(Graphics g, int n, int p, int q, double R, double r,
                              Pen pen, Brush brush = null, int omega = 0)
        {
            double alpha2 = Math.PI / n, omegar = omega * Math.PI / 180, fi, ro;
            Point[] P = new Point[2 * n];
            for (int k = 0; k < 2 * n; k++)
            {
                fi = k * alpha2 + omegar;
                ro = (k % 2 == 0) ? R : r;
                P[k].X = (int)(ro * Math.Cos(fi)) + p;
                P[k].Y = q - (int)(ro * Math.Sin(fi));
            }
            if (brush != null)
                g.FillPolygon(brush, P);
            g.DrawPolygon(pen, P);
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            P = (int)Math.Min(ClientSize.Height, ClientSize.Width / 1.9) / 14;
            A = 13 * P;
            B = (int)(1.9 * A);
            C = 7 * P;
            D = (int)(0.76 * A);
            G = (int)(0.0308 * A);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            int dx = (ClientSize.Width - B) / 2;
            int dy = (ClientSize.Height - A) / 2;
            for (int i = 0; i < 13; i++)
                e.Graphics.FillRectangle((i % 2 == 0) ? Brushes.Red : Brushes.White,
                                         dx, i * P + dy, B, P);
            e.Graphics.FillRectangle(Brushes.Blue, dx, dy, D, C);
            e.Graphics.DrawRectangle(Pens.Gray, dx, dy, B, A);
            for (int j = 1; j <= 5; j++)
            {
                int y = (2 * j - 1) * C / 10 + dy;
                for (int i = 1; i <= 6; i++)
                    Gwiazdka(e.Graphics, 5, (2 * i - 1) * D / 12 + dx, y, G, 0.4 * G,
                             Pens.White, Brushes.White, 90);
            }
            for (int j = 1; j <= 4; j++)
            {
                int y = j * C / 5 + dy;
                for (int i = 1; i <= 5; i++)
                    Gwiazdka(e.Graphics, 5, i * D / 6 + dx, y, G, 0.4 * G,
                             Pens.White, Brushes.White, 90);
            }
        }

        private void Form1_Resize(object sender, EventArgs e)
        {
            Form1_Load(sender, e);
            Invalidate();
        }

        private int P;      // Szerokość pasa
        private int A;      // Szerokość flagi
        private int B;      // Długość flagi
        private int C;      // Szerokość kantonu
        private int D;      // Długość kantonu
        private int G;      // Promień gwiazdy
    }
}

Poniższy rysunek prezentuje wynik wykonania programu.


Opracowanie przykładu: styczeń 2019