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
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?