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

WinAPI C++

Spacerujący skrzat Podstawowe operacje rastrowe Klasa animacji "Sprite" Bitmapy w zasobach programu Parametry tworzenia okna programu Program "Skrzat" w WinAPI C++ Poprzedni przykład Następny przykład Kontakt

Spacerujący skrzat

Animacja zaprezentowana w programie Rotacja gwiazdki polega na użyciu bitmapy będącej sekwencją prosto­kątnych klatek (ramek) reprezen­tujących kolejne obrazy obraca­jących się gwiazdek i funkcji BitBlt kopiu­jącej klatkę po klatce do obszaru robo­czego okna z określoną częstotli­wością. Bitmapa jest tworzona w programie za pomocą funkcji GDI. Tło bitmapy jest identy­czne z powierz­chnią okna, dzięki czemu wyświe­tlanie każdej klatki sprowadza się do wywołania funkcji BitBlt z kodem najpro­stszej operacji rastrowej SRCCOPY (kopiowanie).

Nieco trudniej jest wyświetlać obiekt o nieprosto­kątnym kształcie na niejedno­litym tle, gdyż w WinAPI nie ma obsługi przezroczy­stości. Klasyczne rozwią­zanie polega na użyciu dwóch bitmap z sekwen­cjami pasują­cych do siebie klatek. Pierwsza bitmapa powinna zawierać klatki z obrazami oryginal­nego obiektu na tle czarnym, druga z obrazami czarnego obiektu o dokła­dnie takim samym kształcie jak obiekt orygi­nalny, ale na tle białym. Klatki pierwszej bitmapy nazywać będziemy mapkami, drugiej maskami, a porusza­jący się obiekt duszkiem (ang. sprite). Wyświe­tlanie każdej klatki wymaga dwukro­tnego wywołania funkcji BitBlt z różnymi kodami operacji rastrowej – pierwszy raz dla maski duszka i kodu SRCAND, drugi raz dla mapki duszka i kodu SRCPAINT.

Innym sposobem byłoby wykorzystanie funkcji TransparentBlt i tylko jednej bitmapy klatek. Z uwagi na jej niedostę­pność we wszystkich środowi­skach i inne słabe strony w programie przedsta­wiającym bajkowego skrzata spaceru­jącego na tle bajko­wego krajo­brazu użyjemy funkcji BitBlt i trzech bitmap:

Wszystkie klatki mają rozmiar 64x96 pikseli. Rozmiar okna zostanie dostoso­wany do rozmiaru bitmapy tła wynoszą­cego 800x480 pikseli. Animacja będzie oparta na zegarze Windows generu­jącym cyklicznie komuni­katy WM_TIMER w odstępie czasu podawanym w milise­kundach, których obsługa będzie polegać na usunięciu bieżącej klatki z obszaru roboczego okna i wyświe­tleniu kolejnej w nowym miejscu. Wydaje się, że interwał czasowy 50 ms jest odpowiedni. Podwójny krok skrzata odtwa­rzany w sekwencji 16 klatek będzie trwał wówczas około 0,8 sekundy (50·16=800 ms), sprawiając wrażenie natural­nego ruchu.

Podstawowe operacje rastrowe

Jak wiadomo, funkcja BitBlt dokonuje transferu bitów prostoką­tnego obszaru bitmapy wybranej w jednym kontekście urzą­dzenia (źródło) do obszaru o takim samym rozmiarze na bitmapie wybranej w drugim kontekście urzą­dzenia (cel). Obraz na ekranie monitora (powierz­chni roboczej okna) jest trakto­wany jak bitmapa przechowy­wania w pamięci karty grafi­cznej. Sposób transferu bitów przez funkcję BitBlt zależy od kodu operacji rastrowej określa­nego w osta­tnim argu­mencie jej wywołania. Spośród 256 możliwych kodów użytecznych jest tylko kilka­naście, im też zostały nadane nazwy. W prostych anima­cjach najczęściej stosuje się (por. opera­tory bitowe):

Kod SRCCOPY oznacza ścisłe kopiowanie prostoką­tnego fragmentu bitmapy źrółowej do bitmapy docelowej, natomiast kody SRCAND, SRCPAINTSRCINVERT oznaczają odpowie­dnio wykony­wanie operacji AND (iloczyn logiczny), OR (suma logiczna) i XOR (różnica symetry­czna) na bitach bitmapy źródłowej i doce­lowej. Poniższy rysunek ilustruje działanie dwukro­tnego wykonania funkcji BitBlt – pierwszy raz z użyciem kodu SRCAND dla maski skrzata (źródło) i kratko­wanego tła (cel), drugi raz z użyciem kodu SRCPAINT dla mapki skrzata (źródło) i tła uzyska­nego w pierwszym kroku (cel). Wynikiem końcowym jest wyświe­tlenie skrzata na kratko­wanym tle.

Wyjaśnijmy, że piksele czarne są reprezentowane wewnę­trznie przez bity 0, piksele białe przez bity 1, a piksele kolorowe przez mieszankę bitów 0 i 1. Zatem wszędzie tam, gdzie maska jest czarna, bity tła zostaną podczas wykony­wania operacji AND wyzero­wane, czyli piksele tła staną się czarne, a tam, gdzie maska jest biała, piksele tła pozostaną niezmie­nione. Następnie wszędzie tam, gdzie mapka jest kolorowa, czarne piksele tła zostaną podczas wykony­wania operacji OR zastąpione pikselami mapki, a tam, gdzie mapka jest czarna, piksele tła pozostaną niezmie­nione. Mówiąc bardziej obrazowo, w pierwszym kroku na bitmapie docelowej zostanie "wypalona czarna dziura" o kształcie duszka, a w drugim kroku kolorowy obraz duszka z mapki trafi podczas wykony­wania operacji OR dokładnie w tę "dziurę", którą wypełni. Zastą­pienie operacji OR w drugim kroku operacją XOR da ten sam efekt końcowy.

Bitmapy masek i mapek skrzata w zapowiedzianym programie animacji zawierają po 16 ustawio­nych poziomo jedna za drugą klatek o rozmiarze 64x96 pikseli. Obie bitmapy mają więc rozmiar 1024x96 pikseli. Jeżeli ich uchwytami są hMaskihMapki, to przy numeracji klatek od 0 do 15 wyświe­tlenie klatki o numerze n w miejscu o współ­rzędnych dxdy obszaru robo­czego okna o uchwycie hWnd możnaby zaprogra­mować następu­jąco:

HDC hdc, hdcMem;
...
hdc = GetDC(hWnd);
hdcMem = CreateCompatibleDC(hdc);
SelectObject(hdcMem, hMaski);
BitBlt(hdc, dx, dy, 64, 96, hdcMem, 64 * n, 0, SRCAND);    // maska -> ekran
SelectObject(hdcMem, hMapki);
BitBlt(hdc, dx, dy, 64, 96, hdcMem, 64 * n, 0, SRCPAINT);  // mapka -> ekran
DeleteDC(hdcMem);
ReleaseDC(hWnd, hdc);

Taki sposób wyświetlania klatki wymaga dwóch kontaktów z ekranem, podczas których dwukrotnie zmieniane są piksele prostoką­tnego fragmentu wyświe­tlanego na ekranie obrazu – najpierw z użyciem kodu SRCAND, a potem SRCPAINT, co może prowadzić do efektu migotania obrazu. Aby go zminima­lizować, lepiej jest obie operacje wykonać na pomocni­czej bitmapie wybieranej w dodatkowym kontekście urzą­dzenia pamięcio­wego, na którą najpierw należy skopiować stosowny fragment ekranu, a na koniec po przetwo­rzeniu skopiować ją na ekran:

