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

Przykład C#

Gra w chaos Liczby losowe Projekt aplikacji C# Kod źródłowy aplikacji Właściwości i zdarzenia Implementacja algorytmu Program w Visual C# Poprzedni przykład Następny przykład Program w C++ Kontakt

Gra w chaos

Niekiedy bardzo proste algorytmy prowadzą do zaskaku­jących wyników. Jednym z nich jest opisany poniżej algorytm generowania obrazu nazywany grą w chaos. Rysunek utworzony na ekranie monitora zaskakuje szczególnie, ponieważ całe postępo­wanie jest oparte na losowości, czyli niemożności przewi­dywania, chaosie.

Algorytm gry w chaos jest rzeczywiście bardzo prosty. Na początku wybie­ramy na ekranie trzy punkty P0, P1P2 tak, by stanowiły wierz­chołki trójkąta równobo­cznego, oraz dowolny punkt Q0 (rys.). Jest to tzw. punkt wiodący. Następnie losujemy jeden z wierz­chołków trójkąta i w środku odcinka łączą­cego ten wierz­chołek z punktem Q0 zazna­czamy nowy punkt wiodący Q1. Ponownie losujemy wierz­chołek trójkąta i w środku odcinka o końcach w wylo­sowanym wierz­chołku i punkcie Q1 zazna­czamy kolejny punkt wiodący Q2. Losowanie wierz­chołka i zazna­czanie nastę­pnego punktu wiodącego dokładnie w połowie odcinka łączącego wyloso­wany wierz­chołek z poprze­dnim punktem wiodącym powtarzamy wiele razy. Na rysunku pokazano pięć począ­tkowych kroków, w których kolejnymi wyloso­wanymi wierz­chołkami były P1, P2, P1, P2, P0.

Jaki będzie rezultat wielokrotnego wyznaczania nastę­pnego punktu wiodącego i ryso­wania go na ekranie? Wydaje się, że jeżeli punkt wiodący Q0 został wybrany na zewnątrz trójkąta P0P1P2, to i tak po niewiel­kiej liczbie kroków któryś z kolejnych punktów wiodących wpadnie do wnętrza trójkąta i każdy następny tam pozo­stanie. Czy rzeczy­wiście po dużej liczbie kroków itera­cyjnych punkty wiodące zapełnią losowo cały trójkąt? Przekonajmy się o tym, tworząc prostą aplikację okienkową Windows w C#.

Liczby losowe

Symulacja zdarzeń losowych na komputerze wymaga genero­wania liczb losowych. Należy jednak sobie uświa­domić, że generator taki działa według ściśle określo­nego algorytmu, a dostar­czone przez niego liczby jedynie sprawiają wrażenie losowości. Bardziej stosowne jest więc nazywanie ich liczbami pseudo­losowymi. W środo­wisku C# dostępna jest klasa Random umożli­wiająca genero­wanie liczb pseudo­losowych o rozkła­dzie jedno­stajnym. Jej bezargu­mentowy konstru­ktor używa zegara syste­mowego do zaini­cjowania genera­tora, zaś jednoargu­mentowy – wartości początkowej typu int stano­wiącej tzw. zarodek lub ziarno (ang. seed). Podanie tego samego zarodka skutkuje otrzyma­niem tej samej serii liczb pseudo­losowych, co można wykorzy­stać podczas testo­wania programu. Zazwyczaj przydatna jest pierwsza wersja konstruktora:

Random gen = new Random();

Do generowania liczb pseudolosowych całkowitych służy metoda Next. W naj­prostszej postaci nie ma ona żadnych argu­mentów, a wylosowana liczba mieści się w zakresie od 0 do 2147483647 (maksy­malna wartość int). Jej górną granicę można obniżyć, przeka­zując większą o 1 wartość argu­mentu. Na przykład instrukcja

int k = gen.Next(100);

przypisuje zmiennej k pseudolosową liczbę całkowitą z prze­działu [0,99]. Można również określić dolną granicę loso­wanej liczby, przeka­zując wartości dwóch argu­mentów do metody Next. Na przykład rzut kostką sześcienną można zasymu­lować następująco:

int n = gen.Next(1, 7);

Wartość zmiennej n należąca do zbioru {1,2,...,6} reprezentuje liczbę wyrzu­conych oczek. Klasa Random udostę­pnia też bezargu­mentową metodę NextDouble zwraca­jącą pseudo­losową liczbę rzeczy­wistą z prze­działu [0,1). Oto przykład jej wywołania:

double x = gen.NextDouble();

Projekt aplikacji C#

Program ma wygenerować w oknie Windows obraz złożony z pikseli, dlatego nie może być apli­kacją konsolową, której kontakt z użytko­wnikiem sprowadza się jedynie do czytania danych z kla­wiatury i wypisy­wania tekstu w konsoli, lecz okien­kową, która zawiera graficzny interfejs użytko­wnika (ang. Graphical User Interface – GUI) do prezen­tacji informacji i inter­akcji z użytko­wnikiem. Aby utworzyć projekt aplikacji okienkowej, po urucho­mieniu krea­tora nowego projektu i uka­zaniu się okna Nowy projekt dokonu­jemy następu­jących ustawień (rys.):

Na koniec naciskamy przycisk OK. W tym momencie zostaje utworzony folder rozwią­zania, a w nim szereg podfolderów i plików. W rozpa­trywanym przy­padku folder rozwią­zania o nazwie Chaos zawiera m.in.:

Jednocześnie na ekranie zostaje udostępniony zestaw narzędzi służących do wizual­nego projekto­wania, urucha­miania i testo­wania aplikacji (rys.). Oprócz głównego menu i zgrupo­wanych na paskach narzę­dziowych przycisków podczas tworzenia aplikacji okienkowej zazwyczaj korzysta się z nastę­pujących okien:

Edycja kodu źródło­wego często wymaga przełą­czania dwóch okien zajmu­jących to samo miejsce na ekranie – okna formu­larza na okno edytora kodu, i odwro­tnie. Niekiedy pomaga tu Visual Studio, udostę­pniając jedno z nich zgodnie z oczeki­waniami progra­misty, ale nieraz musi to zrobić on sam. Dowolne okno można przywołać, gdy nie jest widoczne, za pomocą menu Widok, chociaż w wię­kszości przypadków nie jest to jedyny sposób. Na przykład po kliknięciu prawym przyci­skiem myszy na formularzu można z menu konteksto­wego dostać się do okna edytora kodu lub okna właści­wości, a po kliknięciu w oknie edytora kodu – do okna formularza.

Kod źródłowy aplikacji

Wygenerowany przez środowisko szablonowy kod źródłowy aplikacji okien­kowej C# zajmuje kilka plików, z których szcze­gólne znaczenie mają Program.cs, Form1.csForm1.Designer.cs. W pierwszym zdefinio­wana jest przestrzeń nazw Chaos, a w niej klasa Program zawiera­jąca metodę statyczną Main, od której rozpo­czyna się wykonanie aplikacji. Oto zawartość tego pliku:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Chaos
{
    static class Program
    {
        /// <summary>
        /// Główny punkt wejścia dla aplikacji.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

Jak widać, działanie aplikacji sprowadza się do wykonania trzech metod standar­dowej klasy Application. Po usta­wieniu pewnych opcji przez dwie pierwsze z nich wywołana zostaje metoda Run, która po utwo­rzeniu okna klasy Form1 wyświetla go i prze­chodzi do obsługi związanych z nim zdarzeń. Definicja klasy Form1 wywodzącej się od standar­dowej klasy Form jest podzielona (partial) na dwa pliki. Część jej kodu, zawarta w pliku Form1.cs i przezna­czona do modyfi­kacji przez progra­mistę, 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 Chaos
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}

Konstruktor klasy wywołuje metodę InitializeComponent, która inicja­lizuje kompo­nenty wstawione do formu­larza. Jej kod znajduje się w drugiej części definicji klasy zawartej w pliku Form1.Designer.cs genero­wanym przez środo­wisko. Mało doświadczony progra­mista nie powinien tego pliku modyfi­kować, by nie zakłócić poprawnego działania tworzonej aplikacji. Oto kod źródłowy tej części definicji klasy Form1 na początkowym etapie jej projekto­wania:

namespace Chaos
{
    partial class Form1
    {
        /// <summary>
        /// Wymagana zmienna projektanta.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Wyczyść wszystkie używane zasoby.
        /// </summary>
        /// <param name="disposing">prawda, jeżeli zarządzane zasoby powinny
        /// zostać zlikwidowane; fałsz w przeciwnym wypadku.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        /// <summary>
        /// Metoda wymagana do obsługi projektanta — nie należy modyfikować
        /// jej zawartości w edytorze kodu.
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(800, 450);
            this.Text = "Form1";
        }
    }
}

