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

WinAPI C++

Generowanie fraktali metodą IFS Implementacja układu IFS w C++ Menu i inne zasoby programu Przechwytywanie komunikatów menu Procedura okna dialogowego Program "Fraktale" Poprzedni przykład Następny przykład Kontakt

Generowanie fraktali metodą IFS

Fraktal w potocznym znaczeniu jest obiektem samopodo­bnym, tzn. takim, którego części są podobne do całości. Strukturę fraktalną ma wiele obiektów w przyro­dzie, np. płatki śniegu, obrazy na zamar­zniętej szybie, łańcuchy górskie, chmury, drzewa, liście, brokuły itd. W bardziej idealnej formie fraktale występują w rzeczywi­stości matema­tycznej jako obiekty geometry­czne. Najprostszą metodą ich konstruo­wania jest wykorzy­stanie tzw. układu itero­wanych odwzo­rowań (ang. Iterated Function System, w skrócie IFS). Układ taki składa się ze skończonej liczby funkcji fk przekształ­cających pewien zbiór w siebie. Zaprezen­towany poniżej program w Windows API C++ generuje kilka przykła­dowych fraktali na płaszczyźnie za pomocą układu odwzo­rowań, które punktom (x, y) o współrzę­dnych rzeczy­wistych przyporzą­dkowują punkty (x', y') według wzorów:

gdzie Ak, Bk, ..., Fk są odpowiednio dobranymi liczbami rzeczywi­stymi.

Przykład 1. Trójkąt Sierpińskiego:

Przykład 2. Pięciokąt Sierpińskiego:

Przykład 3. Gałązka:

Przykład 4. Drzewko:

Przykład 5. Paprotka Barnsleya:

Algorytm IFS polega na iteracyjnym losowaniu jednego z odwzo­rowań układu i przekształ­ceniu punktu (x, y) poprzez to odwzoro­wanie (por. program Gra w chaos). Otrzy­many punkt (x', y') staje się w nastę­pnym kroku itera­cyjnym punktem (x, y) dla kolej­nego wylosowa­nego odwzoro­wania. Jako punkt startowy można wybrać dowolny punkt płaszczyzny. Wygenero­wany w ten sposób dostate­cznie liczny zbiór punktów jest po odrzu­ceniu niewiel­kiej ich liczby na początku obrazem fraktalnym. Prawdopodo­bieństwo wyloso­wania każdej z funkcji jest często takie samo, ale nie zawsze. Gdy nie jest jedna­kowe, powinno być określone jak w ostatnim przykładzie (IFS z prawdopodo­bieństwami, w skrócie IFSP). Oczywiście suma tych prawdopodo­bieństw powinna wynosić 1.

Niemal wszystkie rozpatrywane w programie fraktale mieszczą się w kwadracie [0,1]x[0,1], z wyjątkiem ostatniego, który niezna­cznie od niego odbiega, dlatego punkt startowy najlepiej wybrać z tego kwadratu. Wyświe­tlenie fraktala na powierz­chni roboczej okna wymaga stoso­wnego przeskalo­wywania i przesu­wania wyzna­czanych punktów.

Implementacja układu IFS w C++

Naszym zadaniem jest zbudowanie programu okienkowego w WinAPI C++, który umożliwia wybór i wyświe­tlenie w oknie dowolnego z pięciu wymienio­nych powyżej fraktali. Aby kod programu nie rozrósł się zbytnio, warto wydzielić z niego do odrębnego modułu obliczenia realizu­jące równania IFS. Plik nagłów­kowy ifs.h tego modułu może wyglądać następu­jąco:

// IFS - Iterated Function System
// ===========================================
// Uklady iterowanych odwzorowań dla fraktali:
// - trójkat Sierpińskiego
// - pięciokąt Sierpińskiego
// - gałązka
// - drzewko
// - paprotka Barnsleya
// ===========================================

#ifndef H_IFS
#define H_IFS

#include <stdlib.h>

void trojkat(double &x, double &y);
void pieciokat(double &x, double &y);
void galazka(double &x, double &y);
void drzewko(double &x, double &y);
void paprotka(double &x, double &y);