HDC hdc, hdcMem, hdcTmp;
HBITMAP hbmpTmp;
...
hdc = GetDC(hWnd);
hbmpTmp = CreateCompatibleBitmap(hdc, 64, 96);             // pomocnicza bitmapa
hdcTmp = CreateCompatibleDC(hdc);                          // dodatkowy kontekst
SelectObject(hdcTmp, hbmpTmp);
BitBlt(hdcTmp, 0, 0, 64, 96, hdc, dx, dy, SRCCOPY);        // ekran -> bitmapa
hdcMem = CreateCompatibleDC(hdc);
SelectObject(hdcMem, hMaski);
BitBlt(hdcTmp, 0, 0, 64, 96, hdcMem, 64 * n, 0, SRCAND);   // maska -> bitmapa
SelectObject(hdcMem, hMapki);
BitBlt(hdcTmp, 0, 0, 64, 96, hdcMem, 64 * n, 0, SRCPAINT); // mapka -> bitmapa
BitBlt(hdc, dx, dy, 64, 96, hdcTmp, 0, 0, SRCCOPY);        // bitmapa -> ekran
DeleteDC(hdcMem);
DeleteDC(hdcTmp);
DeleteObject(hbmpTmp);
ReleaseDC(hWnd, hdc);

Klasa animacji "Sprite"

Proces tworzenia animacji polegającej na wyświetlaniu małych bitmap (klatek) przedsta­wiających przesuwa­jącego się lub przesuwa­nego na ekranie duszka można usprawnić poprzez zdefinio­wanie typu obiekto­wego – klasy w języku C++. Dysponując taką klasą animacji, wystarczy dla każdego duszka, który ma występować w programie, utworzyć obiekt tej klasy lub obiekt klasy pochodnej i manipu­lować nim za pomocą jej funkcji (metod) składowych. Obraz duszka może się zmieniać w czasie (szereg klatek), jak np. w przy­padku skrzata, który przechodząc od jednego do drugiego brzegu okna przebiera nóżkami i rączkami, bądź może być stały (jedna klatka), jak w przy­padku karty przecią­ganej myszką podczas układania pasjansa. Przy projekto­waniu klasy duszka wypada uwzględnić oba przypadki, a wtedy będzie ona w miarę ogólna i użyteczna w wielu programach.

Jest oczywiste, że pola klasy powinny określać wygląd duszka (bitmapy mapek i masek oraz aktualną klatkę), jego szerokość i wysokość oraz bieżącą pozycję na ekranie (obszarze roboczym okna), zaś metody umożliwiać tworzenie i ew. niszczenie obiektu reprezen­tującego duszka, wyświe­tlanie go i ukry­wanie oraz przesu­wanie. Zadanie to można rozwiązać na wiele sposobów, wypada więc nadmienić, że podana poniżej propo­zycja jest ze względu na przejrzy­stość niezbyt efektywna, ale w prostszych zastoso­waniach wystar­czająca. Plik nagłów­kowy sprite.h modułu definiu­jącego klasę animacji o nazwie Sprite ma postać:

// Sprite - klasa animacji duszka
// ------------------------------

#ifndef H_SPRITE
#define H_SPRITE

#include <windows.h>

class Sprite
{
protected:
    HBITMAP hMapki, hMaski;     // Mapki i maski duszka
    HBITMAP hSave;              // Tło zakryte przez duszka
    int cx, cy;                 // Szerokość i wysokość klatki
    int xKla;                   // Współrzędna x bieżącej klatki
    int xMax;                   // Współrzędna x ostatniej klatki
    int dx, dy;                 // Pozycja duszka na ekranie
    bool wid;                   // Widoczny (true), ukryty (true)
public:
    Sprite(HBITMAP Mapki, HBITMAP Maski, int n = 1, int x = 0, int y = 0);
    virtual ~Sprite();
    virtual void Show(HDC hdc);                 // Pokaż duszka
    virtual void Hide(HDC hdc);                 // Ukryj duszka
    virtual void Move(HDC hdc, int x, int y);   // Przesuń duszka
    bool Contains(int x, int y);                // Zawiera punkt (x, y)
    int Width()     { return cx; }              // Szerokość klatki duszka
    int Height()    { return cy; }              // Wysokość klatki duszka
    int X()         { return dx; }              // Aktualna współrzędna x duszka
    int Y()         { return dy; }              // Aktualna współrzędna y duszka
    bool Visible()  { return wid; }             // Duszek widoczny (true, false)
};

#endif // H_SPRITE

