Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'Złośliwiec'
Biblioteki C++

Bitmapy

[lekcja] Rozdział 6. Ładowanie bitmap z pliku i wyświetlanie ich na ekranie, oraz tworzenie masek na bazie istniejących obrazów. Dodatkowo w rozdziale znajdują się także informacje o zwalnianiu zarezerwowanej pamięci, zagadnieniu kluczowym w GDI.
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:

C/C++
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ę:

C/C++
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:

C/C++
HBITMAP hbmOld =( HBITMAP ) SelectObject( hdcNowy, hbmObraz );
// ...
// coś tam
// ...
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:
C/C++
BitBlt( hdcDest, nXDest, nYDest, nWidth, nHeight, hdcSrc, nXSrc, nYSrc, dwRop )

ArgumentZnaczenie
hdcDestHDC docelowe (np. HDC okna, na którym rysujemy)
nXDest, nYDestWspółrzędne docelowe
nWidth, nHeightWymiary obrazka
hdcSrcHDC źródłowe
nXSrc, nYSrWspółrzędne źródłowe (zazwyczaj równe 0)
dwRopKod operacji rastrowej

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:
 
StałaZnaczenie
SRCCOPYpo prostu kopiuje bity, nie wykonując na nich żadneej operacji (najczęściej stosowane)
SRCINVERTodwraca bity przy użyciu operatora XOR
SRCPAINTtworzy kombinację bitów przy użyciu operatora OR
SRCANDtworzy kombinację bitów przy użyciu operatora AND

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!):

C/C++
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:

C/C++
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:

C/C++
DeleteObject( hbmObraz ); // kasowanie bitmapy
SelectObject( hdcNowy, hbmOld ); // przywróć bitmapę domyślną
DeleteDC( hdcNowy ); // usuń kontekst razem z jego domyślną bitmapą

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):

Bitmapa i jej maska
Bitmapa i jej maska
 
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:

C/C++
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:

C/C++
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:

C/C++
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ę:

C/C++
case WM_PAINT:
{
    PAINTSTRUCT ps; // deklaracja struktury
    HDC hdc = BeginPaint( hwnd, & ps );
    // ...
    // instukcje rysujące coś na oknie
    // ...
    EndPaint( hwnd, & ps ); // zwalniamy hdc
}
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 ;-).
Poprzedni dokument Następny dokument
Grafika Animacja