#endif // H_IFS

Każda z funkcji IFS dostaje w przekazy­wanych przez refe­rencję argumen­tach x i y współ­rzędne punktu wejścio­wego (x, y) i w tych samych argu­mentach umieszcza współ­rzędne punktu wyjścio­wego (x', y'), który staje się punktem wejściowym w nastę­pnym kroku itera­cyjnym. Liczba odwzo­rowań układu, ich wybór i sposób obliczeń są ukryte w drugim pliku o nazwie ifs.cpp stano­wiącym implemen­tację modułu:

#include "ifs.h"

void trojkat(double &x, double &y)
{
    switch (rand() % 3)
    {
        case 0:
            x = 0.5 * x;
            y = 0.5 * y;
            return;
        case 1:
            x = 0.5 * x + 0.5;
            y = 0.5 * y;
            return;
        case 2:
            x = 0.5 * x + 0.25;
            y = 0.5 * y + 0.5;
    }
}

void pieciokat(double &x, double &y)
{
    switch (rand() % 5)
    {
        case 0:
            x = 0.382 * x + 0.3072;
            y = 0.382 * y + 0.6190;
            return;
        case 1:
            x = 0.382 * x + 0.6033;
            y = 0.382 * y + 0.4044;
            return;
        case 2:
            x = 0.382 * x + 0.0139;
            y = 0.382 * y + 0.4044;
            return;
        case 3:
            x = 0.382 * x + 0.1253;
            y = 0.382 * y + 0.0595;
            return;
        case 4:
            x = 0.382 * x + 0.4920;
            y = 0.382 * y + 0.0595;
    }
}

void galazka(double &x, double &y)
{
    double p = x;
    switch (rand() % 3)
    {
        case 0:
            x =  0.387 * x + 0.430 * y + 0.2560;
            y =  0.430 * p - 0.387 * y + 0.5220;
            return;
        case 1:
            x =  0.441 * x - 0.091 * y + 0.4219;
            y = -0.009 * p - 0.322 * y + 0.5059;
            return;
        case 2:
            x = -0.468 * x + 0.020 * y + 0.4000;
            y = -0.113 * p + 0.015 * y + 0.4000;
    }
}

void drzewko(double &x, double &y)
{
    double p = x;
    switch (rand() % 5)
    {
        case 0:
            x =  0.195 * x - 0.488 * y + 0.4431;
            y =  0.344 * p + 0.443 * y + 0.2452;
            return;
        case 1:
            x =  0.462 * x + 0.414 * y + 0.2511;
            y = -0.252 * p + 0.361 * y + 0.5692;
            return;
        case 2:
            x = -0.058 * x - 0.070 * y + 0.5976;
            y =  0.543 * p - 0.111 * y + 0.0969;
            return;
        case 3:
            x = -0.035 * x + 0.070 * y + 0.4884;
            y = -0.469 * p - 0.022 * y + 0.5069;
            return;
        case 4:
            x = -0.637 * x + 0.8562;
            y =  0.501 * y + 0.2513;
    }
}

void paprotka(double &x, double &y)
{
    double p = x;
    int r = rand() % 100;
    if (r < 74)             // p = 0.74 (r = 0..73)
    {
        x =  0.849 * x + 0.037 * y + 0.075;
        y = -0.037 * p + 0.849 * y + 0.183;
    }
    else if (r < 87)        // p = 0.13 (r = 74..86)
    {
        x =  0.197 * x - 0.226 * y + 0.400;
        y =  0.226 * p + 0.197 * y + 0.049;
    }
    else if (r < 98)        // p = 0.11 (r = 87..97)
    {
        x = -0.150 * x + 0.283 * y + 0.575;
        y =  0.260 * p + 0.237 * y - 0.084;
    }
    else                    // p = 0.02 (r = 98..99)
    {
        x =  0.5;
        y =  0.16 * y;
    }
}

Menu i inne zasoby programu