Wszystkie pola klasy Sprite są zadeklarowane jako chronione (specyfi­kator protected), by w razie potrzeby był do nich dostęp w klasach pochodnych. Natomiast wszystkie metody są publiczne (specyfi­kator public), są więc dostępne z dowolnego miejsca programu w zasięgu wido­czności obiektu tej klasy. Trzy argu­menty konstru­ktora (liczba klatek i wstępna pozycja duszka) są domyślne. Gdy liczba klatek nie jest podana jawnie, przyjęta wartość domyślna 1 oznacza, że obraz duszka nie zmienia się w czasie. Metody wirtualne (wyróżnione słowem kluczowym virtual) można redefi­niować w klasach pochodnych, określając ich inne dzia­łanie niż w klasie bazowej. Bardzo krótki kod pięciu ostatnich metod udostępnia­jących cechy obiektu został od razu umieszczony w defi­nicji klasy. Jest to wskazówką dla kompila­tora, by traktował je jako tzw. metody inline, zastępując wywołania tych metod ich prostym kodem. Druga część modułu zawarta w pliku sprite.cpp stanowi implemen­tację pozosta­łych metod klasy:

#include "sprite.h"

Sprite::Sprite(HBITMAP Mapki, HBITMAP Maski, int n, int x, int y)
{
    HDC hdc;
    BITMAP bitmap;
    GetObject(Mapki, sizeof(BITMAP), &bitmap);
    if (n <= 0) n = 1;
    cx = bitmap.bmWidth / n;
    cy = bitmap.bmHeight;
    xKla = 0;
    xMax = (n - 1) * cx;
    dx = x;
    dy = y;
    hMapki = Mapki;
    hMaski = Maski;
    hdc = GetDC(NULL);
    hSave = CreateCompatibleBitmap(hdc, cx, cy);
    ReleaseDC(NULL, hdc);
    wid = false;
}

Sprite::~Sprite()
{
    if (hSave) DeleteObject(hSave);
}

void Sprite::Show(HDC hdc)
{
    HDC hdcMem, hdcTmp;
    HBITMAP hbmpTmp = CreateCompatibleBitmap(hdc, cx, cy);
    hdcTmp = CreateCompatibleDC(hdc);
    SelectObject(hdcTmp, hbmpTmp);
    BitBlt(hdcTmp, 0, 0, cx, cy, hdc, dx, dy, SRCCOPY);
    hdcMem = CreateCompatibleDC(hdc);
    SelectObject(hdcMem, hSave);
    BitBlt(hdcMem, 0, 0, cx, cy, hdcTmp, 0, 0, SRCCOPY);
    SelectObject(hdcMem, hMaski);
    BitBlt(hdcTmp, 0, 0, cx, cy, hdcMem, xKla, 0, SRCAND);
    SelectObject(hdcMem, hMapki);
    BitBlt(hdcTmp, 0, 0, cx, cy, hdcMem, xKla, 0, SRCPAINT);
    BitBlt(hdc, dx, dy, cx, cy, hdcTmp, 0, 0, SRCCOPY);
    DeleteDC(hdcMem);
    DeleteDC(hdcTmp);
    DeleteObject(hbmpTmp);
    wid = true;
}

void Sprite::Hide(HDC hdc)
{
    HDC hdcMem = CreateCompatibleDC(hdc);
    SelectObject(hdcMem, hSave);
    BitBlt(hdc, dx, dy, cx, cy, hdcMem, 0, 0, SRCCOPY);
    DeleteDC(hdcMem);
    wid = false;
}

