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

WinAPI C++

Rotacja gwiazdki Tworzenie i malowanie bitmap Komunikaty zegarowe Aktywne i nieaktywne elementy menu Przygotowanie klatek bitmapy do animacji Program "Rotacja" w WinAPI C++ Poprzedni przykład Następny przykład Kontakt

Rotacja gwiazdki

Naszym zadaniem jest zbudowanie programu prostej animacji. Program ma utworzyć bitmapę przedsta­wiającą kolejne obrazy (klatki) obraca­jącej się gwiazdki, a nastę­pnie cykli­cznie je wyświetlać w równych odstępach czasu odmierza­nych przez zegar dostępny w Windows. Bitmapa (ang. bitmap), nazywana też mapą bitową, jest prosto­kątną tablicą pikseli o określo­nych kolorach. Jej szerokość i wysokość są wyrażane liczbą pikseli. Każdy piksel zajmuje tę samą liczbę bitów (ang. bits per pixel). Przy 1 bicie na pixel dostępne są tylko dwa kolory (czarny i biały, bitmapa monochro­matyczna), przy 4 bitach 16 kolorów (standard VGA), przy 8 bitach 256 kolorów, przy 16 bitach 65536 kolorów (ang. high color), a przy 24 bitach 16777216 kolorów nazywanych rzeczywi­stymi (ang. true color).

W programach okienkowych bitmapy są obiektami GDI podobnie jak pióra, pędzle i czcionki. Mają uchwyty typu HBITMAP, można je wybierać w konte­kście urzą­dzenia za pomocą funkcji Windows API SelectObject i usuwać z pamięci za pomocą DeleteObject. Po wybraniu bitmapy w konte­kście urzą­dzenia można na niej rysować, używając funkcji graficznych GDI takich jak SetPixel, LineTo, Rectangle, Ellipse, TextOut itp.

Tworzenie i malowanie bitmap

Jeden ze sposobów tworzenia bitmapy polega na użyciu funkcji CreateCompa­tibleBitmap, która wymaga podania kontekstu urządzenia oraz szero­kości i wyso­kości. Funkcja tworzy tzw. bitmapę kompaty­bilną z urzą­dzeniem graficznym, przydzie­lając jej i inicja­lizując pamięć. Na przykład bitmapę o rozmiarze 400x300 kompaty­bilną z urzą­dzeniem, którego uchwyt jest podany w zmiennej hdc typu HDC, można utworzyć, a po wykorzy­staniu usunąć z pamięci:

HBITMAP hBitmap;
...
hBitmap = CreateCompatibleBitmap(hdc, 400, 300);
...
DeleteObject(hBitmap);

Zazwyczaj bitmapa jest tworzona podczas innego wywołania proce­dury okna lub innej funkcji, niż jest w niej używana, toteż zgodnie z zasadą unikania zmiennych globalnych zmienna zawiera­jąca uchwyt bitmapy jest dekla­rowana w tej funkcji jako statyczna. Dzięki temu, pomimo że zmienna jest poza funkcją nieznana, nie ginie wraz z wyjściem z niej, jest przecho­wywana wraz z wartością i po ponownym wejściu do funkcji nadal jest w niej dostępna.

Aby wyświetlić bitmapę na ekranie, wydrukować na drukarce lub przesłać na inne urzą­dzenie graficzne, potrzebny jest kontekst urządzenia pamięcio­wego, w którym zostanie ona wybrana, a potem skopio­wana na to urzą­dzenie za pomocą funkcji BitBlt (ang. bit-block transfer, transfer bloku bitów). Urządzenie pamię­ciowe nie istnieje w rzeczywi­stości, a tylko w pamięci komputera. Kontekst takiego urzą­dzenia można utworzyć za pomocą funkcji CreateCompa­tibleDC, która wymaga podania uchwytu kontekstu urzą­dzenia rzeczywi­stego (argument ten może być równy NULL, a wtedy urzą­dzenie pamię­ciowe jest kompaty­bilne z monitorem). Kontekst urzą­dzenia pamięcio­wego powinien być ostate­cznie usunięty z pamięci za pomocą funkcji DeleteDC. Jeżeli hdc jest uchwytem urzą­dzenia grafi­cznego, to powyższą bitmapę można narysować na powierz­chni tego urzą­dzenia w sposób następu­jący:

HDC hdcMem;
...
hdcMem = CreateCompatibleDC(hdc);
SelectObject(hdcMem, hBitmap);
BitBlt(hdc, 0, 0, 400, 300, hdcMem, 0, 0, SRCCOPY);
DeleteDC(hdcMem);

Funkcja BitBlt przenosi piksele z prostokątnego obszaru urządzenia o konte­kście hdcMem do prostoką­tnego obszaru urządzenia o konte­kście hdc. Prostokąt docelowy ma współ­rzędne lewego górnego narożnika (0,0) i rozmiar 400x300 pikseli. Prostokąt źródłowy ma takie same parametry jak docelowy. SRCCOPY jest stałą określa­jącą jedną z 256 operacji rastrowych – proste kopiowanie (bez przekształ­cania bitów). Funkcją podobną do BitBlt jest StrechBlt. Jej dwa dodatkowe argu­menty określają rozmiar obszaru źródło­wego, co pozwala na rozcią­ganie lub zmniej­szanie obrazów podczas kopiowania, gdy rozmiary obu obszarów są różne.

Jak wiadomo, podczas przetwarzania komunikatu WM_PAINT należy najpierw uzyskać dostęp do kontekstu urzą­dzenia wyświe­tlającego, wywołując funkcję BeginPaint, a po wykonaniu funkcji GDI malują­cych obszar roboczy okna zwolnić ten kontekst, wywołując EndPaint. Obie wymagają podania uchwytu okna i wska­źnika na strukturę typu PAINTSTRUCT. Wszystkie instrukcje rysujące obrazek powinny znajdować się pomiędzy tymi dwoma wywoła­niami:

PAINTSTRUCT ps;
...
hdc = BeginPaint(hWnd, &ps);
...
EndPaint(hWnd, &ps);

Funkcja BeginPaint czyści tło obszaru roboczego za pomocą pędzla określo­nego w polu hbrBackground struktury WNDCLASS używanej do reje­stracji klasy okna, wypełnia danymi pola struktury ps i zwraca uchwyt kontekstu urzą­dzenia, zaś EndPaint zwalnia ten uchwyt. Malowanie obszaru robo­czego lub jego części może być również potrzebne w trakcie obsługi innych komuni­katów niż WM_PAINT. Uchwyt kontekstu urzą­dzenia wyświetla­jącego uzyskuje się wówczas za pomocą funkcji GetDC, a po użyciu zwalnia za pomocą ReleaseDC:

hdc = GetDC(hWnd);
...
ReleaseDC(hWnd, hdc);

Oto przykładowy program ilustru­jący, jak utworzyć bitmapę o rozmiarze 400x300 pikseli, zapełnić ją na niebiesko, narysować na niej pośrodku ośmiora­mienną gwiazdkę czerwonym piórem o szerokości dwóch pikseli i wypełnić żółtym pędzlem (zob. moduł rysowania gwiazdki), a potem skopiować centralnie do okna:

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

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

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    static char szClassName[] = "BitmapTest";
    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(NULL, IDI_APPLICATION);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = NULL;
    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, 450, 370, 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 HBITMAP hBitmap;
    static int dx, dy;
    PAINTSTRUCT ps;
    HDC hdc, hdcMem;
    HBRUSH hBrush;
    HPEN hPen;

    switch (message)
    {
        case WM_CREATE:
            hdc = GetDC(hWnd);
            hBitmap = CreateCompatibleBitmap(hdc, 400, 300);
            hdcMem = CreateCompatibleDC(hdc);
            ReleaseDC(hWnd, hdc);
            SelectObject(hdcMem, hBitmap);
            SelectObject(hdcMem, CreateSolidBrush(RGB(0, 0, 255)));
            Rectangle(hdcMem, -1, -1, 401, 301);
            hBrush = CreateSolidBrush(RGB(255, 255, 0));
            DeleteObject(SelectObject(hdcMem, hBrush));
            hPen = CreatePen(PS_SOLID, 2, RGB(255, 0, 0));
            SelectObject(hdcMem, hPen);
            gwiazdka(hdcMem, 8, 200, 150, 120, 50);
            DeleteDC(hdcMem);
            DeleteObject(hBrush);
            DeleteObject(hPen);
            return 0;

        case WM_SIZE:
            dx = (LOWORD(lParam) - 400) / 2;
            dy = (HIWORD(lParam) - 300) / 2;
            return 0;

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            hdcMem = CreateCompatibleDC(hdc);
            SelectObject(hdcMem, hBitmap);
            BitBlt(hdc, dx, dy, 400, 300, hdcMem, 0, 0, SRCCOPY);
            DeleteDC(hdcMem);
            EndPaint(hWnd, &ps);
            return 0;

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

Program tworzy bitmapę, gdy do procedury okna dociera komunikat WM_CREATE. Na czas tworzenia bitmapy i kontekstu urządzenia pamięcio­wego udostę­niany jest kontekst rzeczywi­stego urządzenia wyświetla­jącego. Po jego zwolnieniu i wybraniu bitmapy w kontekście urządzenia pamięcio­wego cała jest koloro­wana na niebiesko uprzednio przygoto­wanym pędzlem. Rysowany przez funkcję Rectangle na powierz­chni urzą­dzenia pamięcio­wego reprezen­towanej przez bitmapę prostokąt wykracza poza nią, na bitmapie pozostaje tylko jego wypełnienie. Po zmianie domyślnego pióra na czerwone i pędzla na żółty na bitmapie rysowana jest gwiazdka. Na koniec kontekst urządzenia pamięcio­wego oraz żłóty pędzel i czerwone pióro są usuwane z pamięci. Bitmapa jest usuwana w trakcie przetwa­rzania komuni­katu WM_DESTROY, tj. gdy okno po usunięciu z ekranu jest niszczone.

Utworzona bitmapa jest wyświetlana za pomocą funkcji BitBlt na środku obszaru roboczego okna podczas przetwa­rzania komuni­katu WM_PAINT. Oprócz kontekstu urządzenia rzeczywi­stego operacja wymaga tymcza­sowego kontekstu urzą­dzenia pamięcio­wego. Współ­rzędne lewego górnego narożnika prosto­kąta docelowego, do którego jest kopiowana bitmapa, są wyzna­czane, gdy do okna dotrze komunikat WM_SIZE. Efekt wykonania programu jest przedsta­wiony na poniższym rysunku.

Uwaga. Kontekst urządzenia pamięciowego ma po utworzeniu początkową powierz­chnię wyświe­tlania reprezen­towaną przez bardzo małą bitmapę monochroma­tyczną. Ma ona zaledwie rozmiar 1x1 piksela i zajmuje tylko jeden bit. Niektórzy progra­miści uważają, że błędem jest pomijanie jej przy wybieraniu nowej bitmapy. Na przykład występu­jącą w powyż­szym programie sekwencję instru­kcji (obsługa komuni­katu WM_PAINT):

hdcMem = CreateCompatibleDC(hdc);
SelectObject(hdcMem, hBitmap);
BitBlt(hdc, dx, dy, 400, 300, hdcMem, 0, 0, SRCCOPY);
DeleteDC(hdcMem);

należałoby według nich zastąpić następującą:

HBITMAP staraMapa;
...
hdcMem = CreateCompatibleDC(hdc);
staraMapa = SelectObject(hdcMem, hBitmap);
BitBlt(hdc, dx, dy, 400, 300, hdcMem, 0, 0, SRCCOPY);
SelectObiect(hdcMem, staraMapa);
DeleteDC(hdcMem);

Rzecz jasna taki sposób wybierania bitmap w kontekście urządzenia pamięciowego jest poprawny, ale czy konieczny? Wydaje się, że nie, bo kontekst ten nie jest udostę­pniany jak kontekst urządzenia rzeczywi­stego, lecz tworzony (create) i na końcu nie zwalniany, lecz usuwany (delete). Nie wydaje się też, by początkową bitmapę, jak pokazują niektórzy, usuwać za pomocą funkcji DeleteObject przy wybie­raniu nowej, bo jest ona de fakto obiektem standar­dowym. Przykłady dostępne w dokumen­tacji Microsoft i podrę­czniku Charlsa Petzolda pt. Progra­mowanie Windows potwier­dzają zasadność pierwszego rozwią­zania.

Komunikaty zegarowe

Zegar (ang. timer) jest urządzeniem generu­jącym z określoną częstotli­wością komuni­katy WM_TIMER. Przydzie­lenie progra­mowi zegara (urucho­mienie zegara) nastę­puje w wyniku wywołania funkcji SetTimer. Jeżeli komuni­katy zegarowe mają być kierowane do proce­dury okna, co zazwyczaj jest stosowane, pierwszym argu­mentem wywo­łania funkcji jest uchwyt okna, drugim niezerowy numer ID zegara, trzecim liczba całko­wita wyraża­jąca długość odcinka czasu w milise­kundach, a czwartym wskaźnik NULL. Na przykład instrukcja

SetTimer(hWnd, 1, 500, NULL);

przydziela programowi zegar o numerze ID równym 1. Od tego momentu do proce­dury okna o uchwycie hWnd będą co pół sekundy nadcho­dzić komuni­katy WM_TIMER. Interwał czasowy może wynosić od 1 ms (teorety­cznie, w Windows NT od 10 ms) do 4 294 967 295 ms (blisko 50 dni). Komuni­katy WM_TIMER są umieszczane w zwykłej kolejce komuni­katów programu, mają podobnie jak WM_PAINT niski priorytet i mogą być gubione, gdy Windows jest zajęty reali­zacją ważniej­szych zadań, nie ma więc gwarancji, że będą pojawiać się idealnie co określony odstęp czasu.

Program może korzystać z więcej niż jednego zegara, pod warunkiem jednak, że ich numery ID są różne. Wartością para­metru wParam typu WPARAM jest w komuni­kacie WM_TIMER numer ID zegara, który ten komunikat wygene­rował, dzięki czemu można rozróżniać nadcho­dzące do proce­dury okna komuni­katy zegarowe. Aby zakończyć wysy­łanie komuni­katów WM_TIMER, wystarczy wywołać funkcję KillTimer, która niszczy zegar. Jej pierwszym argu­mentem jest uchwyt okna, a drugim numer ID zegara. W powyż­szym przypadku odpowie­dnią instrukcją kończącą genero­wanie komuni­katów zegarowych jest

KillTimer(hWnd, 1);

Gdy okno jest niszczone, wszystkie uruchomione zegary są również niszczone, nie ma więc potrzeby usuwania ich za pomocą funkcji KillTimer. Do dobrych zwyczajów programo­wania należy jednak kasowanie zegarów w ramach obsługi komuni­katu WM_DESTROY.

Aktywne i nieaktywne elementy menu

Proste menu programu animacji będzie składać się z czterech elementów poleca­jących urucho­mienie rotacji gwiazdki, zatrzy­manie jej, odwró­cenie kierunku obrotu i zakoń­czenie wyko­nania programu. Plik nagłów­kowy resource.h zasobów programu zawie­rający definicje numerów ID tych elementów ma postać:

#define IDM_START   101
#define IDM_STOP    102
#define IDM_ODWROC  103
#define IDM_KONIEC  104

Skrypt zasobów rotacja.rc mieści w sobie definicję struktury menu i odwołanie do ikony programu:

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

Menu MENU
BEGIN
    MENUITEM "&Start", IDM_START
    MENUITEM "Sto&p", IDM_STOP, GRAYED
    MENUITEM "&Odwróć", IDM_ODWROC
    MENUITEM "&Koniec", IDM_KONIEC
END

Ikona ICON "Star.ico"

Słowo kluczowe GRAYED oznacza, że element menu Stop o numerze ID równym IDM_STOP jest początkowo nieaktywny (tekst polecenia wyświe­tlany na szaro). Domyślnie elementy menu są aktywne – ENABLED. Gdy po urucho­mieniu programu użytko­wnik wybierze pole­cenie Start, element menu o numerze IDM_START stanie się nieaktywny, element IDM_STOP aktywny i zegar zostanie włączony:

EnableMenuItem(menu, IDM_START, MF_GRAYED);
EnableMenuItem(menu, IDM_STOP, MF_ENABLED);
DrawMenuBar(hWnd);
SetTimer(hWnd, 1, TIME, NULL);

W rezultacie gwiazdka będzie się obracać. Gdy potem użytko­wnik zatrzyma obrót, wybie­rając polecenie Stop, element menu o numerze IDM_START stanie się aktywny, element IDM_STOP nieaktywny i zegar zostanie zniszczony:

EnableMenuItem(menu, IDM_START, MF_ENABLED);
EnableMenuItem(menu, IDM_STOP, MF_GRAYED);
DrawMenuBar(hWnd);
KillTimer(hWnd, 1);

Funkcja EnableMenuItem zależnie od wartości trzeciego argu­mentu (stała MF_ENABLED lub MF_GRAYED) aktywuje lub dezakty­wuje element menu o numerze ID określonym w drugim argu­mencie, a DrawMenuBar rysuje zmieniony pasek menu. Argument menu jest uchwytem menu programu. Można go uzyskać, znając uchwyt okna:

HMENU menu;
...
menu = GetMenu(hWnd);

Przygotowanie klatek bitmapy do animacji

Technika animacji z użyciem bitmapy będącej sekwencją prosto­kątnych klatek (ang. frame – kadr, ramka) reprezen­tujących kolejne obrazy porusza­jącgo się obiektu polega na kopio­waniu klatek jednej po drugiej do obszaru roboczego okna z określoną szybko­ścią w miarę pojawiania się komuni­katów zegarowych. W przy­padku rotacji gwiazdki bitmapę można utworzyć bezpo­średnio w programie za pomocą narzędzi GDI. Kolejne klatki powinny stanowić cykl zamknięty przedsta­wiający tę samą gwiazdkę obróconą względem poprze­dniej (pierwsza względem ostatniej) o niewielki stały kąt zależny od liczby jej ramion i liczby wszystkich klatek. Gdy obiekt porusza się po jedno­litym tle, nie ma problemu z wyświe­tlaniem klatek. Wystarczy jedynie zadbać, by ich tło było takie samo jak tło obszaru robo­czego okna i użyć stałej SRCCOPY w wywo­łaniu funkcji BitBlt kopiującej klatkę na powierz­chnię okna. W rozpatry­wanym programie tło animacji będzie turkusowe, a klatki będą przedsta­wiać dwie gwiazdki o różnych rozmia­rach nałożone jedna na drugą, turkusową na żółtej (rys.), obraca­jące się w przeci­wnych kierunkach.

Program stanie się bardziej czytelny i łatwiejszy do eksperymen­towania, gdy zdefi­niujemy następu­jące stałe:

const int NRAM = 9;                   // Liczba ramion gwiazdki
const int NKLA = 20;                  // Liczba klatek animacji
const int DKAT = 360 / (NRAM * NKLA); // Przyrost kąta obrotu

Kąt 360o powinien być podzielny przez iloczyn liczby ramion gwiazdki i liczby klatek animacji, gdyż w przeci­wnym razie nie będzie ona płynna. Do przygoto­wania bitmapy klatek użyjemy następu­jących zmiennych:

static HPEN hPen;                     // Pióro do kreślenia gwiazdek
static HBRUSH hBkBrush,               // Pędzel do wypełniania tła okna
              hBrush;                 // Pędzel do wypełniania gwiazdki
static HBITMAP klatki = NULL;         // Bitmapa klatek animacji
static int R, r,                      // Promienie większej gwiazdki
           S, s,                      // Promienie mniejszej gwiazdki
           xs, ys,                    // Środek obszaru roboczego
           d;                         // Wysokość bitmapy klatek
HDC hdc, hdcMem;                      // Kontekst ekranowy i pamięciowy

Pędzel używany do wypełniania tła obszaru roboczego okna jest tworzony w funkcji głównej programu, gdy inicjali­zowana jest struktura typu WNDCLASS potrzebna do reje­stracji klasy okna. Jego uchwyt uzyskujemy w proce­durze okna, wywołując funkcję GetClassLong pobiera­jącą dane z obszaru pamięci skojarzo­nego z klasą okna. Rodzaj danych do pobrania można określać za pomocą stałych, np. GCL_HBRBACKGROUND oznacza uchwyt pędzla wypełnia­jącego tło okna, a GCL_HICON uchwyt ikony. Tworzymy również pomarań­czowe pióro o szero­kości dwóch pikseli do rysowania konturów gwiazdek i żółty pędel do wypeł­niania większej gwiazdki:

hBkBrush = (HBRUSH)GetClassLong(hWnd, GCL_HBRBACKGROUND);
hPen = CreatePen(PS_SOLID, 2, RGB(255, 165, 0));
hBrush = CreateSolidBrush(RGB(255, 255, 0));

Odpowiednim momentem do wykonania tych operacji jest obsługa komuni­katu WM_CREATE, gdyż obydwa pędzle i pióro będą używane do malowania bitmapy klatek, która będzie budo­wana na początku, gdy okno zostało utworzone, a potem za każdym razem do malowania nowej bitmapy, która zastąpi poprzednią, gdy zmieni się rozmiar okna.

Bitmapę klatek tworzymy zatem, gdy do procedury okna dotrze komunikat WM_SIZE. Jego obsługę rozpoczy­namy od spraw­dzenia, czy bitmapa klatek istnieje, a jeśli tak, usuwamy ją. Następnie obliczamy współ­rzędne środka obszaru robo­czego okna, długość boku klatek (wysokość bitmapy) odpowie­dnią do jego rozmiaru i promienie gwiazdek, a po uzyskaniu uchwytu kontekstu urzą­dzenia wyświetla­jącego tworzymy kompaty­bilny z nim kontekst urzą­dzenia pamięcio­wego i bitmapę klatek, po czym po zwolnieniu kontekstu urzą­dzenia rzeczy­wistego i wybraniu bitmapy w konte­kście urzą­dzenia pamięcio­wego przystę­pujemy do malowania jej tła i wszyst­kich gwiazdek:

if (klatki != NULL) DeleteObject(klatki);
xs = LOWORD(lParam) / 2;
ys = HIWORD(lParam) / 2;
d = max(9 * min(xs, ys) / 5, 4);
r = (R = d / 2) / 2;
s = (S = 4 * r / 5) / 2;
hdc = GetDC(hWnd);
hdcMem = CreateCompatibleDC(hdc);
klatki = CreateCompatibleBitmap(hdc, NKLA * d, d);
ReleaseDC(hWnd, hdc);
SelectObject(hdcMem, klatki);
SelectObject(hdcMem, hBkBrush);
Rectangle(hdcMem, -1, -1, NKLA * d + 1, d + 1);
SelectObject(hdcMem, hPen);
for (int k = 0; k < NKLA; k++)
{
    SelectObject(hdcMem, hBrush);
    gwiazdka(hdcMem, NRAM, R + k * d, R, R, r, DKAT * k);
    SelectObject(hdcMem, hBkBrush);
    gwiazdka(hdcMem, NRAM, R + k * d, R, S, s, -DKAT * k);
}
DeleteDC(hdcMem);

Bitmapę wypełniamy jednolitym tłem – takim samym, jakie ma obszar roboczy okna, rysując na niej domyślnym piórem i turkusowym pędzlem nieco szerszy z każdej strony prostokąt. Następnie po wybraniu pomarań­czowego pióra rysujemy sekwencję par gwiazdek, większej o żółtym i mniejszej o turku­sowym wypeł­nieniu, przesu­niętych kolejno względem siebie o długość boku klatki i obró­conych o niewielki kąt zależny od liczby ramion gwiazdek i liczby klatek. Przeciwne znaki kątów obrotu gwiazdek większych i mniejszych sprawią, że w trakcie animacji będą się one obracać w odwro­tnych kierun­kach. Na koniec, gdy bitmapa klatek jest przygoto­wana, kontekst urządzenia pamię­ciowego jest niszczony.

Program "Rotacja" w WinAPI C++

W celu pamiętania aktualnego stanu animacji definiujemy w proce­durze okna jeszcze dwie zmienne statyczne typu całko­witego określa­jące numer wyświe­tlanej w danym momencie klatki (od 0 do NKLA–1) i kierunek obrotu większej gwiazdki (1 przeciwnie do ruchu wskazówek zegara, –1 zgodnie):

static int n = 0, dir = 1;

Aktualna klatka jest wyświetlana w obszarze roboczym okna podczas przetwa­rzania komuni­katów WM_PAINTWM_TIMER. Obsługa komuni­katu WM_PAINT rozpoczyna się od udostę­pnienia kontekstu urzą­dzenia wyświetla­jącego, utwo­rzenia kontekstu kompaty­bilnego urzą­dzenia pamięcio­wego i wybrania w nim bitmapy klatek. Następnie wywołana zostaje funkcja BitBlt, która dokonuje transferu pikseli aktualnej klatki w kontekście urzą­dzenia pamięcio­wego do obszaru robo­czego w kontekście urzą­dzenia wyświetla­jącego. Na koniec kontekst urzą­dzenia pamięcio­wego zostaje usunięty, a kontekst urzą­dzenia wyświetla­jącego zwolniony:

case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    hdcMem = CreateCompatibleDC(hdc);
    SelectObject(hdcMem, klatki);
    BitBlt(hdc, xs - R, ys - R, d, d, hdcMem, n * d, 0, SRCCOPY);
    DeleteDC(hdcMem);
    EndPaint(hWnd, &ps);
    return 0;

Gdyby ten komunikat nie był obsługiwany, po urucho­mieniu programu gwiazdki nie byłyby widoczne. Pojawi­łyby się dopiero po urucho­mieniu zegara (polecenie Start). Gdyby następnie zegar został zatrzymany (polecenie Stop) i okno zostało zwinięte, po przywró­ceniu go gwiazdki znowu nie byłyby widoczne.

Obsługa komunikatu WM_TIMER generowanego przez zegar jest niemal taka sama jak komunikatu WM_PAINT. Inny jest sposób udostę­pniania i zwal­niania kontekstu urzą­dzenia pamięcio­wego – za pomocą funkcji GetDCReleaseDC zamiast BeginPaintEndPaint. Ponadto uaktual­niany jest numer klatki przechowy­wany w zmiennej n, by po nadejściu kolejnego komuni­katu zegaro­wego identyfi­kował następną lub poprze­dnią klatkę zależnie od wartości 1 lub –1 zmiennej dir określa­jącej kierunek rotacji:

case WM_TIMER:
    hdc = GetDC(hWnd);
    hdcMem = CreateCompatibleDC(hdc);
    SelectObject(hdcMem, klatki);
    BitBlt(hdc, xs - R, ys - R, d, d, hdcMem, n * d, 0, SRCCOPY);
    DeleteDC(hdcMem);
    ReleaseDC(hWnd, hdc);
    n = (n + dir + NKLA) % NKLA;
    return 0;

Kod źródłowy programu rotacji gwiazdek jest pokazany na poniższym listingu. Program tworzy w funkcji WinMain pędzel koloru turkuso­wego służący do malowania tła okna, który usuwa tuż przed zakoń­czeniem swojego wykonania. Trzy inne obiekty (żółty pędzel, pomarań­czowe pióro i bitmapa klatek), które tworzy w proce­durze okna WndProc, niszczy podczas obsługi komuni­katu WM_DESTROY.

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

const int NRAM = 9;                     // Liczba ramion gwiazdki
const int NKLA = 20;                    // Liczba klatek animacji
const int DKAT = 360 / (NRAM * NKLA);   // Przyrost kąta obrotu
const int TIME = 15;                    // Interwał czasowy (ms)

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

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    static char szClassName[] = "Rotacja";
    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, "Ikona");
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = CreateSolidBrush(RGB(0, 176, 176));
    wc.lpszMenuName  = "Menu";
    wc.lpszClassName = szClassName;

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

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HPEN hPen;
    static HBRUSH hBkBrush, hBrush;
    static HMENU menu;
    static HBITMAP klatki = NULL;
    static int R, r, n = 0, dir = 1, xs, ys, d, S, s;
    HDC hdc, hdcMem;
    PAINTSTRUCT ps;

    switch (message)
    {
        case WM_CREATE:
            hBkBrush = (HBRUSH)GetClassLong(hWnd, GCL_HBRBACKGROUND);
            hPen = CreatePen(PS_SOLID, 2, RGB(255, 165, 0));
            hBrush = CreateSolidBrush(RGB(255, 255, 0));
            menu = GetMenu(hWnd);
            return 0;

        case WM_SIZE:
            if (klatki != NULL) DeleteObject(klatki);
            xs = LOWORD(lParam) / 2;
            ys = HIWORD(lParam) / 2;
            r = (R = (d = max(9 * min(xs, ys) / 5, 4)) / 2) / 2;
            s = (S = 4 * r / 5) / 2;
            hdc = GetDC(hWnd);
            hdcMem = CreateCompatibleDC(hdc);
            klatki = CreateCompatibleBitmap(hdc, NKLA * d, d);
            ReleaseDC(hWnd, hdc);
            SelectObject(hdcMem, klatki);
            SelectObject(hdcMem, hBkBrush);
            Rectangle(hdcMem, -1, -1, NKLA * d + 1, d + 1);
            SelectObject(hdcMem, hPen);
            for (int k = 0; k < NKLA; k++)
            {
                SelectObject(hdcMem, hBrush);
                gwiazdka(hdcMem, NRAM, R + k * d, R, R, r, DKAT * k);
                SelectObject(hdcMem, hBkBrush);
                gwiazdka(hdcMem, NRAM, R + k * d, R, S, s, -DKAT * k);
            }
            DeleteDC(hdcMem);
            return 0;

        case WM_COMMAND:
            switch (wParam)
            {
                case IDM_START:
                    EnableMenuItem(menu, IDM_START, MF_GRAYED);
                    EnableMenuItem(menu, IDM_STOP, MF_ENABLED);
                    DrawMenuBar(hWnd);
                    SetTimer(hWnd, 1, TIME, NULL);
                    return 0;

                case IDM_STOP:
                    EnableMenuItem(menu, IDM_START, MF_ENABLED);
                    EnableMenuItem(menu, IDM_STOP, MF_GRAYED);
                    DrawMenuBar(hWnd);
                    KillTimer(hWnd, 1);
                    return 0;

                case IDM_ODWROC:
                    dir = -dir;
                    return 0;

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

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            hdcMem = CreateCompatibleDC(hdc);
            SelectObject(hdcMem, klatki);
            BitBlt(hdc, xs - R, ys - R, d, d, hdcMem, n * d, 0, SRCCOPY);
            DeleteDC(hdcMem);
            EndPaint(hWnd, &ps);
            return 0;

        case WM_TIMER:
            hdc = GetDC(hWnd);
            hdcMem = CreateCompatibleDC(hdc);
            SelectObject(hdcMem, klatki);
            BitBlt(hdc, xs - R, ys - R, d, d, hdcMem, n * d, 0, SRCCOPY);
            DeleteDC(hdcMem);
            ReleaseDC(hWnd, hdc);
            n = (n + dir + NKLA) % NKLA;
            return 0;

        case WM_CLOSE:
            if (MessageBox(hWnd, "Czy rzeczywiście zakończyć program?", "Koniec",
                           MB_YESNO | MB_ICONQUESTION) == IDYES)
                DestroyWindow(hWnd);
            return 0;

        case WM_DESTROY:
            KillTimer(hWnd, 1);
            DeleteObject(klatki);
            DeleteObject(hPen);
            DeleteObject(hBrush);
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Domyślnie obsługa komunikatu WM_CLOSE polega na wywołaniu funkcji DestroyWindow zamyka­jącej i niszczącej okno oraz wysyła­jącej komunikat WM_DESTROY kończący działanie programu za pomocą funkcji PostQuit­Message. Powyższy program obsłu­guje komunikat WM_CLOSE, wyświe­tlając okno komuni­katu z prośbą o potwier­dzenie, czy program ma być zakoń­czony (rys.). Jeśli użytko­wnik naciśnie przycisk Tak, program zakończy działanie, zaś w przeciwnym razie będzie kontynu­owany.


Opracowanie przykładu: lipiec 2020