Jednym z najważniejszych elementów GUI jest menu, które oferuje użytko­wnikowi wybór dowolnej operacji, jaką może wykonać program. Elementy menu mogą być rozwijane lub nie. Elementy rozwijane otwie­rają podmenu, a nieroz­wijane generują komunikat WM_COMMAND z okre­ślonym numerem ID, z wyjątkiem separa­tora służącego do oddzie­lania poziomą kreską grup tematy­cznych podmenu. Gdy użytko­wnik wybierze nierozwi­jany element menu różny od separa­tora, do proce­dury okna jest wysyłany komunikat WM_COMMAND, którego parametr typu WPARAM ma wartość równą numerowi ID wybra­nego elementu. Przyjęło się, aby numerami ID były liczby całkowite od 100 do 32000, gdyż pewnym numerom spoza tego zakresu przypi­sano różne znaczenia domyślne.

W celu polepszenia czytelności programu dobrze jest numerom ID nadawać sugestywne nazwy. Wielu progra­mistów rozpo­czyna je od przedrostka IDM_ (identyfi­kator menu) jak w przedsta­wionym poniżej pliku resource.h. Nietrudno się domyślić, że menu tworzo­nego programu generu­jącego fraktale umożliwi użytko­wnikowi wybór dowolnego z pięciu fraktali, zakoń­czenie wykonania programu i uzyskanie o nim krótkiej infor­macji.

#define IDM_TROJKAT   101
#define IDM_PIECIOKAT 102
#define IDM_GALAZKA   103
#define IDM_DRZEWKO   104
#define IDM_PAPROTKA  105
#define IDM_KONIEC    106
#define IDM_OPROG     107

Menu jest często jednym z elementów zasobów programu (ang. program resources), które są danymi przechowy­wanymi w pliku wykonaw­czym .exe lub niekiedy w osobnym pliku binarnym. Innymi rodzajami zasobów są okna dialo­gowe, łańcuchy, bitmapy i ikony. Projekt zasobów składa się zazwyczaj z pliku nagłówko­wego .h i pliku skrypto­wego .rc (w rozpatry­wanym przypadku resource.hfraktale.rc). Definio­wanie zasobów wydaje się proste, gdy używa się narzędzi specjal­nych, jak np. edytora zasobów dostępnego w Visual Studio. Uzyskana w nim struktura plików jest jednak dość zagmatwana, toteż z tego powodu, a także z uwagi na brak podobnych narzędzi w dwóch pozo­stałych środo­wiskach, których używamy, możemy posłużyć się Notatnikiem Windows, darmowym programem Notepad++ lub bezpo­średnio środowi­skowym edytorem kodu C++. Język skryptowy zasobów nie powinien bowiem nastrę­czać żadnych trudności. Menu i okna dialo­gowe są w nim opisane tekstowo, a bitmapy i ikony są reprezen­towane za pomocą odwołań do zawiera­jących je plików binarnych. W poniższym skrypcie zasobów tworzo­nego programu występuje defi­nicja menu i okna dialogo­wego oraz odwołanie do ikony znajdu­jącej się w pliku Ikona.ico:

#include <windows.h>
#include "resource.h"

MyMenu MENU
BEGIN
    POPUP "&Fraktale"
    BEGIN
        MENUITEM "&Trójkąt Sierpińskiego", IDM_TROJKAT
        MENUITEM "&Pięciokąt Sierpińskiego", IDM_PIECIOKAT
        MENUITEM "&Gałązka", IDM_GALAZKA
        MENUITEM "&Drzewko", IDM_DRZEWKO
        MENUITEM "&Paprotka Barnsleya", IDM_PAPROTKA
        MENUITEM SEPARATOR
        MENUITEM "Konie&c", IDM_KONIEC
    END
    POPUP "&Pomoc"
    BEGIN
        MENUITEM "O &programie", IDM_OPROG
    END
END

MyDialog DIALOG 160, 15, 161, 101
STYLE DS_MODALFRAME | WS_POPUP | WS_SYSMENU
CAPTION "O programie"
FONT 8, "Verdana"
BEGIN
    CTEXT "Program kreśli kilka fraktali,\nwykorzystując układy iterowanych\nodwzorowań (IFS).",
          -1, 17, 17, 128, 24
    CTEXT "(C) 2020 by Kazimierz Jakubczyk", -1, 25, 55, 112, 8
    DEFPUSHBUTTON "OK", IDOK, 57, 79, 50, 14
END

MyIcon ICON "Ikona.ico"

Poszczególne rodzaje zasobów określają słowa kluczowe MENU, DIALOGICON poprze­dzone nazwami MyMenu, MyDialogMyIcon, które zostaną użyte w pro­gramie, gdy trzeba będzie odwołać się do tych zasobów. Nawiasy zdaniowe BEGINEND określa­jące strukturę menu i okna dialogo­wego można zastąpić nawiasami klamrowymi { i }.

Elementy rozwijane menu są definiowane jako POPUP, nieroz­wijane jako MENUITEM. Tekst elementu menu może zawierać znak & (amper­sand), który powoduje podkre­ślenie występu­jącego po nim znaku, umożli­wiając użytkowni­kowi użycie skrótu klawiatu­rowego, gdy naciśnie lewy Alt. Jeżeli w elemencie MENUITEM zamiast tekstu występuje słowo SEPARATOR, element ten reprezen­tuje poziomą kreskę. Element nierozwi­jany niebędący separa­torem ma przypisany numer ID.

Definicja okna dialogowego zawiera współrzędne lewego górnego narożnika w odnie­sieniu do obszaru roboczego okna nadrzę­dnego oraz szero­kość i wyso­kość. Wielkości te nie wyrażają jednak liczby pikseli, lecz liczbę jednostek proporcjo­nalnych do rozmiaru zastoso­wanej w oknie czcionki. Dla osi poziomej jednostką tą jest 1/4 średniej szero­kości znaku, a dla pionowej 1/8 wysokości znaku. Taki system miary pozwala na zacho­wanie ogólnego wyglądu okna dialogo­wego nieza­leżnie od rozdziel­czości ekranu i wybranej czcionki, ale bez korzy­stania z pro­gramów narzę­dziowych utrudnia projekto­wanie okien dialo­gowych.

Styl okna dialogowego określają stałe DS_MODALFRAME, WS_POPUPWS_SYSMENU używane najczę­ściej w przy­padku okien modalnych posiada­jących menu systemowe. Wyjaśnijmy, że gdy okno modalne zostanie wyświe­tlone, nie pozwoli na przejście do innego okna tego samego programu, dopóki nie zostanie zamknięte. Słowo CAPTION służy do umieszczenia napisu na pasku tytu­łowym okna, FONT określa rozmiar i rodzaj czcionki, zaś CTEXTDEFPUSHBUTTON precy­zują zawartość okna, definiując trzy małe obiekty wizualne zwane kontrolkami (ang. controls), które Windows traktuje jak okna. Najczęściej spoty­kanymi kontrol­kami w oknach dialo­gowych są:

Wszystkie kontrolki mają przypisany numer ID. Ponieważ teksty statyczne i ikony nie wysyłają żadnych komuni­katów, ich numerem ID jest –1 oznaczany w plikach nagłów­kowych jako IDC_STATIC. Numerami ID przycisków OKAnuluj są 1 i 2 o nazwach symboli­cznych IDOKIDCANCEL. Poło­żenie kontrolek względem obszaru robo­czego okna dialogo­wego i ich rozmiar ustala się według tych samych zasad jak poło­żenie i rozmiar samego okna dialo­gowego.

Przechwytywanie komunikatów menu