void Sprite::Move(HDC hdc, int x, int y)
{
    HBITMAP hbmpTmp;
    HDC hdcMem, hdcTmp;
    RECT RectOld, RectNew, RectAll;
    if (wid)
    {
        SetRect(&RectOld, dx, dy, dx + cx, dy + cy);
        SetRect(&RectNew, x, y, x + cx, y + cy);
        UnionRect(&RectAll, &RectOld, &RectNew);
        hbmpTmp = CreateCompatibleBitmap(hdc,
                  RectAll.right - RectAll.left, RectAll.bottom - RectAll.top);
        hdcTmp = CreateCompatibleDC(hdc);
        SelectObject(hdcTmp, hbmpTmp);
        BitBlt(hdcTmp, 0, 0,
                  RectAll.right - RectAll.left, RectAll.bottom - RectAll.top,
                  hdc, RectAll.left, RectAll.top, SRCCOPY);
        hdcMem = CreateCompatibleDC(hdc);
        SelectObject(hdcMem, hSave);
        BitBlt(hdcTmp, dx - RectAll.left, dy - RectAll.top, cx, cy,
                  hdcMem, 0, 0, SRCCOPY);
        BitBlt(hdcMem, 0, 0, cx, cy,
                  hdcTmp, x - RectAll.left, y - RectAll.top, SRCCOPY);
        xKla = (xKla < xMax) ? xKla + cx : 0;
        SelectObject(hdcMem, hMaski);
        BitBlt(hdcTmp, x - RectAll.left, y - RectAll.top, cx, cy,
                  hdcMem, xKla, 0, SRCAND);
        SelectObject(hdcMem, hMapki);
        BitBlt(hdcTmp, x - RectAll.left, y - RectAll.top, cx, cy,
                  hdcMem, xKla, 0, SRCPAINT);
        BitBlt(hdc, RectAll.left, RectAll.top,
                  RectAll.right - RectAll.left, RectAll.bottom - RectAll.top,
                  hdcTmp, 0, 0, SRCCOPY);
        DeleteObject(hdcMem);
        DeleteObject(hdcTmp);
        DeleteObject(hbmpTmp);
    }
    dx = x;
    dy = y;
}

bool Sprite::Contains(int x, int y)
{
    return (x >= dx) && (x < dx + cx) && (y >= dy) && (y < dy + cy);
}

Konstruktor klasy Sprite inicjalizuje wszystkie pola tworzo­nego obiektu. W celu obliczenia rozmiaru klatek wywołuje funkcję GetObject, która wypełnia strukturę BITMAP danymi dotyczą­cymi bitmapy klatek. Pola bmWidthbmHeight tej struktury określają szerokość i wysokość bitmapy. Liczba klatek jest w razie potrzeby korygowana. Po wyzna­czeniu współrzę­dnej x pierwszej i ostatniej klatki, zapamię­taniu pozycji duszka oraz uchwytów bitmap mapek i masek konstru­ktor tworzy dodatkową bitmapę hSave potrzebną do odtwa­rzania tła zasłania­nego przez duszka. Każdora­zowo, gdy duszek ma być wyświe­tlony, odpowiedni fragment obszaru roboczego okna zostanie w niej zapamię­tany, a gdy duszek ma być przesu­nięty w inne miejsce, zasłonięty przez niego fragment obszaru zostanie na jej podstawie odtworzony. Wskaźnik NULL w wywołaniu funkcji GetDCReleaseDC oznacza, że udostę­pniany i zwal­niany kontekst urządzenia dotyczy ekranu monitora, nie obszaru roboczego okna. Utworzony duszek jest wstępnie niewi­doczny.

Rolą destruktora ~Sprite jest zniszczenie bitmapy utworzonej przez konstruktor. Operacja ta jest możliwa, gdy wartość hSave jest niezerowa (różna od NULL). Zauważmy, że destruktor nie niszczy bitmap hMapkihMaski, gdyż są one tworzone poza klasą Sprite, więc to nie ona powinna zwalniać przydzie­loną im pamięć.

Metoda Show wyświetla duszka na powierzchni roboczej okna, używając pomocni­czej bitmapy o rozmiarze jednej klatki i dwóch kontekstów pamięcio­wych, by zminima­lizować efekt migotania obrazu. Łącznie wykonuje pięć operacji rastro­wych, z których tylko ostatnia zapisuje obraz na ekranie. Działanie metody można opisać następu­jąco:

O wiele prostsza jest metoda Hide ukrywająca duszka, która odtwarza zapamię­tany na bitmapie hSave fragment tła zakryty przez duszka. Wprost przeciwnie przedstawia się sytuacja w przy­padku metody Move przesuwa­jącej duszka z jednego miejsca ekranu do drugiego. Metoda wykonuje łącznie sześć operacji rastrowych i podobnie jak Show używa dwóch kontekstów pamię­ciowych i pomocni­czej bitmapy. Obraz, jaki ma się pojawić na ekranie po wyko­naniu kolejnego kroku animacji, jest przygoto­wywany na tej bitmapie, ale jej rozmiar jest większy, gdyż ma ona kształt najmniej­szego prosto­kąta (nazwanego RectAll) obejmu­jącego dwie klatki duszka – przed i po przesu­nięciu go (prosto­kąty RectOldRectNew). Pracę z prosto­kątami ułatwiają funkcje SetRectUnionRect. Pierwsza ustawia cztery pola struktury typu RECT, a druga wyznacza najmniejszy prostokąt zawiera­jący dwa dane prosto­kąty. Metoda Move realizuje następu­jące operacje:

Oczywiście w przypadku, gdy duszek nie widnieje na ekranie, działanie metody Move ogranicza się do uaktual­nienia współ­rzędnych jego pozycji. Prosta metoda Contains sprawdza, czy prostokąt duszka zawiera punkt o podanych współ­rzędnych i zależnie od spełnienia tego warunku zwraca wartość true lub false.

Bitmapy w zasobach programu

Dodanie bitmapy do zasobów programu jest równie proste jak dodanie ikony – polega na podaniu w skrypcie zasobów odwołania BITMAP do pliku grafi­cznego w formacie BMP zawiera­jącego bitmapę. W przedsta­wionym poniżej skrypcie zasobów występują odwołania do trzech bitmap znajdu­jących się w plikach Pejzaz.bmp, Mapki.bmpMaski.bmp (plik nagłów­kowy resource.h zawiera jedynie numery ID trzech elementów menu).

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

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

Tlo   BITMAP "Pejzaz.bmp"
Mapki BITMAP "Mapki.bmp"
Maski BITMAP "Maski.bmp"

Ikona ICON "Skrzat.ico"

W gruncie rzeczy bitmapy masek można nie umieszczać w pliku graficznym i zasobach programu, lecz gene­rować ją na podstawie bitmapy mapek bezpośrednio po załado­waniu do programu lub w konstru­ktorze klasy Sprite (stosowne podpro­gramy można znaleźć w zasobach interne­towych). Ładowanie bitmapy z zasobów programu umożliwia funkcja LoadBitmap, która podobnie jak LoadIcon wymaga podania uchwytu programu i nazwy zasobu, np.:

HBITMAP hTlo;
...
hTlo = LoadBitmap(hInstance, "Tlo");

Uwaga. Kompilator Borland 5.5 sygnalizuje błąd linkera świadczący o przekro­czeniu limitu udostępnionej mu pamięci podczas kompilacji końcowej wersji programu (rys.). Kłopotu udaje się uniknąć, konwer­tując 24-bitową bitmapę Pejzaz.bmp na 8-bitową.

Inne rozwiązanie polega na usunięciu ze skryptu odwołania do bitmapy Pejzaz.bmp sprawia­jącej problem i łado­wania jej z odrę­bnego pliku zamiast z zasobów:

hTlo = (HBITMAP)LoadImage(NULL, "Pejzaz.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);

Rzecz jasna nazwę pliku należy poprzedzić ścieżką dostępu, gdy znajduje się on w innym folderze niż plik wykona­wczy programu. Stała IMAGE_BITMAP oznacza bitmapę (zamiast niej może wystąpić IMAGE_CURSOR lub IMAGE_ICON), zaś LR_LOAD­FROMFILE wczyty­wanie jej z pliku. Argu­menty czwarty i piąty określają szerokość i wysokość obrazu. Gdy są zerami, przyjmo­wane są rzeczy­wiste rozmiary podane w pliku.

Parametry tworzenia okna programu

Okno programu przedstawiającego spacerują­cego skrzata ma mieć stały rozmiar, a cały jego obszar roboczy ma być wypeł­niony bitmapą Pejzaz.bmp stanowiącą tło animacji. Rozmiar tego obszaru można uzyskać w polach bmWidth (szerokość) i bmHeight (wysokość) struktury BITMAP, wywo­łując funkcję GetObject po załado­waniu bitmapy tła. Funkcja CreateWindow wymaga jednak podania rozmiaru tworzo­nego okna, nie obszaru roboczego. Na szczęście istnieje funkcja Adjust­WindowRect, która na podstawie żądanego obszaru roboczego wyznacza prostokąt okna. Jej pierwszym argu­mentem jest wskaźnik na strukturę RECT określa­jącą prostokąt obszaru roboczego, drugim styl okna, a trzecim wartość typu BOOL informu­jąca, czy okno ma menu:

static HBITMAP hTlo = NULL;
BITMAP bitmap;
RECT rect;
...
hTlo = LoadBitmap(hInstance, "Tlo");
GetObject(hTlo, sizeof(BITMAP), &bitmap);
rect.left = rect.top = 0;
rect.right = bitmap.bmWidth;
rect.bottom = bitmap.bmHeight;
AdjustWindowRect(&rect, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, TRUE);

Styl okna określają stałe WS_OVERLAPPED (można go nakładać na inne okna), WS_CAPTION (ma pasek tytułowy i ramkę), WS_SYSMENU (ma menu systemowe) i WS_MINIMI­ZEBOX (ma przycisk minimali­zacji). Funkcja wypełnia strukturę RECT współrzę­dnymi lewego górnego i prawego dolnego rogu okna. Uchwyt bitmapy Pejzaz.bmp ładowanej przed zarejestro­waniem klasy okna i utwo­rzeniem go jest potrzebny również w proce­durze okna do malowania tła. Można go oczywiście zapamiętać w zmiennej globalnej, ale lepiej przekazać go do procedury okna w niewykorzy­stywanym w poprze­dnich programach ostatnim argu­mencie wywołania funkcji CreateWindow:

static char szClassName[] = "Skrzat";
...
hwnd = CreateWindow(szClassName, "Spacerujący skrzat",
            WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
            CW_USEDEFAULT, CW_USEDEFAULT,
            rect.right - rect.left, rect.bottom - rect.top,
            NULL, NULL, hInst, hTlo);

Funkcja umieszcza wartość tego argumentu w polu lpCreateParams struktury CREATESTRUCT. Wskaźnik na tę strukturę typu LPCREATESTRUCT jest dostępny jako wartość argumentu lParam typu LPARAM procedury okna podczas obsługi komuni­katu WM_CREATE. Do przeka­zanego w ten sposób uchwytu bitmapy tła można się więc dostać podobnie jak do uchwytu programu:

static HINSTANCE hInstance;
static HBITMAP hTlo = NULL;
...
case WM_CREATE:
    hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
    hTlo = (HBITMAP)(((LPCREATESTRUCT)lParam)->lpCreateParams);
    ...
    return 0;

Program "Skrzat" w WinAPI C++

Kod źródłowy programu wyświetlającego spaceru­jącego skrzata jest przedsta­wiony na poniższym listingu. W programie zdefinio­wane są trzy stałe globalne określa­jące liczbę klatek animacji, wielkość przesu­nięcia jednostko­wego skrzata i odstęp czasowy genero­wania komuni­katów zegarowych. Program ładuje na początku funkcji WinMain bitmapę służącą do wyzna­czenia rozmiaru okna i malowania tła, a usuwa po zakoń­czeniu pętli komuni­katów. Dwie inne bitmapy (klatki mapek i masek skrzata) ładuje w proce­durze okna WndProc podczas przetwa­rzania komuni­katu WM_CREATE, a niszczy podczas przetwa­rzania komuni­katu WM_DESTROY. Tuż po załado­waniu bitmap klatek program tworzy obiekt dynami­czny klasy Sprite reprezen­tujący skrzata, a na koniec przed ich zniszcze­niem usuwa go.

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

const int NKLA = 16,        // Liczba klatek
          DX   = 4,         // Przesunięcie skrzata
          TIME = 50;        // Interwał czasowy (ms)

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

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow)
{
    static char szClassName[] = "Skrzat";
    static HBITMAP hTlo = NULL;
    HWND hWnd;
    MSG msg;
    WNDCLASS wc;
    BITMAP bitmap;
    RECT rect;

    hTlo = LoadBitmap(hInst, "Tlo");
    GetObject(hTlo, sizeof(BITMAP), &bitmap);
    rect.left = rect.top = 0;
    rect.right = bitmap.bmWidth;
    rect.bottom = bitmap.bmHeight;
    AdjustWindowRect(&rect, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, TRUE);

    wc.style         = 0;
    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 = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = "Menu";
    wc.lpszClassName = szClassName;

    if (!RegisterClass(&wc))
    {
        MessageBox(NULL, "Ten program wymaga Win32!", szClassName, MB_ICONERROR);
        DeleteObject(hTlo);
        return 0;
    }
    hwnd = CreateWindow(szClassName, "Spacerujący skrzat",
                WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                CW_USEDEFAULT, CW_USEDEFAULT,
                rect.right - rect.left, rect.bottom - rect.top,
                NULL, NULL, hInst, hTlo);
    ShowWindow(hwnd, iCmdShow);
    UpdateWindow(hwnd);
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    DeleteObject(hTlo);
    return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    static HMENU menu;
    static HBITMAP hTlo = NULL, hMapki = NULL, hMaski = NULL;
    static int cx, cy;
    static Sprite *skrzat = NULL;
    HDC hdc, hdcMem;
    PAINTSTRUCT ps;

    switch (message)
    {
        case WM_CREATE:
            hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
            hTlo = (HBITMAP)(((LPCREATESTRUCT)lParam)->lpCreateParams);
            hMapki = LoadBitmap(hInstance, "Mapki");
            hMaski = LoadBitmap(hInstance, "Maski");
            skrzat = new Sprite(hMapki, hMaski, NKLA);
            menu = GetMenu(hWnd);
            return 0;

        case WM_SIZE:
            cx = LOWORD(lParam);
            cy = HIWORD(lParam);
            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_KONIEC:
                    SendMessage(hWnd, WM_CLOSE, 0, 0);
                    return 0;
            }
            break;

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);
            hdcMem = CreateCompatibleDC(hdc);
            SelectObject(hdcMem, hTlo);
            BitBlt(hdc, 0, 0, cx, cy, hdcMem, 0, 0, SRCCOPY);
            DeleteDC(hdcMem);
            if (!skrzat->Visible())
                skrzat->Move(hdc, cx, 6 * cy / 11);
            skrzat->Show(hdc);
            EndPaint(hWnd, &ps);
            return 0;

        case WM_TIMER:
            hdc = GetDC(hWnd);
            if (skrzat->X() + skrzat->Width() > 0)
                skrzat->Move(hdc, skrzat->X() - DX, skrzat->Y());
            else
                skrzat->Move(hdc, cx, skrzat->Y());
            ReleaseDC(hWnd, hdc);
            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);
            if (skrzat) delete skrzat;
            DeleteObject(hMaski);
            DeleteObject(hMapki);
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

