Bitmapy i blittowanie

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

2 myśli nt. „Bitmapy i blittowanie

  1. snow

    no dobra. przeczytalem to i teraz bede to stosowac sie staral. bo musze tradycyjny brzydki kalkulator zrobic ladnym i ladnym.

    Odpowiedz

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *