Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Animacja

[lekcja]

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. 

C/C++
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:

C/C++
// w zasięgu globalnym
HWND g_hwnd;

// w funkcji WinMain
g_hwnd = hwnd;

// gdziekolwiek
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:

C/C++
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:

C/C++
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:

C/C++
const WORD ID_TIMER = 1;
 
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:

C/C++
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ę:

C/C++
case WM_TIMER:
RysujKulke();
break;
 
Jeszcze tylko wyczarować funkcję RysujKulke i kulka skacze sobie radośnie po całym okienku:

C/C++
void RysujKulke()
{
    // wylicz nowe parametry kulki
    if( KulkaX <= 0 || KulkaX >= ptOkno.x ) SpeedX = - SpeedX;
   
    if( KulkaY <= 0 || KulkaY >= ptOkno.y ) SpeedY = - SpeedY;
   
    KulkaX += SpeedX;
    KulkaY += SpeedY;
    // narysuj na nowej pozycji
    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:

C/C++
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:

C/C++
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:
 
C/C++
void RysujKulke()
{
    // pobierz HDC okna
    HDC hdc = GetDC( g_hwnd );
    //zamaż "starą" kulkę
    FillRect( hdc, & rcTemp,( HBRUSH ) GetStockObject( LTGRAY_BRUSH ) );
    // wylicz nowe parametry kulki
    if( rcKulka.left <= 0 || rcKulka.right >= rcOkno.right ) SpeedX = - SpeedX;
   
    if( rcKulka.top <= 0 || rcKulka.bottom >= rcOkno.bottom ) SpeedY = - SpeedY;
   
    OffsetRect( & rcKulka, SpeedX, SpeedY );
    // narysuj na nowej pozycji
    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":

C/C++
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):
 
C/C++
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:

C/C++
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:
 
C/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:

C/C++
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):
 
C/C++
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:
 
C/C++
// zamaż "starą" kulkę
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:

C/C++
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.
Poprzedni dokument Następny dokument
Bitmapy Okna dialogowe, cz. 1