Z tego tutoriala dowiesz się w jaki sposób odczytywać bitmapy z plików BMP i wyświetlać je w oknie. Bazą będzie dla nas program napisany w artykule „Grafika w oknie – inicjalizacja”.
Zacznijmy od napisania procedury odczytującej pliki BMP, która będzie jako parametr przyjmować nazwę pliku i zwracać adres do bitmapy oraz jej wymiary. Użyjemy do tego celu funkcji LoadImage oraz GetDIBits. Oto parametry funkcji LoadImage:
HINSTANCE hinst, // handle of the instance that contains the image LPCTSTR lpszName, // name or identifier of image UINT uType, // type of image int cxDesired, // desired width int cyDesired, // desired height UINT fuLoad // load flags
Jeżeli chcemy odczytywać bitmapę z pliku, wystarczy że podamy jego nazwę jako lpszName, w uType użyjemy stałej IMAGE_BITMAP, która oznacza, że podany plik jest bitmapą, natomiast w fuLoad skorzystamy z flagi LR_LOADFROMFILE. W pozostałych parametrach możemy podać 0.
invoke LoadImage,0,lpFileName,IMAGE_BITMAP,0,0,LR_LOADFROMFILE mov ebx,eax
Jeżeli wszystko pójdzie dobrze, LoadImage zwróci nam uchwyt do bitmapy (hBitmap), który zapamiętujemy w rejestrze ebx.
Mając już uchwyt, możemy skorzystać z GetDIBits. Wywołamy ją dwukrotnie: najpierw aby odczytać wymiary i inne właściwości bitmapy, a później aby uzyskać samą bitmapę jako ciąg bajtów. Funkcja ta wymaga następujących parametrów:
HDC hdc, // handle of device context HBITMAP hbmp, // handle of bitmap UINT uStartScan, // first scan line to set in destination bitmap UINT cScanLines, // number of scan lines to copy LPVOID lpvBits, // address of array for bitmap bits LPBITMAPINFO lpbi, // address of structure with bitmap data UINT uUsage // RGB or palette index
Widzimy, że funkcja wymaga podania HDC. Właściwie nie wiadomo po co, ale jak chce to jej to damy :).
invoke CreateCompatibleDC,0 mov esi,eax
Dzięki temu w rejestrze esi ląduje uchwyt device contextu kompatybilnego z pulpitem.
Z API helpa dowiadujemy się, że jeżeli parametr lpvBits jest ustawiony na 0, to funkcja GetDIBits wypełnia strukturę BITMAPINFO, której adres podamy jako lpbi. W tekście „Grafika w oknie – inicjalizacja” zadeklarowaliśmy już taką strukturę, teraz możemy ją wykorzystać. Dla pewności wypełnijmy ją zerami i ustawmy właściwą wartość biSize.
mov edi,offset bmi assume edi:ptr BITMAPINFO push edi xor eax,eax mov ecx,sizeof BITMAPINFO rep stosb ;wypełnianie struktury zerami pop edi mov [edi].bmiHeader.biSize,sizeof BITMAPINFOHEADER
Teraz można już wywołać funkcję GetDIBits.
xor edx,edx invoke GetDIBits,esi,ebx,edx,edx,edx,edi,edx
W rejstrze esi mamy hDC, w ebx – hBitmap, a w edi adres struktury BITMAPINFO. Edx wynosi zero. Gdy w strukturze bmi znajdują się już wszystkie własności bitmapy, przyjrzyjmy się ponownie parametrom funkcji GetDIBits.
- HDC hdc – to już mamy
- HBITMAP hbmp – to też
- UINT uStartScan – pierwsza linia bitmapy do odczytania, my chcemy odczytać całą bitmapę, więc podamy tutaj 0 (linie są oczywiście numerowane od zera)
- UINT cScanLines – ilość linii do odczytu, podamy tutaj wysokość bitmapy, którą już znamy
- LPVOID lpvBits – adres tablicy na bitmapę, o tym za chwilę
- LPBITMAPINFO lpbi – to już mamy
- UINT uUsage – ustawiamy tutaj czy chcemy korzystać z palety czy nie, my nie musimy więc dajemy DIB_RGB_COLORS
Mamy więc już wszystkie potrzebne parametry oprócz lpvBits. Tę tablicę musimy utworzyć, na przykład przy pomocy funkcji GlobalAlloc. Tablica musi pomieścić całą bitmapę. Jak obliczyć jej wielkość w bajtach? Po prostu obliczamy ilość pixeli (zwyczajnie mnożąc wysokość przez szerokość) i mnożymy przez cztery, ponieważ każdy pixel jest zapisany jako cztery bajty.
mov eax,[edi].bmiHeader.biWidth ;eax=width mul [edi].bmiHeader.biHeight ;eax=width*height shl eax,2 ;eax=width*height*4 invoke GlobalAlloc,GMEM_FIXED,eax push eax ;zapamietaj test eax,eax jz @F
Wartość zwróconą przez GlobalAlloc (czyli adres obszaru pamięci, w którym zostanie umieszczona nasza bitmapa) zapamiętujemy na stosie – przyda nam się później. Sprawdzamy również czy pamięć została zaalokowana poprawnie, a jeśli wystąpił błąd skaczemy do miejsca w którym zostaną zwolnione zasoby i nastąpi zakończenie procedury.
Teraz możemy ponownie wywołać funkcję GetDIBits. Musimy jeszcze pamiętać o jednej rzeczy – parametr biHeight w strukturze bmi musi być liczbą przeciwną do wysokości bitmapy (w innym przypadku otrzymamy bitmapę odwróconą do góry nogami).
push DIB_RGB_COLORS ;uUsage push edi ;struktura bmi push eax ;tablica w której będzie zapisana bitmapa push [edi].bmiHeader.biHeight ;wysokość bitmapy push 0 ;pierwsza linia do odczytania push ebx ;hBitmap push esi ;hDC mov [edi].bmiHeader.biBitCount,32 ;ustawiamy głębię kolorów na 32 bity neg [edi].bmiHeader.biHeight ;odwracamy parametr biHeight call GetDIBits
Gdy mamy już wszystko co potrzeba, należy jeszcze po sobie posprzątać. Usuwamy więc device context oraz uchwyt do bitmapy (hBitmap), który nie będzie nam już potrzebny.
@@: invoke DeleteDC,esi invoke DeleteObject,ebx
Teraz wystarczy już tylko zwrócić potrzebne wartości i zakończyć procedurę. Zwracamy w eax adres do pamięci zawierającej bitmapę oraz jej szerokość i wysokość (odpowiednio w ecx i edx).
pop eax ;lpBitmap mov ecx,[edi].bmiHeader.biWidth ;szerokość mov edx,[edi].bmiHeader.biHeight ;wysokość assume edi:nothing _ret: ret
Gdy mamy już procedurę odczytującą bitmapy z plików, spróbujmy wyświetlić jedną z nich w oknie. W archiwum dołączonym do tego tekstu znajdują się pliki galaxy.bmp oraz kolo.bmp. Aby je odczytać w programie, wystarczy dopisać ich nazwy w sekcji .data…
Galaxy db "galaxy.bmp",0 Kolo db "kolo.bmp",0
… oraz zadeklarować zmienne w których będziemy trzymać adresy tych bitmap.
lpKolo dd ? lpGalaxy dd ?
Podczas inicjalizacji (w procedurze DoGfx) możemy użyć funkcji LoadBitmapFromFile.
invoke LoadBitmapFromFile,addr Galaxy mov lpGalaxy,eax invoke LoadBitmapFromFile,addr Kolo mov lpKolo,eax
Pamiętajmy o tym aby zwolnić pamięć zajętą przez te bitmapy, gdy już nie będziemy ich potrzebować. Najlepiej zrobić to podczas deinicjalizacji. Używamy funkcji GlobalFree, gdyż pamięć na bitmapy została zaalokowana za pomocą GlobalAlloc.
invoke GlobalFree,lpKolo invoke GlobalFree,lpGalaxy
Jak wyświetlić bitmapę w oknie? Wiemy, że bitmapy są jedynie fragmentami pamięci zawierającymi ciąg bajtów, w których jest zapisana grafika. Całą grafikę rysujemy w bitmapie, której adres mamy zapamiętany w zmiennej lpBackBitmap. A zatem skoro zarówno lpGalaxy jak i lpBackBitmap są bitmapami i to w dodatku o takich samych rozmiarach (320×200), wystarczy skopiować bitmapę lpGalaxy do lpBackBitmap, tak jak byśmy kopiowali zwykły obszar pamięci.
mov esi,lpGalaxy mov edi,lpBackBitmap mov ecx,320*200 rep movsd
W ten sposób można postępować tylko jeżeli obydwie bitmapy (źródłowa i docelowa) mają takie same rozmiary.
Spróbujmy teraz nałożyć bitmapę lpKolo na lpGalaxy (proces ten nazywa się blittowaniem). Wiemy, że mają one różne rozmiary (lpKolo – 64×64, lpGalaxy – 320×200). Popatrzmy na rysunek.
Zastanówmy się jak to zrobić. Najpierw trzeba obliczyć miejsce w pamięci w którym znajduje się punkt A (korzystając ze wzoru podanego w artykule „Grafika w oknie – inicjalizacja”), następnie skopiować tam pierwsze 64 pixele z bitmapy lpKolo (jak widzimy, 64 to szerokość tej bitmapy). W ten sposób znajdziemy się w punkcie B. Aby przejść do kolejnej linii musimy dodać 320-64, czyli 256 pixeli, czyli 1024 bajty. Potem znowu skopiować 64 pixele, i tak dalej, w sumie 64 razy (bo 64 to wysokość lpKolo). Zapiszmy to jako kod.
mov esi,lpKolo ;bitmapa źródłowa mov edi,lpGalaxy ;bitmapa docelowa add esi,(68*320+10)*4 ;przechodzimy do punktu A ;o współrzędnych (10,68) mov eax,64 ;licznik linii czyli wysokość lpKolo _blt: mov ecx,64 ;szerokość lpKolo rep movsd ;kopiujemy 64 pixele add edi,(320-64)*4 ;przechodzimy do następnej linii dec eax ;zmniejszamy licznik linii... jnz _blt ;...i powtarzamy operacje tak długo ;aż nie skopiujemy całej bitmapy
Mam nadzieję, że rozumiecie cały ten proces. Teraz zapiszmy go jako uniwersalną procedurę.
BitBlt32 proc uses esi edi lpDstBitmap:dword, ;adres docelowej bitmapy lpSrcBitmap:dword, ;adres źródłowej bitmapy SrcW:dword, ;szerokość źródłowej bitmapy SrcH:dword, ;wysokość źródłowej bitmapy DstX:dword, ; DstY:dword, ;współrzędne punktu A DstW:dword, ;szerokość docelowej bitmapy DstH:dword ;wysokość docelowej bitmapy mov esi,lpSrcBitmap mov edi,DstY ;przechodzimy do punktu A imul edi,DstW add edi,DstX shl edi,2 add edi,lpDstBitmap mov edx,DstW ;obliczanie wartości którą należy sub edx,SrcW ;dodać do edi w celu przejścia shl edx,2 ;do następnej linii mov eax,SrcH ;licznik linii _blt: mov ecx,SrcW ;szerokość bitmapy źródłowej rep movsd ;kopiowanie add edi,edx ;przejście do następnej linii dec eax ;zmniejszenie licznika linii jnz _blt ;powtarzanie operacji aż ;skopiujemy wszystkie linie ret BitBlt32 endp
Z tej procedury możemy bardzo łatwo skorzystać podczas rysowania.
invoke BitBlt32,lpBackBitmap,lpKolo,64,64,10,68,WND_WIDTH,WND_HEIGHT
Widzmy, że koło które dzięki temu wyświetliło się w oknie posiada jaskrawozieloną ramkę. Dobrze byłoby mieć możliwość wyświetlania bitmap z niewidocznym jednym kolorem. Nic prostszego! Wystarczy przy blittowaniu pomijać pixele o kolorze, który uznamy za przezroczysty (w naszym przypadku jest to kolor zielony, 00FF0000h).
Musimy zamienić linijkę „rep movsd” w powyższej procedurze na kod, który odczytuje pixel po pixelu i porównuje je z kolorem przezroczystym, a jeżeli są równe, po prostu pominie ten pixel. Oto przykład takiego kodu:
_wew: lodsd ;odczytaj pixel and eax,00FFFFFFh ;wyczyść bajt alpha cmp eax,ColorKey ;czy pixel ma być przezroczysty? je @F ;jeśli tak, pomiń go mov [edi],eax ;zapisz pixel do bitmapy docelowej @@: add edi,4 ;przejście do następnego pixela dec ecx jnz _wew ;powtórz dla wszystkich pixeli
No i w ten sposób doszedłem do 10 kB tekstu, wiec pora kończyć ;). Jako zadanie domowe (ehehhe już widzę jak wszyscy je robią) spróbuj napisać procedury które blittują nie całą bitmapę, a jedynie jej fragment. Efekt naszego kodzenia (przykładowy program wraz ze źródłem) znajdziesz w archiwum dołączonym do tego tekstu.
Załącznik:
czajnick_gdibitmap.rar
no dobra. przeczytalem to i teraz bede to stosowac sie staral. bo musze tradycyjny brzydki kalkulator zrobic ladnym i ladnym.
Cześć wszystkim. Chciałem tylko zgłosić że strona co jakiś czas nie działa…