Komunikaty generowane przez elementy menu są przechwy­tywane w proce­durze okna WndProc. Gdy wybrane zostanie pole­cenie menu, Windows wysyła do niej komunikat WM_COMMAND, przeka­zując w jego parame­trze wParam typu WPARAM numer ID wybra­nego elementu. W zapowie­dzianym programie genero­wania fraktali nieco uproszczona wersja procedury okna analizu­jącej wartości tego parametru i reagu­jącej na nie ma postać:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    static void(*fraktal)(double &, double &) = NULL;
    static COLORREF kolor = 0;
    HDC hdc;
    PAINTSTRUCT ps;

    switch (message)
    {
        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            srand(unsigned(time(NULL)));
            PostMessage(hWnd, WM_COMMAND, IDM_TROJKAT, 0);
            return 0;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDM_TROJKAT:
                    fraktal = trojkat;
                    kolor = RGB(96, 0, 224);
                    InvalidateRect(hWnd, NULL, TRUE);
                    return 0;

                case IDM_PIECIOKAT:
                    fraktal = pieciokat;
                    kolor = RGB(0, 160, 192);
                    InvalidateRect(hWnd, NULL, TRUE);
                    return 0;

                case IDM_GALAZKA:
                    fraktal = galazka;
                    kolor = RGB(84, 128, 0);
                    InvalidateRect(hWnd, NULL, TRUE);
                    return 0;

                case IDM_DRZEWKO:
                    fraktal = drzewko;
                    kolor = RGB(144, 96, 0);
                    InvalidateRect(hWnd, NULL, TRUE);
                    return 0;

                case IDM_PAPROTKA:
                    fraktal = paprotka;
                    kolor = RGB(0, 144, 0);
                    InvalidateRect(hWnd, NULL, TRUE);
                    return 0;

                case IDM_KONIEC:
                    SendMessage(hWnd, WM_CLOSE, 0, 0);
                    return 0;

                case IDM_OPROG:
                    DialogBox(hInstance, "MyDialog", hWnd, (DLGPROC)DlgProc);
                    return 0;
            }
            break;

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            if (fraktal != NULL)
            {
                ...                 // Generowanie i rysowanie fraktala
            }
            EndPaint(hWnd, &ps);
            return 0;

        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Procedura WndProc obsługuje komunikaty WM_CREATE, WM_COMMAND, WM_PAINTWM_DESTROY, pozosta­wiając inne do przetwo­rzenia proce­durze domyślnej DefWindowProc. W przy­padku komuni­katu WM_COMMAND wartość IDM_TROJKAT, IDM_PIECIOKAT, IDM_GALAZKA, IDM_DRZEWKO lub IDM_PAPROTKA parametru wParam oznacza, że użytko­wnik wybrał w menu jeden z pięciu dostępnych w pro­gramie fraktali. Proce­dura okna przypisuje wtedy zmiennej fraktal wskaźnik na wyzna­czającą ten fraktal funkcję IFS, ustala kolor ryso­wania go (makro RGB) i zapamię­tuje ten kolor w zmiennej kolor, po czym unieważnia obszar roboczy okna (funkcja InvalidateRect). Obie zmienne zostały zadekla­rowane jako statyczne, by istniały pomiędzy wywoła­niami procedury WndProc, zacho­wując przypisane im wartości. Parame­trami RGB są trzy liczby całko­wite od 0 do 255 określa­jące stopień nasilenia barw podsta­wowych: czerwonej, zielonej i niebie­skiej. Unieważ­nienie obszaru roboczego polega na wsta­wieniu komunikatu WM_PAINT do kolejki komuni­katów programu. Pierwszym argu­mentem InvalidateRect jest uchwyt okna. Wartość NULL drugiego argu­mentu oznacza, że unieważ­niony zostaje cały obszar roboczy, a wartość TRUE trzeciego, że po nadejściu komuni­katu WM_PAINT do proce­dury okna tło tego obszaru ma być wyczy­szczone przez funkcję BeginPaint.

Gdy procedura okna odbierze komunikat WM_COMMAND z parame­trem IDM_KONIEC oznacza­jącym, że użytko­wnik wybrał w menu polecenie Koniec, wysyła do siebie za pomocą funkcji SendMessage komunikat WM_CLOSE. Proce­dura okna nie obsługuje tego komuni­katu bezpo­średnio, lecz pozostawia go do przetwo­rzenia domyślnej proce­durze DefWindowProc, która po odebraniu go wywołuje funkcję Destroy­Window w celu zniszczenia okna. Gdy okno jest niszczone przez tę funkcję, procedura okna dostaje komunikat WM_DESTROY, który obsługuje, wsta­wiając do kolejki komunikat WM_QUIT kończący wyko­nanie programu (zob. program Pierwszy).

Z kolei obsługa komunikatu WM_COMMAND z parametrem wParam równym IDM_OPROG oznacza­jącym, że użytko­wnik wybrał w menu polecenie O pro­gramie, polega na utwo­rzeniu okna dialogo­wego za pomocą funkcji DialogBox. Funkcja wymaga podania uchwytu programu, nazwy zasobu tworzo­nego okna dialogo­wego, uchwytu okna nadrzę­dnego i wskaźnika na proce­durę obsługu­jącą komuni­katy okna dialogo­wego. Uchwyt programu procedura okna uzyskuje podczas przetwa­rzania komuni­katu WM_CREATE. Pobiera go z pola struk­tury zawiera­jącej infor­macje o utwo­rzonym oknie i przypisuje lokalnej zmiennej staty­cznej hInstance.

Zauważmy, że przeglądanie listy komumnikatów WM_COMMAND kończy się instrukcją break, gdy żaden z komuni­katów WM_COMMAND genero­wanych przez menu nie został napotkany. Dzięki temu inne komuni­katy WM_COMMAND, o ile pojawią się w pro­gramie, zostaną obsłużone przez domyślną proce­durę DefWindowProc. Gdyby użyć instru­kcji return, nigdy nie zostałyby obsłużone, co mogłoby dopro­wadzić do niepopra­wnego działania programu.

Powróćmy do komunikatu WM_CREATE wysyłanego do okna, gdy jest ono tworzone. Procedura okna obsłu­guje ten komunikat, zapamię­tując w lokalnej zmiennej staty­cznej hInstance uchwyt programu pobrany ze struktury klasy okna, inicjując gene­rator liczb losowych i wsta­wiając do kolejki komuni­katów programu komunikat WM_COMMAND z parame­trami IDM_TROJKAT i 0 (funkcja PostMessage). Gdy zatem okno zostanie utworzone, nie zawiera żadnego obrazu, ale po bezzwło­cznym dotarciu do niego powyż­szego komuni­katu pojawi się w nim trójkąt Sierpiń­skiego.

Procedura okna dialogowego

Funkcja obsługująca komunikaty okna dialogo­wego, nazywana procedurą okna dialogo­wego, ma takie same argumenty jak procedura okna, ale zwraca nie wartości typu LRESULT, lecz typu BOOL. Gdy obsłuży komunikat, zwraca wartość TRUE, a gdy tego nie zrobi, zwraca FALSE, zatem działa inaczej niż zwykła procedura okna, która nieobsłu­żone komuni­katy pozostawia do przetwa­rzania proce­durze domyślnej. Procedura okna dialogo­wego zawiera­jącego podsta­wowe informacje o programie generu­jącym fraktale wygląda następu­jąco:

BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        case WM_INITDIALOG:
            return TRUE;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDOK:
                case IDCANCEL:
                    EndDialog(hDlg, wParam);
                    return TRUE;
            }
    }
    return FALSE;
}

Komunikat WM_INITDIALOG jest pierwszym, który dociera do proce­dury okna dialogo­wego, a dzieje się to, gdy jest ono tworzone. Proce­dura zwraca wartość TRUE, która oznajmia Windows, że ma ustawić tzw. fokus (ang. focus) na pierwszą kontrolkę, która może przyj­mować komuni­katy od klawia­tury, czyli w rozpatry­wanym przy­padku na przycisku OK. Kontrolki informują swoim wyglądem, że mają fokus, wyświe­tlając wokół siebie prostokąt wyróżniony innym kolorem i linią przery­waną. Okno, w którym się znajdują, jest wtedy aktywne.

Drugim obsługiwanym komunikatem jest WM_COMMAND, którego parametr wParam jest równy IDOK lub IDCANCEL. Pierwsza sytuacja ma miejsce wówczas, gdy użytkownik naciśnie przycisk OK lub klawisz Enter, a druga, gdy naciśnie przycisk zamknięcia w prawym górnym narożniku okna lub klawisz Esc. Obsługa komuni­katu polega na wywo­łaniu funkcji EndDialog, która niszczy okno. Jej drugi argument określa wartość zwracaną przez funkcję DialogBox, która to okno utworzyła w trybie modalnym.

Program "Fraktale"

Rysowanie dowolnego z pięciu przewidzianych w pro­gramie fraktali odbywa się w proce­durze okna WndProc podczas przetwa­rzania komuni­katu WM_PAINT, gdy wartością zmiennej staty­cznej fraktal jest niepusty wskaźnik na jedną z funkcji realizu­jących równania IFS. Wartością zmiennej staty­cznej kolor typu COLORREF jest wtedy kolor rysowa­nego fraktala. Zmienne lokalne rect, a, dxdy określają odpowie­dnio prostokąt obszaru robo­czego okna, długość boku kwadratu, w którym rysowany jest fraktal, oraz odle­głość lewego dolnego rogu tego kwadratu od lewej i górnej krawędzi obszaru robo­czego, zaś zmienna n – liczbę iteracji proporcjo­nalną do powierz­chni kwadratu. Współ­rzędne punktów otrzymy­wanych w kolej­nych itera­cjach są przecho­wywane w zmien­nych x, y (punkt startowy jest wybierany losowo z kwa­dratu [0,1]x[0,1] zawiera­jącego rzeczy­wisty fraktal). Podobnie jak w pro­gramie konsolowym Gra w chaos używa­jącym biblio­teki grafi­cznej WinBGI, wyzna­czane punkty oprócz pięciu początko­wych są po przeska­lowaniu, przesu­nięciu i odwró­ceniu kierunku osi y wyświe­tlane za pomocą funkcji API SetPixel, której argu­menty określają uchwyt kontekstu ekranu, współ­rzędne punktu na obszarze roboczym i kolor rysowania:

case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    if (fraktal != NULL)
    {
        RECT rect;
        GetClientRect(hWnd, &rect);
        int a = 9 * min(rect.right, rect.bottom) / 10;
        int dx = (rect.right - a) / 2;
        int dy = (rect.bottom + a) / 2;
        int n = a * a / 3;
        double x = double(rand()) / RAND_MAX;
        double y = double(rand()) / RAND_MAX;
        for (int k = -5; k < n; k++)
        {
            fraktal(x, y);
            if (k >= 0) SetPixel(hdc, int(a * x) + dx, dy - int(a * y), kolor);
        }
    }
    EndPaint(hWnd, &ps);
    return 0;

Kod funkcji WinMain programu wyświetla­jącego fraktale możemy utworzyć, kopiując szablon tej funkcji z poprze­dniego programu i wprowa­dzając odwołania do zasobów przy wypeł­nianiu struktury WNDCLASS używanej do reje­stracji klasy okna głównego. Ikona jest pobierana z zasobów za pomocą funkcji LoadIcon, której argu­mentem jest uchwyt programu i nazwa zasobu, a wartością uchwyt ikony przypi­sywany polu hIcon struktury. Pobranie menu następuje w wyniku przypi­sania nazwy zasobu polu lpszMenuName tej struktury. Okno dialogowe jest tworzone w proce­durze WndProc przez funkcję DialogBox wymaga­jącą podania uchwytu programu, nazwy zasobu, uchwytu okna nadrzę­dnego i wskaźnika proce­dury okna dialogo­wego. Konse­kwencją tych wymagań jest zamieszczenie w programie proto­typu i definicji funkcji DlgProc będącej procedurą okna dialo­gowego.

Kod źródłowy programu jest przedstawiony na poniższym listingu. Program dodatkowo umieszcza nazwę fraktala na pasku tytu­łowym okna za pomocą funkcji SetWindowText, a w trakcie ryso­wania fraktala wyświetla zamiast trady­cyjnej strzałki kursor oczeki­wania wskazu­jący, że jest zajęty wykony­waniem czaso­chłonnej operacji. Nowy kursor myszy ustawia, wywołując funkcję SetCursor, która pobiera go z zasobów Windows i zwraca uchwyt do poprze­dniego kursora. Uchwyt ten jest wykorzy­stany po naryso­waniu fraktala do przywró­cenia kursora strzałki.

