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

Przykład C#

Krzywe Sierpińskiego Kontrolka użytkownika przewijania obrazka Rekurencyjny algorytm kreślenia krzywej Program w C# Algorytm (wersja 2) Program w C# (wersja 2) Poprzedni przykład Następny przykład Program w C++ Kontakt

Krzywe Sierpińskiego

Przykładami atrakcyjnie wyglądających układów grafi­cznych, które dają się łatwo wygene­rować za pomocą programu kompute­rowego, są krzywe Sierpiń­skiego. Przy ich rysowaniu nie korzysta się z równań matematy­cznych, lecz ze ściśle określo­nego schematu rekuren­cyjnego sterują­cego narzę­dziem kreślącym kompu­tera. Poniższy rysunek przedstawia krzywe Sierpiń­skiego rzędu 1 (z lewej) i 2 (z prawej). Już na pierwszy rzut oka widać, że krzywą rzędu 2 złożono z czterech krzywych rzędu 1 dwukro­tnie zmniej­szonych, w których najpierw usunięto po jednym narożnym odcinku, a nastę­pnie połą­czono je czterema odcinkami na przemian poziomymi i pionowymi. Odkrycie schematu rekurencji krzywych stanowi klucz do napisania programu ich kreślenia.

Zazwyczaj algorytmy i podprogramy (metody w C#) rekuren­cyjne są bardziej przej­rzyste i krótsze niż ich odpowie­dniki itera­cyjne, jednak realizo­wane na komputerze mają większą złożoność pamięciową i czasową. Przykładem rozwią­zania iteracyj­nego i rekuren­cyjnego jest obliczanie współczyn­ników Newtona. Nawet w przypadku tak prostych dwóch metod wersja rekuren­cyjna jest czytel­niejsza. Jednak wraz ze wzrostem wartości argumentów liczba uakty­wnień rekuren­cyjnych rośnie wykła­dniczo, wiodąc do wielokro­tnego obliczania tych samych wartości, przez co metoda ta staje się niepra­ktyczna. Chociaż każdy podprogram rekuren­cyjny można przekształcić na czysto itera­cyjny, to jednak w wielu sytua­cjach rekurencja jest w pełni uzasa­dniona. Zaprezen­towane poniżej programy zawierają metody sformuło­wane jako rekuren­cyjne, bo takie są naturalne schematy kreślenia krzywych Sierpiń­skiego. Przekształ­cenie ich na postać itera­cyjną byłoby dosyć kłopotliwe.

Kontrolka użytkownika przewijania obrazka

Rysowanie krzywych Sierpińskiego będzie oczywiście odbywać się za pomocą narzędzi klasy Graphics. Najczęściej stoso­wanym sposobem uzyskania obiektu tej klasy jest obsługa zdarzenia Paint formu­larza lub kontrolki, na powierz­chni której ma być tworzony rysunek. Naturalnym oczeki­waniem jest, by pojawiły się paski przewi­jania, gdy jest on zbyt duży, by zmieścił się w oknie. Najpro­stszy sposób zaprogra­mowania tej funkcjonal­ności polega na użyciu kontrolki wywodzącej się od standar­dowej klasy Scrollable­Control, jak np. FormPanel. Kontrolka takiej klasy może służyć jako pojemnik dla innych kontrolek. Ustawienie jej właści­wości AutoScroll na True powoduje wyświe­tlenie pasków przewi­jania i ich automa­tyczną obsługę, gdy zawartość kontrolki przekracza jej rozmiar.

Poniżej przedstawiono dwie testowe aplikacje rysujące wypeł­nioną elipsę na kontrolce PictureBox, której rozmiar można zmieniać w pewnym zakresie. Do formu­larza pierwszej aplikacji wstawiono menu, a bezpo­średnio pod nim przy lewym brzegu obszaru robo­czego kontrolkę PictureBox. Ustawiono również właści­wość AutoScroll formu­larza na True. Gdy w trakcie wykonania aplikacji użytko­wnik powiększy rysunek lub zmniejszy okno tak, że obraz nie zmieści się w jego obszarze roboczym, pojawią się paski przewi­jania (rys. poniżej z lewej), a gdy następnie sprawi, że cały będzie widoczny, paski przewi­jania znikną. Wadą takiego rozwią­zania jest przewi­janie całego obszaru roboczego wraz z menu i ew. innymi kontrol­kami, jak np. np. pasek statusu, które powinny pozostawać na pierwo­tnym miejscu.

W drugiej aplikacji defekt ten nie występuje. Do jej formu­larza wstawiono identy­czne jak poprzednio menu, a pod nim kontrolkę Panel, której właści­wość AutoScroll ustawiono na TrueDock na Fill. Ta druga właści­wość oznacza, że panel wypełni całą pozostałą część powierz­chni formu­larza. Na koniec do panela wstawiono kontrolkę PictureBox, na której rysowana jest elipsa. Tym razem paski przewi­jania są wyświe­tlane w panelu, gdy cały obraz się w nim nie mieści (rys. powyżej z prawej). Manipulo­wanie nimi wpływa jedynie na zawartość panela, menu główne aplikacji pozostaje na swoim miejscu. Kod źródłowy obydwu aplikacji różni się niewiele. Pliki Form1.cs modyfi­kowane przez progra­mistę są niemal takie same, inna jest tylko nazwa przestrzeni wynika­jąca z nazwy aplikacji:

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 test_1        // test_2 (dla drugiej aplikacji)
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void powiększToolStripMenuItem_Click(object sender, EventArgs e)
        {
            pictureBox1.Size = new Size(2 * pictureBox1.Width, 2 * pictureBox1.Height);
            pictureBox1.Invalidate();
            if (Math.Max(pictureBox1.Width, pictureBox1.Height) > 1000)
                powiększToolStripMenuItem.Enabled = false;
            zmniejszToolStripMenuItem.Enabled = true;
        }

        private void zmniejszToolStripMenuItem_Click(object sender, EventArgs e)
        {
            pictureBox1.Size = new Size(pictureBox1.Width / 2, pictureBox1.Height / 2);
            pictureBox1.Invalidate();
            if (Math.Min(pictureBox1.Width, pictureBox1.Height) < 20)
                zmniejszToolStripMenuItem.Enabled = false;
            powiększToolStripMenuItem.Enabled = true;
        }

        private void zakończToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            int dx = pictureBox1.Width / 20;
            int dy = pictureBox1.Height / 20;
            e.Graphics.FillEllipse(Brushes.Blue, dx / 2, dy / 2,
                                   pictureBox1.Width - dx, pictureBox1.Height - dy);
        }
    }
}

Kontrolka PictureBox jest szczególnie użyteczna w progra­mowaniu grafiki, ponieważ można nie tylko rysować po jej powierz­chni za pomocą narzędzi klasy Graphics, lecz także umieszczać w niej utworzony w pamięci lub pobrany z pliku obraz poprzez przypi­sanie go właści­wości Image. Kontrolka obsłu­guje kilka popular­nych formatów, m.in. JPG, PNG, GIF, ICO i WMP. Aby umożli­wiała wyświe­tlanie większych obrazów, należy ją wyposażyć w paski przewi­jania, umieszczając np. w panelu o właściwości AutoScroll ustawionej na True. Inne rozwią­zanie polega na wykorzy­staniu jej we własnej kontrolce dającej się przewijać i dostępnej w przyborniku.

Konstrukcję kontrolki użytkownika rozpoczynamy od nowego projektu prostej przeglą­darki plików grafi­cznych o nazwie Kontrolka. Do jej formu­larza, któremu wstępnie nadajemy tytuł Brak obrazka, wstawiamy menu z dwoma polece­niami OtwórzZakończ oraz kompo­nent OpenFile­Dialog, którego właściwość FileName (nazwa pliku) pozosta­wiamy pustą, usuwając propono­wany przez Visual Studio tekst, a właści­wość Filter (filtr) ustawiamy tak, by aplikacja miała dostęp do plików zapisanych w powsze­chnie stoso­wanych formatach grafi­cznych i zwycza­jowo do wszystkich:

Pliki JPEG (*.jpg;*.jpeg)|*.jpg;*.jpeg|PNG (*.png)|*.png|Wszystkie pliki (*.*)|*.*

Następnie tworzymy kontrolkę użytko­wnika, wybierając pole­cenie Projekt Dodaj kontrolkę użytkownika... lub Projekt Dodaj nowy element... . Po poja­wieniu się okna dodawania nowego elementu wskazu­jemy w nim pozycję Kontrolka użytko­wnika, zastępujemy zapropo­nowaną na dole okna nazwę UserControl1.cs np. nazwą Obrazek.cs i naciskamy przycisk Dodaj. W tym momencie w folderze projektu zostaną utworzone trzy pliki podobne do istnie­jących już tam plików formu­larza Form1. Dwa z nich są odpowie­dzialne za zachowanie kontrolki:

Szary prostokąt w oknie projektanta jest graficznym reprezen­tantem utworzonej kontrolki Obrazek, toteż kierując się zasadami programo­wania wizualnego, wstawiamy do niego z przybor­nika, podobnie jak w przypadku formu­larza, kontrolkę PictureBox, którą Visual Studio nazwie domyślnie pictureBox1, a po kliknięciu prawym przyciskiem myszki na powierzchni tego prosto­kąta usta­wiamy właści­wość AutoScroll kontrolki na True (rys.).

Wygenerowany w pliku Obrazek.cs kod źródłowy kontrolki uzupełniamy o dwie metody. Pierwsza ma wstawić określoną w jej argu­mencie mapę bitową do obiektu pictureBox1, ustawić rozmiar obrazu reprezen­towanego przez ten obiekt na rozmiar mapy bitowej i ulokować go w odpowie­dniej odległości od lewego górnego rogu kontrolki. Gdy szero­kość obszaru roboczego kontrolki jest większa od szerokości obrazu, ma on być wycentro­wany poziomo, zaś w przeciwnym razie ma przylegać do jej lewego brzegu. Według tej samej reguły obraz ma być ustawiony w pionie. Metoda ma nastę­pującą postać:

public void Wstaw(Bitmap mapa)
{
    pictureBox1.Image = mapa;
    pictureBox1.Size = mapa.Size;
    int dx = ClientSize.Width - pictureBox1.Width;
    int dy = ClientSize.Height - pictureBox1.Height;
    AutoScrollPosition = new Point(0, 0);
    pictureBox1.Location = new Point((dx > 0) ? dx / 2 : 0, (dy > 0) ? dy / 2 : 0);
}

Właściwość AutoScrollPosition kontrolki określa pozycję widocznej części powierz­chni obrazu. Gdyby przypisanie jej lewego górnego rogu w powyższej metodzie pominąć, wstawienie innej mapy bitowej w miejsce aktualnej mogłoby zaburzyć wyświe­tlanie nowego obrazu.

Druga metoda dotyczy obsługi zdarzenia Resize kontrolki Obrazek. Jej zadaniem jest wycentro­wanie obrazu, gdy jego szerokość i/lub wysokość jest mniejsza od szerokości i/lub wysokości obszaru roboczego kontrolki, a w prze­ciwnym razie pozosta­wienie jednej lub obydwu współrzę­dnych punktu lokowania obrazu wewnątrz kontrolki bez zmian:

private void Obrazek_Resize(object sender, EventArgs e)
{
    int dx = ClientSize.Width - pictureBox1.Width;
    int dy = ClientSize.Height - pictureBox1.Height;
    pictureBox1.Location = new Point((dx > 0) ? dx / 2 : pictureBox1.Location.X,
                                     (dy > 0) ? dy / 2 : pictureBox1.Location.Y);
}

Jeżeli projekt się kompiluje i jest włączona opcja autouzupeł­niania przybor­nika, kontrolka zostaje w nim umieszczona i jest dostępna w fazie projekto­wania aplikacji (rys.). Standar­dowo opcja jest wyłączona. Aby ją włączyć, należy wybrać polecenie Narzędzia Opcje, a następnie w oknie, które się pojawi, zaznaczyć pozycję Projektant formu­larzy systemu Windows i przestawić opcję Automaty­cznie wypełnij przybornik na True. Po zamknięciu przybornika i ponownym otwarciu go kontrolka powinna się w nim pojawić.

Kontrolkę Obrazek pobieramy z przybornika i wsta­wiamy do formu­larza. Środowisko nazwie ją obrazek1. Jej właści­wość Dock ustawiamy na Fill, przez co wypełni ona całą wolną powierz­chnię formu­larza. Pozostały jeszcze do zaprogra­mowania dwie metody obsługi zdarzeń Click poleceń OtwórzZakończ menu. Druga powinna tylko zamknąć okno aplikacji i zakoń­czyć jej działanie, pierwsza zaś po wybraniu przez użytko­wnika pliku grafi­cznego wstawić zapisany w nim obraz do kontrolki obrazek1 i zmienić tytuł okna na ścieżkę dostępu do pliku:

private void otwórzToolStripMenuItem_Click(object sender, EventArgs e)
{
    if (openFileDialog1.ShowDialog() == DialogResult.OK)
    {
        Bitmap mapa = new Bitmap(openFileDialog1.FileName);
        obrazek1.Wstaw(mapa);
        Text = openFileDialog1.FileName;
    }
}

Chociaż program daje się skompilować i po uruchomieniu umożliwia przeglą­danie plików grafi­cznych (rys.), ma defekt wycieku pamięci spowodo­wany niewła­ściwą obsługą bitmap. Aktualnie wyświe­tlana bitmapa powinna zostać zniszczona za pomocą metody Dispose po zastą­pieniu jej inną bitmapą lub na końcu wykonania programu. Błąd można łatwo usunąć, wprowa­dzając globalną referencję do aktualnie przeglą­danej bitmapy, modyfi­kując metodę obsługi pole­cenia Otwórz i dodając metodę obsługi zdarzenia FormClose formularza.

Ostateczna wersja kodu źródłowego zawartego w plikach Form1.csObrazek.cs tej prostej przeglą­darki grafi­cznej jest przedsta­wiona na dwóch poniż­szych listingach. Kod w pierwszym pliku ma postać:

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 Kontrolka
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        Bitmap akt_mapa = null;     // Aktualnie wyświetlana bitmapa

        private void otwórzToolStripMenuItem_Click(object sender, EventArgs e)
        {
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                Bitmap mapa = new Bitmap(openFileDialog1.FileName);
                obrazek1.Wstaw(mapa);
                if (akt_mapa != null) akt_mapa.Dispose();
                akt_mapa = mapa;
                Text = openFileDialog1.FileName;
            }
        }

        private void zakończToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (akt_mapa != null) akt_mapa.Dispose();
        }
    }
}

Z kolei kod źródłowy kontrolki użytkownika zawarty w pliku Obrazek.cs wygląda nastę­pująco:

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

namespace Kontrolka
{
    public partial class Obrazek : UserControl
    {
        public Obrazek()
        {
            InitializeComponent();
        }

        public void Wstaw(Bitmap mapa)
        {
            pictureBox1.Image = mapa;
            pictureBox1.Size = mapa.Size;
            int dx = ClientSize.Width - pictureBox1.Width;
            int dy = ClientSize.Height - pictureBox1.Height;
            AutoScrollPosition = new Point(0, 0);
            pictureBox1.Location = new Point((dx > 0) ? dx / 2 : 0, (dy > 0) ? dy / 2 : 0);
        }

        private void Obrazek_Resize(object sender, EventArgs e)
        {
            int dx = ClientSize.Width - pictureBox1.Width;
            int dy = ClientSize.Height - pictureBox1.Height;
            pictureBox1.Location = new Point((dx > 0) ? dx / 2 : pictureBox1.Location.X,
                                             (dy > 0) ? dy / 2 : pictureBox1.Location.Y);
        }
    }
}

Uwaga: Kompilator może dla rozpakowanego projektu wyświe­tlić komunikat informu­jący o problemie z odnale­zieniem typu Kontrol­ka.Obrazek. Kod programu jest jednak poprawny i nadal funkcjo­nuje. Po zignoro­waniu uwagi kompila­tora, skompilo­waniu programu, ew. zamknięciu i otwarciu przybor­nika oraz zapisaniu wszystkich plików komunikat nie będzie się pojawiał. Program był tworzony przy włączonym autouzupeł­nianiu przybor­nika, przez co efektem pobrania z niego kontrolki Obrazek i wsta­wienia jej do formu­larza Form1 było pojawienie się dekla­racji pola obrazek1 i szeregu instrukcji w genero­wanym automa­tycznie pliku Form1.Designer.cs:

private Obrazek obrazek1;
...
this.obrazek1 = new Kontrolka.Obrazek();
this.obrazek1.AutoScroll = true;
this.obrazek1.Dock = DockStyle.Fill;
this.obrazek1.Location = new System.Drawing.Point(0, 24);
this.obrazek1.Name = "obrazek1";
this.Controls.Add(this.obrazek1);

Wstawienie kontrolki do formu­larza bez korzy­stania z przybor­nika wymaga tzw. dynami­cznego jej utworzenia. W tym celu trzeba zadekla­rować w klasie Form1 (np. w pliku Form1.cs) pole, którego wartością ma być referencja do obiektu klasy Obrazek:

private Obrazek obrazek1;

(pole można nazwać inaczej, ale pozostanie przy nazwie obrazek1 nie wymaga zmiany kodu metody obsługi pole­cenia Otwórz). Następnie należy utworzyć obiekt klasy Obrazek i przy­pisać referencję do niego zmiennej obrazek1, nadać jego właści­wości Dock wartość Fill, a na koniec dodać go do kolekcji kompo­nentów formularza:

obrazek1 = new Obrazek();
obrazek1.Dock = DockStyle.Fill;
Controls.Add(obrazek1);

Początkujący programista nie powinien modyfikować pliku Form1.Designer.cs, by nie zakłócić poprawnego działania budowa­nego programu. Powyższe trzy instrukcje powinien umieścić w konstru­ktorze klasy Form1 (na końcu jego kodu, po wywo­łaniu metody Initialize­Component) znajdu­jącym się w pliku Form1.cs.

Jest oczywiste, że kontrolka Obrazek mogłaby oferować więcej funkcji, jak np. skalo­wanie obrazu czy dopaso­wanie go do rozmiaru okna. W obecnej postaci posłuży do wygodnego wyświe­tlania krzywych Sierpiń­skiego rysowanych na mapie bitowej, dodatkowe możliwości kontrolki nie znala­złyby w tym przypadku zasto­sowania.

Rekurencyjny algorytm kreślenia krzywej

W celu znalezienia schematu rekurencji krzywej Sierpiń­skiego oznaczmy przez n jej rząd, a przez h przesu­nięcie jednostkowe pióra. Jak widać na poniższym rysunku, krzywa składa się z ukośnych odcinków wiodących wzdłuż przeką­tnych kwadratów o boku h oraz poziomych i piono­wych odcinków o długości 2h. Rysunek sugeruje również, że w krzywej rzędu n można wyróżnić cztery identy­czne segmenty Sn (kolor czarny) obrócone względem siebie o kąt prosty i powią­zane czterema narożnymi odcinkami (kolor czerwony). Jeśli przyjąć, że zegment S0 jest krzywą pustą, to wówczas cała krzywa redukuje się do kwadratu złożonego tylko z naroż­nych odcinków. Traktujemy ją jako krzywą Sierpiń­skiego rzędu 0.

Przyjmijmy, że pióro może, podobnie jak żółw w języku Logo, poruszać się naprzód wzdłuż odcinka prostej i zmieniać kierunek ruchu. Jeżeli po naryso­waniu segmentu jego kierunek ruchu pozo­stanie taki sam jak przed rozpoczę­ciem ryso­wania go (strzałki), to schemat kreślenia krzywej Sierpiń­skiego rzędu n sprowadzi się do wstępnego usta­wienia kierunku ruchu pióra pod kątem –45o względem osi x (zgodnie z ruchem wskazówek zegara) i czterokro­tnego wykonania ciągu trzech operacji:

Rekurencyjność rozwiązania występuje w proce­durze kreślenia segmentu Sn i przejawia się w zastą­pieniu go czterema segmentami Sn–1 odpowie­dnio obróco­nymi i połą­czonymi trzema odcinkami:

Nietrudno zauważyć, że krzywa rzędu 0 zawiera się w kwadracie o boku 2h, krzywa rzędu 1 w kwadracie o boku 6h, a krzywa rzędu 2 w kwadracie o boku 14h. Z konstru­kcji krzywej Sierpiń­skiego rzędu n>0 wynika, że mieści się ona w kwadracie o boku an równym sumie długości dwóch boków an–1 i odcinka 2h. Łatwo stąd wyprowa­dzić ogólny wzór na rozmiar takiego kwadratu zależny tylko od przesu­nięcia jednostko­wego h:

Program w C#

Program rysujący krzywe Sierpińskiego tworzymy przy włączonym autouzupeł­nianiu przybor­nika. Nowemu formula­rzowi nadajemy tytuł Krzywa Sierpiń­skiego, jego właści­wość BackColor ustawiamy na Window (białe tło), a nastę­pnie wstawiamy do niego menu z dwoma polece­niami ParametryKoniec oraz pasek statusu z dwiema etykie­tami, które posłużą do wyświe­tlania parame­trów n i h naryso­wanej krzywej.

Naszym zadaniem jest teraz dołączenie oprogramo­wania kontrolki Obrazek do projektu programu. W tym celu najpierw kopiu­jemy pliki Obrazek.cs, Obrazek.Desi­gner.csObrazek.resx z aplikacji Kontrolka do folderu projektu, a potem wybieramy pole­cenie Projekt Dodaj istniejący element... i w oknie, które się pojawi, zazna­czamy je wszystkie i naciskamy przycisk Dodaj (dodatkowe składniki można też dodawać bez kopio­wania ich do folderu projektu). Po skompi­lowaniu programu kontrolka Obrazek pojawi się w oknie przybor­nika. Wstawiamy ją do formu­larza Form1 i usta­wiamy jej właści­wość Dock na Fill.

Rozbudowę kodu źródłowego programu zaczynamy od zadekla­rowania pól prywa­tnych klasy Form1 dla parame­trów krzywej, bieżącej pozycji pióra i kierunku jego ruchu oraz bitmapy, na której będzie rysowana krzywa:

private int n = 1;              // Rząd krzywej
private int h;                  // Przesunięcie jednostkowe
private int xPos, yPos;         // Pozycja pióra
private int kierunek;           // Kierunek ruchu pióra
private Bitmap mapa = null;     // Obraz krzywej

Przyjęliśmy, że na początku będzie rysowana krzywa Sierpiń­skiego rzędu pierw­szego. Przesu­nięcie jednostkowe pióra zostanie w metodzie obsługi zdarzenia Load dobrane do rozmiaru okna tak, by cała krzywa się w nim mieściła. Dwa kolejne pola określa­jące pozycję pióra w trakcie rysowania krzywej wykorzy­stujemy w metodzie Rysuj kreślącej odcinek prostej od aktualnej pozycji pióra do pozycji przesu­niętej o wskazane w parame­trach dx i dy wartości:

private void Rysuj(Graphics g, int dx, int dy)
{
    int x = xPos, y = yPos;
    g.DrawLine(Pens.Black, x, y, xPos += dx, yPos += dy);
}

Podobnie jak w przypadku funkcji linerel z biblio­teki grafi­cznej WinBGI w C++, nową pozycję pióra określa koniec naryso­wanego odcinka. W trakcie rysowania krzywej Sierpiń­skiego pióro może przesuwać się tylko w ośmiu kierun­kach. Gdy ponume­rujmy je kolej­nymi liczbami całko­witymi: 0 – północ, 1 – półno­cny zachód, 2 – zachód, ..., 7 – północny wschód, to wszystkie odcinki krzywej możemy rysować za pomocą metody:

private void Odcinek(Graphics g)
{
    switch (kierunek)
    {
        case 0: Rysuj(g, 0, -2 * h); break;  // Północ
        case 1: Rysuj(g, -h, -h);    break;  // Północny zachód
        case 2: Rysuj(g, -2 * h, 0); break;  // Zachód
        case 3: Rysuj(g, -h, h);     break;  // Południowy zachód
        case 4: Rysuj(g, 0, 2 * h);  break;  // Południe
        case 5: Rysuj(g, h, h);      break;  // Południowy wschód
        case 6: Rysuj(g, 2 * h, 0);  break;  // Wschód
        case 7: Rysuj(g, h, -h);     break;  // Północny wschód
    }
}

Na początku pióro jest ustawione w kierunku –45o względem osi x, co odpowiada wartości 5 zmiennej kierunek. Zwiększenie tej wartości o 1 oznacza obrót o 45o, zwiększenie o 3 obrót o 3x45o=135o, zaś zwiększenie o 6 obrót o 6x45o=270o, czyli o –90o. Jeżeli po tej modyfi­kacji nowa wartość zmiennej przekroczy zakres od 0 do 7, należy ją skory­gować za pomocą działania modulo 8 (% 8). Przedsta­wiony powyżej rekuren­cyjny schemat kreślenia segmentu Sn może więc wyglądać następująco:

private void Segment(Graphics g, int n)
{
    if (n > 0)
    {
        Segment(g, n - 1);
        Odcinek(g);
        kierunek = (kierunek + 6) % 8;    // Obrót o -90 stopni
        Segment(g, n - 1);
        kierunek = (kierunek + 3) % 8;    // Obrót o +135 stopni
        Odcinek(g);
        kierunek = (kierunek + 1) % 8;    // Obrót o 45 stopni
        Segment(g, n - 1);
        Odcinek(g);
        kierunek = (kierunek + 6) % 8;    // Obrót o -90 stopni
        Segment(g, n - 1);
    }
}

Metoda rysowania pełnej krzywej Sierpiń­skiego powinna wyzna­czyć pozycję począ­tkową pióra, ustawić początkowy kierunek jego ruchu i złożyć krzywą z czterech segmentów odpowie­dnio obróconych i połą­czonych narożnymi odcinkami. Wymagania te spełnia poniższa wersja rysująca krzywą na mapie bitowej:

private void Krzywa()
{
    xPos = h + 4;
    yPos = 4;
    kierunek = 5;                         // Południowy wschód
    int a = ((1 << (n + 2)) - 2) * h + 8;
    Bitmap temp = new Bitmap(a, a);
    Graphics g = Graphics.FromImage(temp);
    for (int k = 0; k < 4; k++)
    {
        Segment(g, n);
        Odcinek(g);
        kierunek = (kierunek + 6) % 8;    // Obrót o -90 stopni
    }
    g.Dispose();
    obrazek1.Wstaw(temp);
    if (mapa != null) mapa.Dispose();
    mapa = temp;
    toolStripStatusLabel1.Text = "n = " + n.ToString();
    toolStripStatusLabel2.Text = "h = " + h.ToString();
}

Zwiększenie współrzędnych punktu początko­wego o 4 i boku kwadratu mieszczą­cego krzywą o 8 (zmienna a) ma na celu utworzenie niewiel­kiego margi­nesu wokół krzywej. Przy obliczaniu boku kwadratu użyto przesu­nięcia bitowego w lewo (operator <<), które w odnie­sieniu do liczby 1 odpo­wiada podnie­sieniu podstawy 2 do potęgi równej liczbie przesu­nięć. Po utwo­rzeniu mapy bitowej temp ryso­wanie po jej powierz­chni stało się możliwe dzięki metodzie FromImage klasy Graphics tworzącej obiekt g udostępnia­jący narzędzia graficzne. Po naryso­waniu krzywej obiekt g zostaje zniszczony, zaś mapa bitowa temp zostaje wstawiona do kontrolki obrazek1, która wyświetla naryso­waną na niej krzywą. Następnie dotychcza­sowa bitmapa, jeśli istnieje, zostaje zniszczona, a jej rolę przejmuje bitmapa z nową krzywą. Na koniec uaktual­niane są etykiety paska statusu informu­jące o parame­trach tej krzywej.

Sekwencję instrukcji dotyczących obiektu g klasy Graphics można zastąpić, podobnie jak w przypadku strumieni, instrukcją using, która ogranicza istnienie zadeklarowanego w niej obiektu do podle­głego jej bloku i daje gwarancję, że zasoby związane z tym obiektem zostaną automaty­cznie zwolnione, gdy tylko nastąpi wyjście poza ten blok:

using (Graphics g = Graphics.FromImage(temp))
{
    for (int k = 0; k < 4; k++)
    {
        Segment(g, n);
        Odcinek(g);
        kierunek = (kierunek + 6) % 8;    // Obrót o -90 stopni
    }
}

Pierwsze wywołanie metody Krzywa ma miejsce w metodzie obsługi zdarzenia Load formu­larza po uprzednim obli­czeniu przesu­nięcia jednostko­wego dla krzywej Sierpiń­skiego rzędu 1:

private void Form1_Load(object sender, EventArgs e)
{
    h = Math.Min(obrazek1.Width, obrazek1.Height) / 7;
    Krzywa();
}

Ostatnim etapem budowy programu jest zaprogramo­wanie metod obsługi poleceń menu i formu­larza podrzę­dnego (rys.) umożliwia­jącego zmianę parame­trów krzywej. Formu­larz zawiera dwie kontrolki edycyjne (typ NumericUpDown) wraz z opisu­jącymi je etykie­tami (typ Label) i dwa przyciski (typ Button) z napi­sami RysujZamknij. Właści­wości Maximum pierwszej kontrolki edycyjnej nadajemy wartość 8, a właści­wościom MinimumMaximum drugiej wartości 1 i 128. Rząd n krzywej będzie więc można zmieniać w zakresie od 0 do 8, a przesu­nięcie jednostkowe h od 1 do 128. Górna granica przesu­nięcia została wstępnie ustawiona dla rzędu n równego 1 i przy każdej jego zmianie będzie określana jako 28–n. Ograni­czenia te wynikają z rozmiaru krzywej i rozdziel­czości ekranu. Na koniec właści­wość Dialog­Result drugiego przycisku usta­wiamy na Cancel, by kliknięcie na nim powodo­wało zamknięcie okna.

Przyjmijmy, że naciśnięcie przycisku Rysuj ma jedynie wymusić natychmia­stowe naryso­wanie krzywej dla parame­trów podanych w kontrol­kach okna podrzę­dnego. Dzięki temu użytko­wnik będzie mógł po otwarciu tego okna przeglądać krzywe o różnych parame­trach bez zamykania go. Zadanie to da się rozwiązać, gdy w formu­larzu Form2 znana będzie referencja do formu­larza Form1. Można ją przekazać w konstru­ktorze. Dodatkowo można w nim też przesłać aktualne parametry krzywej w celu ich wyświe­tlenia w kontrol­kach edycyjnych. Tak więc modyfi­kujemy plik Form2.cs, dekla­rując w klasie Form2 pole form typu referen­cyjnego Form1 i uzupeł­niając jej domyślny konstruktor:

private Form1 form;

public Form2(Form1 form, int n, int h)
{
    InitializeComponent();
    numericUpDown1.Value = n;
    numericUpDown2.Value = h;
    this.form = form;
}

Powróćmy na moment do formularza klasy Form1, by zaprogra­mować metodę obsługi pole­cenia Parametry (polecenie Koniec ma tylko zamknąć okno główne i zakoń­czyć wykonanie programu). Metoda powinna utworzyć okno podrzędne klasy Form2, przekazać mu stosowne argumenty i pokazać go w trybie modalnym. Ponieważ nie ma potrzeby zapamię­tania referencji do niego, metodę można sformu­łować następująco:

private void parametryToolStripMenuItem_Click(object sender, EventArgs e)
{
    (new Form2(this, n, h)).ShowDialog();
}

Potrzebna jest również metoda klasy Form1 o dostępie publi­cznym, za pomocą której okno podrzędne prześle do okna głównego nowe parametry krzywej i zleci jej naryso­wanie:

public void nowaKrzywa(int n, int h)
{
    this.n = n;
    this.h = h;
    Krzywa();
}

Na zakończenie definiujemy jeszcze dwie metody obsługi zdarzeń formu­larza klasy Form2. Pierwsza dotyczy zdarzenia Value­Changed kontrolki numeric­UpDown1 wyzwala­nego, gdy nastąpi zmiana właści­wości Value określa­jącej rząd n krzywej. Metoda ma wówczas ustawić właści­wość Maximum kontrolki numericUpDown2 na maksy­malną wartość przesu­nięcia h równą 28–n:

private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
    numericUpDown2.Maximum = 1 << (8 - (int)numericUpDown1.Value);
}

Zadaniem drugiej metody jest przekazanie do okna głównego apli­kacji parametrów krzywej zapisanych w kontrol­kach okna podrzę­dnego celem naryso­wania jej, gdy naciśnięty zostanie przycisk Rusuj:

private void button1_Click(object sender, EventArgs e)
{
    form.nowaKrzywa((int)numericUpDown1.Value, (int)numericUpDown2.Value);
}

Ostateczne sformułowanie kodu źródłowego C# zawartego w plikach Form1.csForm2.cs programu kreślącego krzywe Sierpiń­skiego przedsta­wiają dwa poniższe listingi. Rekuren­cyjna metoda kreślenia segmentu występu­jąca w pierwszym pliku jest w sto­sunku do pierwo­wzoru nieco uspra­wniona poprzez użycie opera­tora dekremen­tacji (--) bezpośre­dnio w porówna­niu n z zerem (zmniej­szenie wartości n po jej wykorzy­staniu). Kod zawarty w tym pliku ma postać:

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 KrzSierp1
{
    public partial class Form1 : Form
    {
        private int n = 1;              // Rząd krzywej
        private int h;                  // Przesunięcie jednostkowe
        private int xPos, yPos;         // Pozycja pióra
        private int kierunek;           // Kierunek ruchu pióra
        private Bitmap mapa = null;     // Obraz krzywej

        public Form1()
        {
            InitializeComponent();
        }

        private void Rysuj(Graphics g, int dx, int dy)
        {
            int x = xPos, y = yPos;
            g.DrawLine(Pens.Black, x, y, xPos += dx, yPos += dy);
        }

        private void Odcinek(Graphics g)
        {
            switch (kierunek)
            {
                case 0: Rysuj(g, 0, -2 * h); break;  // Północ
                case 1: Rysuj(g, -h, -h);    break;  // Północny zachód
                case 2: Rysuj(g, -2 * h, 0); break;  // Zachód
                case 3: Rysuj(g, -h, h);     break;  // Południowy zachód
                case 4: Rysuj(g, 0, 2 * h);  break;  // Południe
                case 5: Rysuj(g, h, h);      break;  // Południowy wschód
                case 6: Rysuj(g, 2 * h, 0);  break;  // Wschód
                case 7: Rysuj(g, h, -h);     break;  // Północny wschód
            }
        }

        private void Segment(Graphics g, int n)
        {
            if (n-- > 0)
            {
                Segment(g, n);
                Odcinek(g);
                kierunek = (kierunek + 6) % 8;      // Obrót o -90 stopni
                Segment(g, n);
                kierunek = (kierunek + 3) % 8;      // Obrót o +135 stopni
                Odcinek(g);
                kierunek = (kierunek + 1) % 8;      // Obrót o 45 stopni
                Segment(g, n);
                Odcinek(g);
                kierunek = (kierunek + 6) % 8;      // Obrót o -90 stopni
                Segment(g, n);
            }
        }

        private void Krzywa()
        {
            xPos = h + 4;
            yPos = 4;
            kierunek = 5;                           // Południowy wschód
            int a = ((1 << (n + 2)) - 2) * h + 8;
            Bitmap temp = new Bitmap(a, a);
            using (Graphics g = Graphics.FromImage(mapa))
                for (int k = 0; k < 4; k++)
                {
                    Segment(g, n);
                    Odcinek(g);
                    kierunek = (kierunek + 6) % 8;  // Obrót o -90 stopni
                }
            obrazek1.Wstaw(temp);
            if (mapa != null) mapa.Dispose();
            mapa = temp;
            toolStripStatusLabel1.Text = "n = " + n.ToString();
            toolStripStatusLabel2.Text = "h = " + h.ToString();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            h = Math.Min(obrazek1.Width, obrazek1.Height) / 7;
            Krzywa();
        }

        private void parametryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            (new Form2(this, n, h)).ShowDialog();
        }

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

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (mapa != null) mapa.Dispose();
        }

        public void nowaKrzywa(int n, int h)
        {
            this.n = n;
            this.h = h;
            Krzywa();
        }
    }
}

Zmiany wprowadzono również w konstruktorze klasy Form2 w pliku Form2.cs. W poprze­dniej postaci okno podrzędne było wyświe­tlane w przypad­kowym (domyślnym dla Windows) miejscu na ekranie zgodnie z usta­wieniem właści­wości StartPo­sition formu­larza na WindowsDe­faultLocation. Po zmianie tej właści­wości na Manual położenie okna jest okre­ślane przez właści­wość Location. Konstru­ktor oblicza jej wartość tak, by okno podrzędne było położone pośrodku prawego brzegu okna głównego. Kod zawarty w tym pliku wygląda następująco:

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 KrzSierp1
{
    public partial class Form2 : Form
    {
        private Form1 form;

        public Form2(Form1 form, int n, int h)
        {
            InitializeComponent();
            int x = form.Left + form.Width;
            int y = form.Top + form.Height / 2;
            Location = new Point(x - Width / 2, y - Height / 2);
            numericUpDown1.Value = n;
            numericUpDown2.Value = h;
            this.form = form;
        }

        private void numericUpDown1_ValueChanged(object sender, EventArgs e)
        {
            numericUpDown2.Maximum = 1 << (8 - (int)numericUpDown1.Value);
        }

        private void button1_Click(object sender, EventArgs e)
        {
            form.nowaKrzywa((int)numericUpDown1.Value, (int)numericUpDown2.Value);
        }
    }
}

A oto krzywa Sierpińskiego rzędu 5 narysowana przez program.

Algorytm (wersja 2)

Inny algorytm kreślenia krzywej Sierpiń­skiego podał Niklaus Wirth w książce Algorithms + Data Structures = Programs (1976, język Pascal) wydawanej również w Polsce (Algorytmy + struktury danych = programy, 1980–2004). Implemen­tację tego algorytmu w Delphi 7 można znaleźć na niniej­szej witrynie. Schemat rekuren­cyjny polega na złożeniu krzywej z czterech krzywych otwartych i łączących je odcinków. Na poniż­szym rysunku przedsta­wiającym krzywe Sierpiń­skiego rzędu 1 i 2 krzywe składowe są naryso­wane kolorem czarnym i oznaczone literami A, B, C i D z inde­ksami określa­jącymi rząd, a łączące je odcinki kolorem czerwonym. Rzecz jasna krzywe składowe rzędu 0 są puste, a krzywa Sierpiń­skiego rzędu 0 jest kwadratem złożonym z narożnych odcinków.