Program daje się pomyślnie skompilować i uruchomić. Wyświe­tlone przezeń okno ma jasno­szary obszar roboczy (ang. client area) ograni­czony ramką i paskiem tytułowym, na którym znajduje się ikona menu syste­mowego, domyślny w Visual Studio C# tekst Form1 i trady­cyjne trzy przyciski – zwijania, maksyma­lizacji i zam­knięcia (rys.). Okno jest w pełni funkcjo­nalne. Można go przesunąć, zmienić jego rozmiar, zwinąć, zmaksyma­lizować, po zmaksyma­lizowaniu przywrócić do poprze­dniego rozmiaru, a na koniec zamknąć.

Właściwości i zdarzenia

Programowanie wizualne jest metodą tworzenia programu za pomocą narzędzi umożliwia­jących wybór standar­dowych elementów steru­jących, zwanych kompo­nentami lub składni­kami, i automa­tyczną generację kodu. Kompo­nent jest obiektem realizu­jącym określone zadanie dostępnym do wykorzy­stania w trakcie projekto­wania programu w postaci ikony w oknie Przybornik umieszczanej na formu­larzu (rys.) lub tworzonym dynami­cznie podczas jego wykonania. Większość kompo­nentów to tzw. kontrolki, z których jest tworzony graficzny interfejs użytko­wnika. Kontrol­kami są m.in. przycisk (Button), pole wyboru (CheckBox), etykieta (Label), pole listy (ListBox), pole edycji tekstu (TextBox), menu (MenuStrip) i panel grupujący inne kontrolki (Panel). Pozostałe kompo­nenty nie mają reprezen­tacji wizualnej w czasie wykony­wania aplikacji, pełnią jedynie różne funkcje sterujące. Przedstawi­cielem tej grupy jest zegar (Timer), który generuje z jedna­kową częstotli­wością komuni­katy, pozwalając zaprogra­mować wykonanie pewnych operacji w jedna­kowych odstępach czasu, co można wyko­rzystać np. w animacji.

Cechy komponentu lub formularza (formularz jest kompo­nentem) określają właści­wości (ang. properties), zaś jego zacho­wanie – metody i zdarzenia (ang. events). W oknie Właściwości (rys.) wyszcze­gólnione są te właści­wości, których wartości można modyfi­kować w fazie projekto­wania aplikacji. Większość z nich ma wstępnie ustawione wartości domyślne. Niektóre właści­wości wymagają wprowa­dzenia liczby, inne łańcucha, jeszcze inne wybrania wartości z listy ustawień. Istnieją także właści­wości zagnie­żdżone, które składają się z innych właści­wości. Są one oznaczone krzyżykiem (+) lub minusem (–) służącym do ich rozwijania lub zwijania. Nazwę domyślną komponentu można zmienić, wpisując nową nazwę jako wartość właści­wości Name i zatwier­dzając klawiszem Enter.

W rozpatrywanym przykładzie programu symulującego grę w chaos modyfi­kujemy następu­jące właści­wości formu­larza (zob. rys. poniżej z lewej):

Okno Właściwości służy nie tylko do ustawiania rozmaitych właści­wości kompo­nentów i formu­larzy, lecz także do dołączania do tych obiektów metod obsługi różnych zdarzeń (zob. rys. poniżej z prawej). Przykła­dowo, dla kontrolki Button (przycisk) można zdefi­niować metodę obsługu­jącą zdarzenie Click (kliknięcie), która będzie wywoły­wana za każdym razem, gdy użytko­wnik kliknie ten przycisk. Wstępnie zdarzeniom nie są przypisane żadne wartości domyślne. W celu zdefinio­wania metody obsługi zdarzenia wystarczy dwukrotnie kliknąć w pustym polu obok nazwy zdarzenia, a Visual Studio udostępni okno edytora kodu z wygene­rowanym przezeń szablonem metody, który należy uzupełnić stosownym kodem w języku C#. Można również podać własną nazwę metody obsługi zdarzenia, wpisując ją obok nazwy zdarzenia i zatwier­dzając klawiszem Enter.

Programowanie metod obsługi zdarzeń często bywa zadaniem niełatwym ze względu na kolejność pojawiania się zdarzeń i powiązania między generującymi je obiektami. W programie gry w chaos sensowne wydaje się uwzglę­dnienie jedynie trzech zdarzeń formu­larza Form1:

Implementacja algorytmu

Opisany na początku algorytm gry w chaos jest oparty na losowości, toteż w wygene­rowanej przez środowisko wstępnie pustej metodzie obsługu­jącej zdarzenie Load wypada utworzyć obiekt genera­tora liczb pseudolo­sowych:

private Random ruletka = null;

private void Form1_Load(object sender, EventArgs e)
{
    ruletka = new Random();
}

Parametrem metody jest obiekt sender klasy object, dla którego zaszło zdarzenie, oraz samo zdarzenie jako obiekt e klasy EventArgs, która dostarcza infor­macji związanych ze zdarzeniem. Zmienna ruletka zawiera­jąca refe­rencję do genera­tora liczb pseudolo­sowych jest zadekla­rowana jako pole klasy Form1, ponieważ ma być dostępna w innym miejscu kodu programu – metodzie tworzenia obrazu.

Zdarzenie Paint jest wyzwalane po raz pierwszy po załado­waniu okna do pamięci, a potem za każdym razem, gdy zmienia się rozmiar okna, gdy jest ono odkrywane po zakryciu przez inne okno (częściowo lub w całości), gdy jest zwijane lub przywracane. Pusty szkielet metody wygene­rowany przez Visual Studio ma postać:

private void Form1_Paint(object sender, PaintEventArgs e)
{
    |
}

Drugim parametrem metody jest obiekt e klasy PaintEventArgs zawiera­jący infor­macje dotyczące grafiki. Udostę­pnia on m.in. obiekt Graphics (rys.), który zawiera wiele narzędzi pozwala­jących na ryso­wanie (rendero­wanie) tekstu, obrazów (ikony, bitmapy) i licznych figur geome­trycznych (linie, prosto­kąty i wielo­kąty, elipsy oraz ich wycinki i łuki, krzywe sklejane), a także wypeł­nianie obszarów i two­rzenie efektów specjal­nych (końcówki linii, koloro­wanie gradien­towe, przekształ­canie obrazów). Do tworzenia grafiki używane są obiekty klasy Pen (pióro), Brush (pędzel) i Font (czcionka). Powierz­chnia ryso­wania jest w Windows złożona z pikseli. Punkt początkowy o współ­rzędnych (0,0) znajduje się w jej lewym górnym narożniku. Oznacza to, że oś x układu współrzę­dnych biegnie górnym brzegiem tej powierz­chni w prawo, a oś y jej lewym brzegiem w dół (por. okno graficzne WinBGI).

Najprostszym sposobem narysowania punktu na powierzchni formu­larza lub kompo­nentu jest utwo­rzenie mapy bitowej o rozmiarze jednego piksela i wyświe­tlenie jej za pomocą metody DrawImage. W przy­padku gry w chaos wyświe­tlanie dużej liczby takich bitmapek reprezen­tujących punkty wiodące wydaje się mniej korzystne niż jednej bitmapy przygoto­wanej wcześniej i przedsta­wiającej cały obraz, a na pewno jest nieefe­ktywne, gdy rozmiar odtwarza­nego okna nie uległ zmianie. Kontyn­ujemy zatem rozbudowę klasy Form1 od zadekla­rowania w niej pola bitmapa typu Bitmap zawierają­cego refe­rencję do obrazu, umieszczenia definicji metody Utwórz_Bitmapę generu­jącej ten obraz według algorytmu gry w chaos opisanego na początku niniej­szej strony i uzupeł­nienia metod obsługi zdarzeń LoadPaint:

private Random ruletka = null;
private Bitmap bitmapa = null;

private void Utwórz_Bitmapę()
{
    int a = 9 * ClientSize.Width / 10;
    int h = 9 * ClientSize.Height / 10;
    bitmapa = new Bitmap(a + 1, h + 1);
    Point[] P = {new Point(a / 2, 0), new Point(0, h), new Point(a, h)};
    Point Q = new Point(ruletka.Next(a), ruletka.Next(h));
    int n = a * b / 3;
    for (int k = 0; k < n; k++)
    {
        int w = ruletka.Next(3);
        Q.X = (Q.X + P[w].X) / 2;
        Q.Y = (Q.Y + P[w].Y) / 2;
        bitmapa.SetPixel(Q.X, Q.Y, Color.Blue);
    }
}

private void Form1_Load(object sender, EventArgs e)
{
    ruletka = new Random();
    Utwórz_Bitmapę();
}

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.DrawImage(bitmapa,
        (ClientSize.Width - bitmapa.Width) / 2,
        (ClientSize.Height - bitmapa.Height) / 2);
}

Jak widać, metoda Form1_Load po zainicjo­waniu genera­tora liczb pseudolo­sowych poleca utwo­rzenie bitmapy, która ma być wyświe­tlona po raz pierwszy po urucho­mieniu aplikacji, zaś metoda Form1_Paint rysuje bitmapę pośrodku obszaru roboczego okna. Bitmapa tworzona w metodzie Utwórz_Bitmapę jest mniejsza od obszaru roboczego okna – mieści się w nim wraz z margi­nesami bocznymi wynoszą­cymi 1/20 jego szero­kości oraz górnym i dolnym równym 1/20 jego wysokości. Drobna korekta jej rozmiaru (zwiększenie obu składowych o 1) ma na celu zapobiec próbie tworzenia bitmapy o zerowej szerokości lub wysokości, co byłoby błędem. Wierz­chołki trójkąta i punkt wiodący są reprezen­towane przez typ Point, który jest strukturą złożoną z dwóch pól całko­witych X i Y. Liczba kroków itera­cyjnych algorytmu, w których są wyznaczane kolejne punkty wiodące, jest proporcjo­nalna do rozmiaru bitmapy – wynosi 1/3 liczby wszys­tkich jej punktów. Do rysowania punktów wiodących posłużyła metoda SetPixel klasy Bitmap.

Poniższy rysunek prezentujący wynik wykonania programu (w wersji końcowej) uświa­damia, jak zawodna może być intuicja. Wygene­rowany obraz przedstawia fraktal zwany trójkątem Sierpiń­skiego. Jest on zbiorem wyjątkowo uporząd­kowanym, niemającym – wydawa­łoby się – nic wspólnego z chaosem i losowością. Cechuje go samopodo­bieństwo przeja­wiające się w tym, że jego część jest podobna do całości.

Utworzony na ekranie niewiary­godnie uporządko­wany obraz łatwo jest zepsuć, zmieniając rozmiar okna np. poprzez przecią­ganie myszką dolnego prawego narożnika (rys.). Przyczyną defektu programu jest jego nieprawi­dłowa reakcja na zdarzenie Resize, które jest wyzwalane każdora­zowo, gdy nastą­piła zmiana rozmiaru okna. Standar­dowo jest ono wtedy przeryso­wywane, jednak w tym przypadku operacja ta powinna zostać wykonana dopiero po wygenero­waniu nowej bitmapy, której rozmiar byłby dostoso­wany do nowego rozmiaru obszaru roboczego okna.

Rozbudowa wygenerowanej przez Visual Studio pustej metody obsługi zdarzenia Resize polega nie tylko na utwo­rzeniu nowej bitmapy za pomocą metody Utwórz_Bitmapę, lecz także na wymuszeniu przeryso­wania okna, a właściwie jego obszaru roboczego. Należy w tym celu wywołać metodę Invalidate, która jedynie unieważnia obszar roboczy (ang. invalidate area), powiada­miając system opera­cyjny komputera, że obszar ten wymaga przemalo­wania (obszar zostanie przeryso­wany dopiero wtedy, gdy system nie będzie zajęty ważniej­szymi sprawami). Metoda Form1_­Resize ma po uzupeł­nieniu postać:

private void Form1_Resize(object sender, EventArgs e)
{
    Utwórz_Bitmapę();
    Invalidate();
}

Niestety, program zawiera błąd zwany wyciekiem pamięci (ang. memory leak). Jak wiadomo, każdemu obiektowi tworzonemu w środo­wisku C# przydzie­lana jest pamięć. Gdy potem w trakcie działania programu obiekt przestaje być potrzebny, o czym świadczy brak referencji do niego, pamięć ta pozostaje bezuży­teczna do momentu wykrycia tego stanu przez wykony­wany w oddzielnym wątku mechanizm garbage collector (GC, odśmie­cacz pamięci) i zwolnienia jej (oddania do dyspo­zycji systemu operacyj­nego). Chociaż nie można określić, kiedy mechanizm GC usunie w ten sposób obiekt, a nawet czy stanie się to jeszcze w czasie wykony­wania programu, nie trzeba zbytnio zajmować się przydzie­laniem i odzyski­waniem pamięci. Ten sposób przydzie­lania i zwalniania pamięci dotyczy tylko zasobów zarzą­dzanych (typy standar­dowe, typ Point, tablice itd.). Jednak wiele klas korzysta z tzw. zasobów niezarzą­dzanych (strumienie, bitmapy, pędzle i pióra, czcionki itd.). Klasy te udostę­pniają metodę Dispose. Jeżeli obiekt ma tę metodę, należy po zakoń­czeniu z nim pracy ją wywołać, by go zniszczyć i odzyskać przydzie­loną mu pamięć. Jeśli się tego nie zrobi, wystąpi niepożą­dane zjawisko wycieku pamięci.

Zatem aby uniknąć tego problemu, formułujemy metodę, która ma odpowiadać za zwal­nianie pamięci przydzie­lonej bitmapie używanej w programie:

private void Usuń_Bitmapę()
{
    if (bitmapa != null)
    {
        bitmapa.Dispose();
        bitmapa = null;
    }
}

Metodę Usuń_Bitmapę wywołujemy na początku metody Form1_­Resize, by uniknąć utwo­rzenia nowej bitmapy bez zniszczenia dotych­czasowej, która stała się bezuży­teczna, a także na końcu wykonania programu – w metodzie obsługi zdarzenia FormClosed formu­larza wyzwala­nego tuż po zamknięciu okna.

Program w Visual C#

Poniżej przedstawiony jest kod źródłowy aplikacji C# realizu­jącej grę w chaos (listing obejmuje tylko plik Form1.cs). Metoda Utwórz_Bitmapę została w porównaniu z omówionym wyżej pierwo­wzorem nieco zmodyfi­kowana. Po pierwsze, skorygo­wano wartości zmiennych a i h określa­jące szerokość i wysokość tworzonej bitmapy, by pasowały do znanego wzoru wyraża­jącego związek pomiędzy długością podstawy i wysokością trójkąta równobo­cznego. Wygene­rowany obraz ma wówczas kształt trójkąta równobo­cznego. Po drugie, pominięto rysowanie pięciu dodatko­wych punktów wiodących o ujemnej numeracji na początku iteracji, gdyż często kilka takich punktów leży na zewnątrz trójkąta i zaburza obraz końcowy.

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

        private Random ruletka = null;
        private Bitmap bitmapa = null;

        private void Utwórz_Bitmapę()
        {
            int a = 9 * ClientSize.Width / 10;
            int h = 9 * ClientSize.Height / 10;
            int n;
            double wsp = 0.5 * Math.Sqrt(3.0);
            if (h > (n = (int)(a * wsp)))
                h = n;
            else if (a > (n = (int)(h / wsp)))
                a = n;
            bitmapa = new Bitmap(a + 1, h + 1);
            Point[] P = {new Point(a / 2, 0), new Point(0, h), new Point(a, h)};
            Point Q = new Point(ruletka.Next(a), ruletka.Next(h));
            n = a * b / 3;
            for (int k = -5; k < n; k++)
            {
                int w = ruletka.Next(3);
                Q.X = (Q.X + P[w].X) / 2;
                Q.Y = (Q.Y + P[w].Y) / 2;
                if (k >= 0)
                    bitmapa.SetPixel(Q.X, Q.Y, Color.Blue);
            }
        }

        private void Usuń_Bitmapę()
        {
            if (bitmapa != null)
            {
                bitmapa.Dispose();
                bitmapa = null;
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            ruletka = new Random();
            Utwórz_Bitmapę();
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.DrawImage(bitmapa,
                (ClientSize.Width - bitmapa.Width) / 2,
                (ClientSize.Height - bitmapa.Height) / 2);
        }

        private void Form1_Resize(object sender, EventArgs e)
        {
            Usuń_Bitmapę();
            Utwórz_Bitmapę();
            Invalidate();
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            Usuń_Bitmapę();
        }
    }
}

Opracowanie przykładu: grudzień 2018