Zadaniem procedury okna po nadejściu komunikatu WM_PAINT jest wypełnienie całego obszaru robo­czego okna bitmapą przedsta­wiającą pejzaż i wyświe­tlenie skrzata za pomocą metody Show klasy Sprite. Gdy jest to pierwszy komunikat, który dotarł do proce­dury po utwo­rzeniu okna, wstępnie niewi­dzialny skrzat jest przed wyświe­tleniem ustawiany tuż za prawym brzegiem obszaru roboczego w odle­głości 6/11 jego wysokości od góry, nadal go więc w oknie nie widać. Pojawi się dopiero po urucho­mieniu animacji polece­niem Start. Animację zatrzymuje polecenie Stop.

Obsługa komunikatu WM_TIMER generowanego przez zegar polega na przesu­nięciu skrzata za pomocą metody Move klasy Sprite. Jeżeli obiekt nie zniknął po lewej stronie obszaru robo­czego okna, zostaje przesu­nięty o 4 piksele w lewo, zaś w przeci­wnym razie zostaje jak w przy­padku pierw­szego komuni­katu WM_PAINT ustawiony tuż za prawym brzegiem obszaru robo­czego, by ponownie odbył wędrówkę w poprzek okna.

Wynik wykonania programu uzyskany w chwilę po uruchomieniu animacji przedstawia poniższy rysunek. Warto zwrócić uwagę, że zaprezen­towane okno jest nieco wyższe od okna analogi­cznego programu w C#, ponieważ w WinAPI C++ pasek menu głównego nie zajmuje górnego fragmentu obszaru roboczego okna, lecz mieści się w obszarze systemowym okna podobnie jak pasek tytułowy i ramka.


Opracowanie przykładu: lipiec/sierpień 2020