Przygotowania

Zapewne nie raz chciałeś urozmaicić swój program za pomocą jakiejś animacji wyświetlającej się w oknie, bądź też stworzyć coś w stylu intra, jednak nie wiedziałeś jak się do tego zabrać. Z tego tutoriala dowiesz się jak utworzyć bitmapę, do której będziesz miał bezpośredni dostęp oraz jak na bieżąco wyświetlać ją w oknie.

Utwórzmy zatem pusty plik o nazwie, dajmy na to, gdibase.asm i zacznijmy jak zwykle, od includów. Poza standardowymi bibliotekami (user32 i kernel32) będzie potrzebna również gdi32, która zawiera funkcje odpowiedzialne za tworzenie grafiki.

.386
.model flat, stdcall
option casemap :none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

Teraz musimy utworzyć okno. Proponuje zapisać jego szerokość, wysokość oraz położenie w postaci stałych, gdyż będziemy tych wartości używać w kodzie wielokrotnie. Przy rejestrowaniu klasy należy pamiętać o tym, aby zastosować styl CS_BYTEALIGNWINDOW oraz CS_BYTEALIGNCLIENT, co da nam lekki wzrost wydajności, a także ustawić parametr hbrBackground na 0, ponieważ tło okna będziemy malować sami.

mov    wc.cbSize,sizeof WNDCLASSEX
mov    wc.style,CS_BYTEALIGNWINDOW or CS_BYTEALIGNCLIENT
mov    wc.lpfnWndProc,offset WndProc
mov    wc.lpszClassName,offset AppName
mov    eax,hInstance
mov    wc.hInstance,eax
invoke LoadIcon,0,IDI_APPLICATION
mov    wc.hIcon,eax
invoke LoadCursor,0,IDC_ARROW
mov    wc.hCursor,eax
;pozostałe wartości są ustawione na 0
invoke RegisterClassEx,addr wc
invoke CreateWindowEx,0,addr AppName,addr AppName,\
WS_VISIBLE or WS_POPUP,WND_LEFT,WND_TOP,WND_WIDTH,\
WND_HEIGHT,0,0,hInstance,0

StartLoop:
invoke GetMessage,addr msg,0,0,0
test eax,eax
jz ExitLoop
invoke DispatchMessage,addr msg
jmp StartLoop
ExitLoop:

Gdy mamy już okno, czas zająć się procedurą jego obsługi. W odpowiedzi na wiadomość WM_CREATE tworzymy wątek, w którym będziemy obsługiwać całą grafikę.

.ELSEIF uMsg==WM_CREATE
     invoke CreateThread,0,0,addr DoGfx,hWnd,0,addr ThreadID
     mov    hThread,eax

Funkcji CreateThread podajemy jako parametry: adres funkcji DoGfx, w której będzie tworzona cała grafika oraz uchwyt okna (hWnd), który będzie używany w procedurze DoGfx, a także adres zmiennej ThreadID, w której zostanie umieszczony numer identyfikacyjny wątku (zmienna ThreadID jest właściwie niepotrzebna i nie będzie używana w naszym programie, jednak bez niej funkcja CreateThread nie wykona się poprawnie).

Przy komunikacie WM_CLOSE musimy ten wątek zakończyć. W żadnym wypadku nie stosujcie do tego celu funkcji TerminateThread, ponieważ wtedy wątek nie ma możliwości „po sobie posprzątać”, czyli zwolnić używanych przez niego zasobów. Zamiast tego użyjemy mojego sposobu (nie wiem czy jest to dobre rozwiązanie, ale najważniejsze ze działa).

.ELSEIF uMsg==WM_CLOSE
     mov    ZakonczThread,1
@@:  invoke GetExitCodeThread,hThread,addr ThreadExitCode
     cmp    ThreadExitCode,STILL_ACTIVE
     je     @B
     invoke DestroyWindow,hWnd

Na początku umieszczamy w zmiennej globalnej ZakonczThread wartość 1. Ta zmienna jest sprawdzana w wątku rysującym i jeżeli wynosi 1, wtedy następuje zwolnienie wszystkich zasobów i wątek jest zakańczany. Następnie za pomocą funkcji GetExitCodeThread sprawdzamy czy wątek już się zakończył, robimy to dopóki tak się nie stanie. Jeżeli wszystko jest ok, niszczymy okno.

Czas najwyższy napisać procedurę DoGfx. Będzie się ona składać z trzech części: inicjalizacji, rysowania oraz deinicjalizacji. Tak wygląda część pierwsza:

invoke CreateCompatibleDC,0
mov    hBackDC,eax
mov bmi.bmiHeader.biSize,sizeof BITMAPINFOHEADER
mov bmi.bmiHeader.biWidth,WND_WIDTH
mov bmi.bmiHeader.biHeight,(not WND_HEIGHT)
mov bmi.bmiHeader.biPlanes,1
mov bmi.bmiHeader.biBitCount,32
mov bmi.bmiHeader.biCompression,BI_RGB
invoke CreateDIBSection,hBackDC,addr bmi,DIB_RGB_COLORS,\
addr lpBackBitmap,0,0
mov hBackBitmap,eax
invoke SelectObject,hBackDC,eax

@@:
invoke GetDC,hWnd
test eax,eax
jz @B
mov hMainDC,eax

Objaśnianie tego fragmentu kodu zacznę od przestawienia użytej tutaj w zmiennej bmi struktury BITMAPINFO. Składają się na nią struktura BITMAPINFOHEADER oraz tablica struktur RGBQUAD, która opisuje paletę nowej bitmapy. Ponieważ będziemy tworzyli bitmapę 32 bitową, która nie potrzebuje palety, nie musimy wypełniać tej tablicy. Zainteresowanych odsyłam do helpa. Czas przyjrzeć się strukturze BITMAPINFOHEADER. Zawiera ona następujące pola:

  • biSize – Rozmiar całej struktury, ustawiamy go na sizeof BITMAPINFOHEADER.
  • biWidth – Szerokość bitmapy w pixelach.
  • biHeight – Wysokość bitmapy w pixelach, z tym że jeżeli podamy tutaj wartość pozytywną (większą od zera), bitmapa będzie odwrócona do góry nogami. Dlatego też musimy ustawić to pole na wartość ujemną, przeciwną do wysokości bitmapy.
  • biPlanes – Ilość warstw bitmapy. To pole trzeba ustawić na 1.
  • biBitCount – Głębia kolorów w bitach. Ustawiamy na 1, 4, 8, 16, 24 lub 32.
  • biCompression – Rodzaj kompresji bitmapy, możemy wybrać jej brak (BI_RGB) lub dwa rodzaje komresji RLE (BI_RLE8 i BI_RLE4 – po więcej informacji odsyłam do helpa). To pole dotyczy jedynie bitmap typu do-góry-nogami, w naszym przypadku musimy podać tutaj stałą BI_RGB.
  • biSizeImage – Rozmiar bitmapy w bajtach. Dla bitmap nieskompresowanych możemy ustawić to pole na 0.
  • biXPelsPerMeter, biYPelsPerMeter – Rozdzielczość pozioma i pionowa urządzenia dla bitmapy. Prawdę mówiąc te pola nigdy nie były mi do niczego potrzebne i nie wiem jak można je wykorzystać.
  • biClrUsed – Ilość kolorów używanych w bitmapie. Jeżeli podamy 0, możemy używać wszystkich kolorów jakie są możliwe do uzyskania w wybranym przez nas trybie (biBitCount). Ta opcja jest przydatna jeśli np. ustawiliśmy głębie kolorów (biBitCount) na 8 (czyli max. 256 kolorów) ale bitmapa wykorzystuje ich mniej, np. tylko 100.
  • biClrImportant – Ilość kolorów, które są w bitmapie najważniejsze. Windows używa tej informacji podczas wyświetlania np. bitmap 256-kolorowych na ekranie który może wyświetlić 16 kolorów. W praktyce możemy ustawić to pole na 0 – wtedy wszystkie kolory będą tak samo ważne.

Skoro już znamy strukturę BITMAPINFO, możemy przejść do omawiania kodu. Na początku tego fragmentu tworzymy device context. Potrzebujemy go właściwie tylko po to aby móc użyć funkcji BitBlt do wyświetlenia bitmapy w oknie (o tym później). Następnie tworzymy bitmapę, do której będziemy mieli bezpośredni dostęp. W strukturze bmi (bmi BITMAPINFO <>) umieszczamy wszystkie informacje niezbędne do utworzenia tej bitmapy czyli jej szerokość (WND_WIDTH), wysokość (a właściwie liczbę przeciwną do wysokości – not WND_HEIGHT), ilość warstw (1), głębię kolorów (32) oraz rodzaj kompresji (BI_RGB oznacza, że nie stosujemy żadnej kompresji). Po wywołaniu funkcji CreateDIBSection w zmiennej lpBackBitmap został umieszczony adres początku naszej bitmapy. Gdy mamy już bitmapę, korzystamy z SelectObject aby połączyć ją z device contextem.

Następnie pobieramy device context naszego okna. Jeżeli funkcja GetDC zwróci zero, oznacza to, że okno nie jest jeszcze gotowe na to, aby w nim rysować. W takim przypadku próbujemy pobrać DC tak długo aż będzie to możliwe.

Czas przejść do części drugiej, czyli rysowania. Jak pisałem wyżej, zmienna lpBackBitmap zawiera adres bitmapy. Każdy pixel tej bitmapy reprezentowany jest przez cztery bajty (przy głębi kolorów 32 bity na pixel – taką właśnie wybraliśmy przy tworzeniu bitmapy). Bajty te oznaczają kolejno: współczynnik przezroczystości alpha (używany w zaawansowanych programach), kolor czerwony, kolor zielony i kolor niebieski. Im wyższa wartość określonego bajtu tym większe nasycenie odpowiadającego mu koloru. Aby obliczyć miejsce w pamięci pod którym znajduje się pixel o określonych współrzędnych (x,y), należy skorzystać z poniższego wzoru:

[lpBackBitmap]+(Y*szerokość_bitmapy+X)*4

Zamalujmy zatem całą bitmapę na czarno i zmieńmy kolor pixela o współrzędnych (30,40) na czerwony.

mov    ecx,WND_WIDTH*WND_HEIGHT
mov    edi,lpBackBitmap
xor    eax,eax
rep    stosd
mov edi,lpBackBitmap
mov dword ptr [edi+(40*WND_WIDTH+30)*4],00FF0000h

Teraz, gdy narysowaliśmy już grafikę w bitmapie, wyświetlimy ją w oknie. W tym celu skorzystamy z funkcji BitBlt.

invoke BitBlt,hMainDC,0,0,WND_WIDTH,WND_HEIGHT,hBackDC,0,0,SRCCOPY

Powoduje ona skopiowanie obrazu z hBackDC czyli device contextu zawierającego bitmapę do hMainDC czyli device contextu naszego okna. Możecie spytać: „a po co bawimy się w tworzenie jakichś bitmap? czy nie można po prostu od razu rysować w oknie?”. Można, ale będzie to na pewno o wiele wolniejsze, ponieważ namalowanie każdego elementu grafiki będzie powodowało odmalowanie okna. Poza tym, może to powodować efekt migania lub inne niepożądane efekty.

Gdy rysunek jest już w oknie, wystarczy sprawdzić czy nie musimy zakończyć wątku. Jeżeli nie, można przejść z powrotem do rysowania (np. kolejnej klatki animacji).

invoke Sleep,10
cmp ZakonczThread,1
jne rysowanie

Gdyby jednak okazało się, że wątek musi zostać zakończony, usuwamy wszystkie obiekty utworzone podczas inicjalizacji, zwalniamy pamięć i wychodzimy z wątku.

deinicjalizacja:
invoke ReleaseDC,hWnd,hMainDC
invoke DeleteDC,hBackDC
invoke DeleteObject,hBackBitmap
ret

DoGfx endp

To by było na tyle jeśli chodzi o szkielet programu wyświetlającego grafikę w oknie. Pamiętaj, że wcale nie musisz wykorzystywać całego okna aby narysować prostą animację. Równie dobrze możesz utworzyć kontrolkę STATIC i na niej umieścić na przykład animowane logo. Być może (czytaj: jeśli będzie mi się chciało) napiszę jeszcze kilka tekstów wyjaśniających tworzenie prostych efektów graficznych, a tymczasem żegnam i życzę wielu wspaniałych animacji okienkowych…

Do tego tekstu załączyłem przykładowy programik ilustrujący to, co zostało opisane w tym tutorialu.

Załącznik:
czajnick_gdibase.rar

Jeden komentarz do “Przygotowania

  1. Gameman

    Ja osobiście programuję pod Wind’ę w Virtual Pascal’u i również próbowałem sztuczki z "CreateDIBSection" do bezpośredniego "dobrania się" do pamięci bitmapy. Niestety za każdym razem, pomimo poprawności zwróconego wskaźnika oraz uchwytu, wszelka próba modyfikacji pamięci przysługującej temu pierwszemu, kończy się ERROREM. Czy powodem tego może byc fakt rysowania bezposrednio z procedury obslugujacej zdarzenia – w tym wypadku WM_PAINT?

    Odpowiedz

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *