Animacja w WinAPI
Czas tchnąć trochę życia w szary świat Okien. Na początek zrobimy sobie kulkę, która lata po całym oknie. Będziemy potrzebować kilku zmiennych, w których zapamiętamy: aktualną pozycję kulki (
KulkaX,
KulkaY) oraz prędkość kulki, czyli liczbę pikseli, o jaką się ona przesuwa w każdej klatce. Prędkość rozdzielimy sobie na prędkość w poziomie i prędkość w pionie, zaraz się okaże po co.
WORD KulkaX = 100, KulkaY = 100;
SHORT SpeedX = 2, SpeedY = 2;
Potrzebujemy jeszcze globalnego uchwytu do głównego okna, coby dało się z niego skorzystać w funkcji rysującej kulkę. Przyda się też struktura typu
RECT, do której pobierzemy wymiary naszego okna, korzystając z funkcji
GetClientRect:
HWND g_hwnd;
g_hwnd = hwnd;
RECT rcOkno;
GetClientRect( g_hwnd, & rcOkno );
Następnie robimy nowy uchwyt do bitmapy i wczytujemy bitmapę z naszą kulką z dysku, po czym pobieramy jej wymiary funkcją
GetObject:
HBITMAP hbmKulka;
hbmKulka =( HBITMAP ) LoadImage( NULL, "kulka.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE );
BITMAP bmKulka;
GetObject( hbmKulka, sizeof( bmKulka ), & bmKulka );
Żeby zrobić użytek z bitmapy, musimy ją przypisać do jakiegoś kompatybilnego
HDC. Tworzymy je funkcją
CreateCompatibleDC, jako parametr podając
HDC naszego okna. Tworzymy też uchwyt do domyślnej bitmapy nowoutworzonego kontekstu, żeby nam się żadne zasoby systemowe nie wymknęły spod kontroli:
HDC hdc = GetDC( g_hwnd );
HDC hdcMem = CreateCompatibleDC( hdc );
HBITMAP hbmOld =( HBITMAP ) SelectObject( hdcMem, hbmKulka );
ReleaseDC( g_hwnd, hdc );
Wszystkie powyższe deklaracje zmiennych (może oprócz
hdc, która w tym akurat miejscu pełni tylko rolę pomocniczą) umieszczamy w globalnym zasięgu, ponieważ musimy mieć do nich dostęp z funkcji rysującej kulkę, którą zaraz sobie utworzymy.
Narysować kulkę to żadna sztuka, ale żeby raczyła ona ruszyć zadek z miejsca, musimy mieć jakieś urządzenie odmierzające czas. W programistycznym żargonie nazywa się takie urządzenie '''timerem'''. WinAPI posiada szereg rozwiązań dla timerów, jedno gorsze od drugiego ;-). I na razie zajmiemy się właśnie tym najgorszym, ale za to bardzo prostym, do naszych celów w zupełności wystarczającym.
Każdy timer (bo możemy ich mieć w programie wiele) powinien mieć swój numer identyfikacyjny; nadajemy go sami. Najlepiej po prostu dawać kolejne liczby:
Teraz możemy już stworzyć timera funkcją
SetTimer. Timer jest zwykle skojarzony z konkretnym okienkiem, dlatego też podajemy uchwyt tego okienka jako argument dla funkcji
SetTimer. Drugim argumentem jest numer identyfikacyjny, trzecim - tzw. '''interwał''' (o nim zaraz), ostatni argument najbezczelniej w świecie olewamy:
if( SetTimer( hwnd, ID_TIMER, 100, NULL ) == 0 )
MessageBox( hwnd, "Nie można utworzyć timera!", "Kurde", MB_ICONSTOP );
Od tej pory nasz timer odmierza czas... Co
100 milisekund (to jest właśnie ów interwał, o którym wcześniej wspomniałem) do okienka o uchwycie
hwnd jest wysyłany komunikat
WM_TIMER. Wystarczy dodać obsługę tego komunikatu i już mamy (prawie) gotową animację:
case WM_TIMER:
RysujKulke();
break;
Jeszcze tylko wyczarować funkcję
RysujKulke i kulka skacze sobie radośnie po całym okienku:
void RysujKulke()
{
if( KulkaX <= 0 || KulkaX >= ptOkno.x ) SpeedX = - SpeedX;
if( KulkaY <= 0 || KulkaY >= ptOkno.y ) SpeedY = - SpeedY;
KulkaX += SpeedX;
KulkaY += SpeedY;
HDC hdc = GetDC( hwnd );
BitBlt( hdc, KulkaX, KulkaY, bmKulka.bmWidth, bmKulka.bmHeight, hdcMem, 0, 0, SRCCOPY );
ReleaseDC( hwnd, hdc );
}
W momencie, kiedy chcemy już zatrzymać naszą animację albo po prostu kończymy wykonywanie programu, musimy zniszczyć niepotrzebnego timera:
case WM_DESTROY:
{
KillTimer( hwnd, ID_TIMER );
PostQuitMessage( 0 );
}
break;
Pamiętamy również o zwolnieniu pozostałych zasobów, tzn. zniszczeniu pomocniczego kontekstu
hdcMem, przywracając mu przedtem jego domyślną bitmapę
hbmOld.
"Cywilizujemy" naszą animację
A więc kulka lata po oknie i nawet zostawia za sobą piękny ślad... No właśnie. Zapomnieliśmy, że przed narysowaniem każdej nowej klatki animacji trzeba zetrzeć starą! A w dodatku chcielibyśmy, aby tło bitmapy z kulką było przezroczyste... To pierwsze to żaden problem, możemy zamazać "starą" klatkę funkcją
FillRect. Funkcja ta pobiera współrzędne prostokąta, który zamazujemy, tak więc rezygnujemy z naszych zmiennych
KulkaX i
KulkaY, a współrzędne kulki (i jej wymiary, przy okazji) przechowywać sobie będziemy w strukturze typu
RECT:
RECT rcKulka;
SetRect( & rcKulka, 5, 5, 5 + bmKulka.bmWidth, 5 + bmKulka.bmHeight );
Przy okazji dowiedzieliśmy się, jak ustawić wszystkie współrzędne prostokąta
RECT za jednym zamachem. Oczywiście wywołanie funkcji
SetRect musi się znaleźć ZA wywołaniem funkcji
GetObject, która pobiera nam wymiary bitmapy z kulką, inaczej zmienne
bmKulka.bmWidth i
bmKulka.bmHeight będą zawierać nieprawidłowe wartości.
Struktura typu
RECT posiada cztery pola:
left, right, top, bottom. Tak więc musimy w naszym programie pozamieniać wszystkie
KulkaX na
rcKulka.left, wszystkie
KulkaY na
rcKulka.top, natomiast w miejscach, gdzie używamy szerokości i wysokości bitmapy piłki, wstawiamy odpowiednio:
bmKulka.right i
bmKulka.bottom (trochę "nielegalnie", o tym zaraz). Poza tym pola
rcKulka.left i
rcKulka.top będą się musiały zmieniać z każdą klatką animacji. To zmienianie możemy sobie znowu załatwić, korzystając z funkcji
OffsetRect, która modyfikuje właśnie pola
left i
top wskazanego prostokąta. Czyli nasza nowa funkcja
RysujKulke będzie mniej więcej taka:
void RysujKulke()
{
HDC hdc = GetDC( g_hwnd );
FillRect( hdc, & rcTemp,( HBRUSH ) GetStockObject( LTGRAY_BRUSH ) );
if( rcKulka.left <= 0 || rcKulka.right >= rcOkno.right ) SpeedX = - SpeedX;
if( rcKulka.top <= 0 || rcKulka.bottom >= rcOkno.bottom ) SpeedY = - SpeedY;
OffsetRect( & rcKulka, SpeedX, SpeedY );
BitBlt( hdc, rcKulka.left, rcKulka.top, rcKulka.right - rcKulka.left,
rcKulka.bottom - rcKulka.top, hdcMem, 0, 0, SRCCOPY );
ReleaseDC( g_hwnd, hdc );
}
Jak widać, użyliśmy funkcji
GetStockObject, aby pobrać systemowy pędzel o kolorze szarym - takiego samego używa kod generowany przez Dev-C++ aby zamalować główne okno programu. Musieliśmy użyć jawnej konwersji na typ
HBRUSH, ponieważ
GetStockObject zwraca uchwyty różnego rodzaju, nie tylko
HBRUSH (niektóre kompilatory ten fakt olewają, te używane z Dev-C++ niestety nie).
No to pierwszy problem z głowy, czas zająć się przezroczystością tła. W części poświęconej bitmapom podałem wam przepis na funkcję
CreateBitmapMask, która tworzy maskę opierając się o daną bitmapę i kolor maski. Wykorzystamy teraz tę funkcję, aby nie trzeba było robić maski "ręcznie":
HBITMAP hbmMaska = CreateBitmapMask( hbmKulka, RGB( 255, 0, 255 ) );
Jako kolor maski podaliśmy
RGB(255,0,255), czyli "magiczny róż", ponieważ tego właśnie koloru używa się najczęściej w celu maskowania. Teraz musimy znów wprowadzić zmiany do funkcji
RysujKulke, a mianowicie dorzucić do niej jedną instrukcję
BitBlt oraz zmienić nieco poprzednią - jeśli nie wiesz dlaczego, zajrzyj do odcinka poświęconego rysowaniu bitmap. Ponieważ zaś stworzony przez nas pomocniczy kontekst
hdcMem będzie teraz musiał "obsłużyć" dwie bitmapy (właściwą kulkę i maskę) zamiast jednej, więc albo będziemy przypisywać do niego jedną bitmapę, a raz drugą, albo też utworzyć dla maski osobny kontekst. Wybieramy to pierwsze rozwiązanie, choć wolniejsze jest, ponieważ nie chcemy się pogubić w aż tylu kontekstach :-). Tak będzie teraz wyglądała funkcja
RysujKulke (podaję tylko część, która się zmieniła, między komentarzem "narysuj na nowej pozycji" a wywołaniem
ReleaseDC):
SelectObject( hdcMem, hbmMaska );
BitBlt( hdc, rcKulka.left, rcKulka.top, rcKulka.right - rcKulka.left,
rcKulka.bottom - rcKulka.top, hdcMem, 0, 0, SRCAND );
SelectObject( hdcMem, hbmKulka );
BitBlt( hdc, rcKulka.left, rcKulka.top, rcKulka.right - rcKulka.left,
rcKulka.bottom - rcKulka.top, hdcMem, 0, 0, SRCINVERT );
To już w zasadzie wszystko, ale zanim uruchomimy kompilator, by podziwiać efekty, jeszcze grzecznie po sobie posprzątamy, żeby później nie zapomnieć ;-). Musimy zwrócić kontekstowi
hdcMem jego bitmapę domyślną (przechowywaną w
hbmOld), usunąć ten kontekst, skasować bitmapy z kulką i z maską, no i oczywiście "zabić" timera. A więc obsługa komunikatu
WM_DESTROY powinna wyglądać mniej więcej tak:
case WM_DESTROY:
{
KillTimer( hwnd, ID_TIMER );
SelectObject( hdcMem, hbmOld );
DeleteDC( hdcMem );
DeleteObject( hbmKulka );
DeleteObject( hbmMaska );
PostQuitMessage( 0 );
}
break;
Podwójne buforowanie
Kulka zasuwa po ekranie, ale robi to z widoczną niechęcią, wręcz z obrzydzeniem; animacja co trochę paskudnie miga i w ogóle nie wygląda zachęcająco. Trzeba zrobić podwójne buforowanie! Brzmi groźnie, ale idea jest bardzo prosta. Do tej pory najpierw rysowaliśmy na ekranie szary prostokąt, potem maskę, która również trafiała prosto na ekran, a na tym dopiero "właściwą" kulkę, która lądowała na ekranie w ułamek sekundy później. W ten sposób czasami karta graficzna mogła czasem nie zdążyć narysować co trzeba przed odświeżeniem obrazu na monitorze i stąd to dziwne miganie. Gdyby jednak utworzyć pomocniczy bufor (kontekst + tymczasowa bitmapa), w nim "skleić" szary prostokąt, maskę i bitmapę, a dopiero cały efekt wysłać na ekran, wówczas animacja byłaby dużo płynniejsza. Spróbujmy więc:
HDC hdcBufor;
HBITMAP hbmBuf, hbmOldBuf;
To na razie tylko deklaracje; potrzebujemy jednego uchwytu do kontekstu oraz dwóch uchwytów do bitmap (jeden będzie pełnił rolę tymczasowej bitmapy, do drugiego przypiszemy domyślną bitmapę naszego bufora). Następnie tworzymy bufor podobnie, jak tworzyliśmy kontekst
hdcMem:
hdcBufor = CreateCompatibleDC( hdc );
hbmBuf = CreateCompatibleBitmap( hdcBufor, rcOkno.right, rcOkno.bottom );
hbmOldBuf =( HBITMAP ) SelectObject( hdcBufor, hbmBuf );
FillRect( hdcBufor, & rcOkno,( HBRUSH ) GetStockObject( LTGRAY_BRUSH ) );
Bitmapa, której używamy jako bufor z kontekstem
hdcBufor, ma wymiary okienka. Oznacza to ni mniej ni więcej, że cały obszar okna będzie rysowany od nowa. W ten sposób możemy zmazać "starą" klatkę animacji oraz narysować nową za jednym zamachem. Oczywiście pociąga to za sobą pewne koszty; animacja będzie wolniejsza. Za to, dzięki podwójnemu buforowaniu, nie będzie nam denerwująco migotać. Dalsze optymalizacje (a trzeba przede wszystkim zmniejszyć powierzchnię odświeżanego obszaru) pozostawiam tobie.
Teraz zmieniamy po raz kolejny funkcję
RysujKulke. Obydwie instrukcje
BitBlt zostają gdzie są, ale zmieniamy im docelowe
HDC; tym razem nie blitujemy na ekran, tylko do bufora. No a ponieważ bufor ma wielkość zaledwie fragmentu całego okna, więc zmieniamy również docelowe współrzędne (na zera):
SelectObject( hdcMem, hbmMaska );
BitBlt( hdcBufor, rcKulka.left, rcKulka.top, rcKulka.right - rcKulka.left,
rcKulka.bottom - rcKulka.top, hdcMem, 0, 0, SRCAND );
SelectObject( hdcMem, hbmKulka );
BitBlt( hdcBufor, rcKulka.left, rcKulka.top, rcKulka.right - rcKulka.left,
rcKulka.bottom - rcKulka.top, hdcMem, 0, 0, SRCINVERT );
Instrukcja
FillRect teraz też będzie malowała w buforze, a nie na ekranie, więc zmieniamy jej prostokąt docelowy oraz uchwyt kontekstu:
FillRect( hdcBufor, & rcOkno,( HBRUSH ) GetStockObject( LTGRAY_BRUSH ) );
Następnie po prostu posyłamy zawartość bufora na ekran, dorzucając trzecią instrukcję
BitBlt, tym razem z flagą
SRCCOPY, ponieważ wszystkie "kombinacje" zostały już wykonane w buforze:
BitBlt( hdc, 0, 0, rcOkno.right, rcOkno.bottom, hdcBufor, 0, 0, SRCCOPY );
Nie zapominamy o dodatkowych obowiązkach porządkowych: pod koniec programu buforowi przywracamy jego domyślną bitmapę i usuwamy go. Niszczymy też oczywiście bitmapę
hbmBuf. Po tych zabiegach możemy wreszcie wcisnąć F9 i rozkoszować widokiem kulki mknącej przez ekran... Z niezbyt imponującą prędkością może, ale bez "efektów ubocznych". Optymalizacje, jak już wspomniałem, są twoją pracą domową, natomiast przykładowy program wraz z kodem źródłowym znajdziesz w dziale Download.