Funkcje rysujące WinAPI przydają się, kiedy trzeba dorysować gdzieś jakąś linię rozdzielającą dwie grupy kontrolek albo coś w tym rodzaju. Trudno jednak przy ich pomocy stworzyć np. grę (chociaż nie jest to niemożliwe, czego dowodem mój WinRobak - znajdziesz go w dziale Download). Do takich celów przydadzą nam się "zewnętrzne" pliki graficzne, czyli grafika, którą przygotowaliśmy sobie wcześniej i zapisaliśmy w jakimś pliku. Na dobry początek najlepiej zaznajomić się z najprostszym z możliwych i najpopularniejszym formatem graficznym - standardową bitmapą Windows, czyli BMP. Spróbujmy obrazek z takiego pliku wyświetlić na ekranie.
Ładowanie bitmapy z pliku
Załóżmy, że mamy już jakiś obrazek w pliku BMP, stworzony np. w programiku Paint. Żeby go użyć, musimy go wprzódy skopiować do pamięci. Bitmapami przechowywanymi w pamięci manipulujemy, korzystając z uchwytów typu
HBITMAP, a żeby wczytać bitmapę i uzyskać do niej taki uchwyt, stosujemy funkcyjkę
LoadImage:
HBITMAP hbmObraz;
hbmObraz =( HBITMAP ) LoadImage( NULL, "c:\\sciezka\\plik.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE );
Jak widać, lista argumentów jest dość długa, ale większość to taki pic na wodę - ważna jest w sumie tylko ścieżka dostępu do pliku BMP. Pierwszy argument pomijamy, wykorzystuje się go tylko przy wczytywaniu bitmap z pliku wykonywalnego naszego programu. Trzeci argument dla bitmap BMP powinien wynosić
IMAGE_BITMAP (zaś dla kursorów i ikon odpowiednio
IMAGE_CURSOR lub
IMAGE_ICON), ostatni argument to flagi, w większości przypadków wpisujemy tam
LR_LOADFROMFILE. Argumenty, które w naszym przykładzie są równe
0 to wymiary obrazka - zera oznaczają wczytanie domyślnych wymiarów z pliku BMP.
Jeśli mamy naszą bitmapę wyświetlić na ekranie monitora, to musimy ją skopiować na
HDC okna. WinAPI posiada taką jedną fajną funkcję,
BitBlt - potrafi ona przenosić bitmapy z kontekstu na kontekst. Ba, tylko skąd wytrzasnąć kontekst, z którego kopiujemy naszą bitmapkę, bo uzyskanie tego drugiego
HDC, od okna, nie jest zbyt wielkim problemem? Ano, możemy sobie taki kontekst sami stworzyć, po czym przypisać mu wczytaną właśnie z pliku bitmapę:
HDC hdcNowy = CreateCompatibleDC( hdc );
SelectObject( hdcNowy, hbmObraz );
Funkcja
CreateCompatibleDC, jak sama nazwa wskazuje, tworzy
HDC kompatybilny z podanym. W większości przypadków nie musimy nawet podawać tego
hdc jako argumentu; wtedy funkcja utworzy
HDC kompatybilne z "bieżącym ekranem aplikacji" (cokolwiek by ten cytat z MSDN nie znaczył, podanie
NULL zamiast argumentu zazwyczaj daje pożądane efekty).
Jest jeszcze mały haczyk. Każdy
HDC ma przypisaną domyślną bitmapę, którą później dobrze jest mu "oddać". Wielu programistów z czystego lenistwa upraszcza sobie proces pisania aplikacji, no i później ludzie skarżą się na Billa, że Windows się zawiesza... A Bill, chociaż czystego sumienia niewątpliwie nie ma, to jednak pewnie nie jest też winien nawet połowy zarzucanych mu rzeczy :-). Tak czy siak, my sobie elegancko zapamiętamy domyślną bitmapę i później ją zwrócimy tuż przed skasowaniem naszego
HDC:
HBITMAP hbmOld =( HBITMAP ) SelectObject( hdcNowy, hbmObraz );
SelectObject( hdcNowy, hbmOld );
DeleteDC( hdcNowy );
Funkcja BitBlt w natarciu
Mamy bitmapę w pamięci, mamy
HDC - bazę wypadową dla owej bitmapy, pora rozpocząć akcję o kryptonimie "Rysowanie Bitmapy". Wspomniałem wcześniej o funkcji
BitBlt (skrót od "bit block transfer"). Ma ona dość długi wykaz argumentów - aż 9, ale szybko je pewnie zapamiętasz, bo jest to jedna z "najpopularniejszych" funkcji w całym WinAPI.
BitBlt służy do kopiowania bitmap (całych lub tylko fragmentów) pomiędzy dwoma kontekstami, przy czym dodatkowo może wykonywać na nich '''operacje rastrowe''', a więc można ją wykorzystać także do rysowania '''maski''' (o tym później). Oto składnia i znaczenie kolejnych argumentów:
Składnia:
BitBlt( hdcDest, nXDest, nYDest, nWidth, nHeight, hdcSrc, nXSrc, nYSrc, dwRop )
Chyba nie trzeba tu nic omawiać, za wyjątkiem ostatniego argumentu. Kod operacji rastrowej określa transformację, jaką wykonujemy na bitach bitmapy. Oto najczęściej stosowane operacje:
Wciąż nie wiesz, o co chodzi z tymi operacjami rastrowymi? Nic nie szkodzi, nie musisz ;-). Zazwyczaj będziemy korzystać z najprostszego SRCCOPY, dopiero kiedy dojdziemy do masek, wykorzystamy również pozostałe trzy rodzaje operacji.
Zanim użyjemy funkcji
BitBlt do narysowania bitmapy na naszym okienku, pozostaje tylko jeszcze pobrać jej wymiary, bo przecież musimy je podać jako argumenty do
BitBlt. Załóżmy, że nie znamy wymiarów obrazka, więc posłużymy się celu ich zdobycia funkcją o mało trafnej nazwie
GetObject. Mało trafnej, bo w istocie nie pobiera ona żadnego obiektu, a jedynie informacje o nim. Dla ścisłości w dodatku - stosuje się ją tylko do obiektów graficznych. Funkcja
GetObject pobiera uchwyt do obiektu, o którym chcemy uzyskać informacje, rozmiar (w bajtach) struktury, do której trafią te informacje (oczywiście musimy tę strukturę najpierw utworzyć) oraz adres tej struktury. W naszym przypadku struktura ta musi być typu
BITMAP (nie mylić z
HBITMAP!):
BITMAP bmInfo;
GetObject( hmbObraz, sizeof( bmInfo ), & bmInfo );
Dzięki takiemu zabiegowi mamy wymiary naszego obrazka w polach
bmInfo.bmWidth oraz
bmInfo.bmHeight. Możemy więc rysować wreszcie bitmapę! Zakładamy, że zmienna
hwnd zawiera uchwyt naszego głównego okna:
hdc = GetDC( hwnd );
BitBlt( hdc, 50, 50, bmInfo.bmWidth, bmInfo.bmHeight, hdcNowy, 0, 0, SRCCOPY );
ReleaseDC( hwnd, hdc );
I to wszystko! Mamy nasz obrazek na ekranie...
Porządki
Żeby nie zaśmiecić systemu na śmierć, kasujemy lub zwalniamy wszystko, cośmy potworzyli lub pobrali. Zaczynamy od zwolnienia
HDC okna, po którym rysujemy (patrz przykład powyżej) i usunięcia bitmapy. Potem wywalamy w cholerę niepotrzebny już tymczasowy
HDC - zauważ, że usuwa się go poleceniem
DeleteDC, a nie
ReleaseDC - to drugie stosuje się tylko w przypadku
HDC pobranego, to pierwsze - wobec
HDC utworzonego przez użytkownika funkcją
CreateCompatibleDC. Zanim wywołamy
DeleteDC, przywracamy mu jego domyślną bitmapę, tak jak pokazałem trochę wcześniej. Na wszelki wypadek dorzucam przykład poprawnego zwalniania zasobów:
DeleteObject( hbmObraz );
SelectObject( hdcNowy, hbmOld );
DeleteDC( hdcNowy );
Przezroczyste bitmapy
A co, jeśli chcemy narysować jedną bitmapę na drugiej, tak żeby ta na wierzchu miała przezroczyste tło? Przy wykorzystaniu "gołego" API nie jest to proste zadanie, ale nie przekracza bynajmniej naszych skromnych możliwości. Potrzebujemy przede wszystkim tzw. maski, czyli oddzielnej, monochromatycznej (czarno-białej) bitmapy, która jest biała we wszystkich miejscach, które chcemy by były przezroczyste, a czarna we wszystkich innych miejscach. Na razie musimy sobie taką maskę utworzyć sami. Oto mały przykładzik bitmapy i dorobionej do niej maski (tutaj są w jednym pliku, a praktyce muszą być oddzielnie):
Oczywiście wczytywanie bitmapy maski - nazwijmy sobie uchwyt do niej
hbmMaska - pomijamy. Ta kolorowa bitmapa z obrazkiem, która ma mieć przezroczyste tło, również musi być utworzona w specjalny sposób, a mianowicie wszystkie piksele, które mają być przezroczyste, rysujemy w niej na czarno - tak jak na rysunku powyżej.
Jeśli obie bitmapy są już gotowe, przystępujemy do ich rysowania. Klucz do sukcesu to podwójne wywołanie
BitBlt z odpowiednimi kodami operacji - najpierw dla maski, potem dla właściwego obrazka. Wykorzystamy do tego nasz tymczasowy
HDC, czyli
hdcNowy:
SelectObject( hdcNowy, hbmMaska );
BitBlt( hdc, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, hdcNowy, 0, 0, SRCAND );
SelectObject( hdcNowy, hbmObraz );
BitBlt( hdc, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, hdcNowy, 0, 0, SRCPAINT );
Szczerze mówiąc, nigdy jakoś nie korciło mnie, żeby zrozumieć tajniki rządzące operacjami rastrowymi, więc nie mam bladego pojęcia, dlaczego powyższy kod działa, jak Pan Bóg przykazał :-). Najważniejsze, że mimo wszystko działa ;-).
Procedura tworzenia maski
"Ręczne" tworzenie masek jest żmudne, sprzyja pomyłkom, a przy tym jest raczej bezsensowne, skoro komputer może sobie taką maskę stworzyć sam. Wystarczy, że we właściwym obrazku zamalujemy przezroczyste miejsca specjalnym, tylko w tym celu użytym kolorem, a komputer później znajdzie te miejsca i na tej podstawie sporządzi maskę.
Oto procedura tworząca maskę dla podanej bitmapy i podanego "przezroczystego" koloru. Skopiowałem ją ze strony
www.winprog.org (mam nadzieję, że autor się nie pogniewa) i raczej nie będę się męczył tłumaczeniem jej działania, które zresztą jest dość proste:
HBITMAP CreateBitmapMask( HBITMAP hbmColour, COLORREF crTransparent )
{
HDC hdcMem, hdcMem2;
HBITMAP hbmMask, hbmOld, hbmOld2;
BITMAP bm;
GetObject( hbmColour, sizeof( BITMAP ), & bm );
hbmMask = CreateBitmap( bm.bmWidth, bm.bmHeight, 1, 1, NULL );
hdcMem = CreateCompatibleDC( NULL );
hdcMem2 = CreateCompatibleDC( NULL );
hbmOld =( HBITMAP ) SelectObject( hdcMem, hbmColour );
hbmOld2 =( HBITMAP ) SelectObject( hdcMem2, hbmMask );
SetBkColor( hdcMem, crTransparent );
BitBlt( hdcMem2, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY );
BitBlt( hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem2, 0, 0, SRCINVERT );
SelectObject( hdcMem, hbmOld );
SelectObject( hdcMem2, hbmOld2 );
DeleteDC( hdcMem );
DeleteDC( hdcMem2 );
return hbmMask;
}
Kompletny program
Jako podsumowanie wyświetlimy sobie przezroczystą bitmapę - zakładamy, że znajduje się ona w pliku '''obrazek.bmp''' i że jej tło jest jasnozielone. Do stworzenia maski wykorzystamy funkcję z poprzedniego rozdziału:
HBITMAP hbmObraz, hbmOld, hbmMaska;
hbmObraz =( HBITMAP ) LoadImage( NULL, "obrazek.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE );
hbmMaska = CreateBitmapMask( hbmObraz, RGB( 0, 255, 0 ) );
HDC hdc = GetDC( hwnd ), hdcNowy = CreateCompatibleDC( hdc );
BITMAP bmInfo;
GetObject( hbmObraz, sizeof( bmInfo ), & bmInfo );
hbmOld =( HBITMAP ) SelectObject( hdcNowy, hbmMaska );
BitBlt( hdc, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, hdcNowy, 0, 0, SRCAND );
SelectObject( hdcNowy, hbmObraz );
BitBlt( hdc, 0, 0, bmInfo.bmWidth, bmInfo.bmHeight, hdcNowy, 0, 0, SRCPAINT );
ReleaseDC( hwnd, hdc );
SelectObject( hdcNowy, hbmOld );
DeleteDC( hdcNowy );
DeleteObject( hbmMaska );
DeleteObject( hbmObraz );
Odświeżanie zawartości okienka
Grafika, którą do tej pory umiemy narysować na oknie, nie jest zbyt trwała. Wystarczy zminimalizować nasze okienko, a wszystkie arcydzieła znikną bezpowrotnie :-/. Co gorsza, nawet jeśli na wierzch naszego okna wyskoczy jakiś głupi komunikat, to zamaże on tę część naszego rysunku, którą zakrywał. Tego oczywiście nie chcemy, a jedynym sposobem żeby tych przykrych rzeczy uniknąć jest właściwe obsłużenie komunikatu
WM_PAINT, który jest wysyłany przez okienko za każdym razem, gdy to wykryje, że ktoś perfidnie zamazał jego zawartość.
Wydawać by się mogło, że nic prostszego - wystarczy wrzucić kod odpowiedzialny za rysowanie do miejsca, gdzie obsługiwany jest komunikat
WM_PAINT. Ale nic z tego! To znaczy owszem, tak właśnie uczynimy, ale będziemy też musieli ten kod odpowiednio zmodyfikować. Napisane jest bowiem w Piśmie (czyli w dokumentacji do API ;-) ): w
WM_PAINT będziesz wywoływał funkcję
BeginPaint!
Odświeżanie okna różni się nieco od innych graficznych czynności, dlatego też trzeba je specjalnie oznakować. Służy do tego wspomniana funkcja
BeginPaint (którą oznaczamy poczatek procedury rysującej) oraz
EndPaint (zgadnij ;-)).
BeginPaint zwraca na przy okazji
HDC naszego okna, więc nie musimy go pobierać przy użyciu
GetDC. Ponadto
BeginPaint wymaga podania adesu specjalnej struktury typu
PAINTSTRUCT. Nie musimy jej wykorzystywać, ale adres musi być, więc i tworzymy taką strukturę:
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint( hwnd, & ps );
EndPaint( hwnd, & ps );
}
break;
Wszystkie instrukcje, które rysują nasz obrazek na oknie, muszą się znaleźć pomiędzy
BeginPaint a
EndPaint. Jeśli tak zrobimy, nic już nie zniszczy naszych wiekopomnych dzieł sztuki malarskiej, chyba że my sami ;-).