#include <windows.h>
#include <time.h>
#include "resource.h"
#include <ifs.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    static char szClassName[] = "Fraktale";
    HWND hWnd;
    MSG msg;
    WNDCLASS wc;

    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInst;
    wc.hIcon         = LoadIcon(hInst, "MyIcon");
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = "MyMenu";
    wc.lpszClassName = szClassName;

    if (!RegisterClass(&wc))
    {
        MessageBox(NULL, "Ten program wymaga Win32!", szClassName, MB_ICONERROR);
        return 0;
    }
    hwnd = CreateWindow(szClassName, szClassName, WS_OVERLAPPEDWINDOW,
                 CW_USEDEFAULT, CW_USEDEFAULT, 560, 400, NULL, NULL, hInst, NULL);
    ShowWindow(hwnd, iCmdShow);
    UpdateWindow(hwnd);
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    static void(*fraktal)(double &, double &) = NULL;
    static COLORREF kolor = 0;
    HDC hdc;
    PAINTSTRUCT ps;

    switch (message)
    {
        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            srand(unsigned(time(NULL)));
            PostMessage(hWnd, WM_COMMAND, IDM_TROJKAT, 0);
            return 0;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDM_TROJKAT:
                    fraktal = trojkat;
                    kolor = RGB(96, 0, 224);
                    InvalidateRect(hWnd, NULL, TRUE);
                    SetWindowText(hWnd, "Trójkąt Sierpińskiego");
                    return 0;

                case IDM_PIECIOKAT:
                    fraktal = pieciokat;
                    kolor = RGB(0, 160, 192);
                    InvalidateRect(hWnd, NULL, TRUE);
                    SetWindowText(hWnd, "Pięciokąt Sierpińskiego");
                    return 0;

                case IDM_GALAZKA:
                    fraktal = galazka;
                    kolor = RGB(84, 128, 0);
                    InvalidateRect(hWnd, NULL, TRUE);
                    SetWindowText(hWnd, "Gałązka");
                    return 0;

                case IDM_DRZEWKO:
                    fraktal = drzewko;
                    kolor = RGB(144, 96, 0);
                    InvalidateRect(hWnd, NULL, TRUE);
                    SetWindowText(hWnd, "Drzewko");
                    return 0;

                case IDM_PAPROTKA:
                    fraktal = paprotka;
                    kolor = RGB(0, 144, 0);
                    InvalidateRect(hWnd, NULL, TRUE);
                    SetWindowText(hWnd, "Paprotka Barsnleya");
                    return 0;

                case IDM_KONIEC:
                    SendMessage(hWnd, WM_CLOSE, 0, 0);
                    return 0;

                case IDM_OPROG:
                    DialogBox(hInstance, "MyDialog", hWnd, (DLGPROC)DlgProc);
                    return 0;
            }
            break;

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            if (fraktal != NULL)
            {
                RECT rect;
                GetClientRect(hWnd, &rect);
                int a = 9 * min(rect.right, rect.bottom) / 10;
                int dx = (rect.right - a) / 2;
                int dy = (rect.bottom + a) / 2;
                int n = a * a / 3;
                double x = double(rand()) / RAND_MAX;
                double y = double(rand()) / RAND_MAX;
                HCURSOR hCursor = SetCursor(LoadCursor(NULL, IDC_WAIT));
                for (int k = -5; k < n; k++)
                {
                    fraktal(x, y);
                    if (k >= 0) SetPixel(hdc, int(a * x) + dx, dy - int(a * y), kolor);
                }
                SetCursor(hCursor);
            }
            EndPaint(hWnd, &ps);
            return 0;

        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        case WM_INITDIALOG:
            return TRUE;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDOK:
                case IDCANCEL:
                    EndDialog(hDlg, wParam);
                    return TRUE;
            }
    }
    return FALSE;
}

Rysowanie fraktali możnaby znacznie przyśpieszyć, używając bitmapy w pamięci opera­cyjnej komputera. A oto kilka obrazów fraktali wygenero­wanych przez program:

a) trójkąt Sierpińskiego,

b) pięciokąt Sierpińskiego,

c) gałązka,

d) drzewko,

e) paprotka Barnsleya,

f) okno dialogowe informujące o programie.


Opracowanie przykładu: lipiec 2020