Schemat rekurencyjny kreślenia krzywych składowych A, B, C i D rzędu n można łatwo zdefi­niować za pomocą wywołań rekuren­cyjnych takich krzywych rzędu n–1:

Strzałki wskazują kierunek ruchu pióra podczas kreślenia odcinków łączących. Linie pojedyncze oznaczają przesu­nięcie pióra wzdłuż przekątnej kwadratu o boku h, a linie podwójne przesu­nięcie o długości 2h wzdłuż osi xy. Całą krzywą Sierpiń­skiego rzędu n można narysować za jednym pociągnię­ciem pióra według schematu:

Program w C# (wersja 2)

Program rysujący krzywe Sierpińskiego według algorytmu Wirtha budujemy podobnie jak poprzedni. W klasie Form1 potrzebne są tylko cztery pola n, h, xPosyPos reprezen­tujące rząd krzywej, przesu­nięcie jedno­stkowe pióra i jego pozycję bieżącą, a oprócz metody Rysuj kreślącej odcinek inne są metody rysujące krzywą. Przedsta­wiony powyżej schemat rekuren­cyjny kreślenia czterech krzywych składo­wych daje się łatwo przekształcić na język C#, np. jego pierwszy wiersz można zapisać w postaci:

private void A(Graphics g, int n)
{
    if (n > 0)
    {
        A(g, n - 1);  Rysuj(g, h, h);
        B(g, n - 1);  Rysuj(g, 2 * h, 0);
        D(g, n - 1);  Rysuj(g, h, -h);
        A(g, n - 1);
    }
}

W podobny sposób można zdefiniować metody rekuren­cyjne odpowia­dające pozostałym wierszom schematu, a stosowny fragment metody Krzywa odpowia­dający podanemu powyżej wzorcowi kreślenia całej krzywej Sierpiń­skiego można sformu­łować następująco:

A(g, n);  Rysuj(g, h, h);
B(g, n);  Rysuj(g, -h, h);
C(g, n);  Rysuj(g, -h, -h);
D(g, n);  Rysuj(g, h, -h);

Pełny kod źródłowy C# programu rysującego krzywą Sierpiń­skiego zawarty w pliku Form1.cs jest przedsta­wiony na poniższym listingu. Cztery rekuren­cyjne funkcje kreślenia krzywych składowych zostały nieco ulepszone poprzez użycie opera­tora dekremen­tacji (--) bezpośre­dnio w porówna­niach ich rzędu z zerem.

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 KrzSierp2
{
    public partial class Form1 : Form
    {
        private int n = 1;              // Rząd krzywej
        private int h;                  // Przesunięcie jednostkowe
        private int xPos, yPos;         // Pozycja pióra
        private int kierunek;           // Kierunek ruchu pióra
        private Bitmap mapa = null;     // Obraz krzywej

        public Form1()
        {
            InitializeComponent();
        }

        private void Rysuj(Graphics g, int dx, int dy)
        {
            int x = xPos, y = yPos;
            g.DrawLine(Pens.Black, x, y, xPos += dx, yPos += dy);
        }

        private void A(Graphics g, int n)
        {
            if (n-- > 0)
            {
                A(g, n);  Rysuj(g, h, h);
                B(g, n);  Rysuj(g, 2 * h, 0);
                D(g, n);  Rysuj(g, h, -h);
                A(g, n);
            }
        }

        private void B(Graphics g, int n)
        {
            if (n-- > 0)
            {
                B(g, n);  Rysuj(g, -h, h);
                C(g, n);  Rysuj(g, 0, 2 * h);
                A(g, n);  Rysuj(g, h, h);
                B(g, n);
            }
        }

        private void C(Graphics g, int n)
        {
            if (n-- > 0)
            {
                C(g, n);  Rysuj(g, -h, -h);
                D(g, n);  Rysuj(g, -2 * h, 0);
                B(g, n);  Rysuj(g, -h, h);
                C(g, n);
            }
        }

        private void D(Graphics g, int n)
        {
            if (n-- > 0)
            {
                D(g, n);  Rysuj(g, h, -h);
                A(g, n);  Rysuj(g, 0, -2 * h);
                C(g, n);  Rysuj(g, -h, -h);
                D(g, n);
            }
        }

        private void Krzywa()
        {
            xPos = h + 4;
            yPos = 4;
            int a = ((1 << (n + 2)) - 2) * h + 8;
            Bitmap temp = new Bitmap(a, a);
            using (Graphics g = Graphics.FromImage(temp))
            {
                    A(g, n);  Rysuj(g, h, h);
                    B(g, n);  Rysuj(g, -h, h);
                    C(g, n);  Rysuj(g, -h, -h);
                    D(g, n);  Rysuj(g, h, -h);
            }
            obrazek1.Wstaw(temp);
            if (mapa != null) mapa.Dispose();
            mapa = temp;
            toolStripStatusLabel1.Text = "n = " + n.ToString();
            toolStripStatusLabel2.Text = "h = " + h.ToString();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            h = Math.Min(obrazek1.Width, obrazek1.Height) / 7;
            Krzywa();
        }

        private void parametryToolStripMenuItem_Click(object sender, EventArgs e)
        {
            (new Form2(this, n, h)).ShowDialog();
        }

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

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (mapa != null) mapa.Dispose();
        }

        public void nowaKrzywa(int n, int h)
        {
            this.n = n;
            this.h = h;
            Krzywa();
        }
    }
}

Opracowanie przykładu: kwiecień/maj 2019