Z poprzedniej lekcji wiemy jak rysować różne figury na okienkach, ale spróbujmy narysować drzewo z samych linni i elips. No właśnie, będzie to co najmniej trudne. Na szczęście GDI to nie same figury, ale także operacje na skomplikowanych obrazach rastrowych, czyli bitmapach. Przejdźmy do rzeczy.

Obiekt bitmapy

Na ten temat już sobie nieco wcześniej powiedzieliśmy. Obiekt bitmapy to poprostu bitmapa, która egzystuje sobie gdzieś w pamięc. Tutaj także trzeba sobie powiedzieć, że do samej bitmapy nie mamy dostępu, możemy się do niej odwoływać poprzez uchwyt(HBITMAP).

Tworzenie bitmapy

Nasz program może sobie stworzyć bitmapę. Robi się to funkcją CreateCompatibleBitmap().

HBITMAP CreateCompatibleBitmap(HDC hdc,INT nWidth,INT nHeight);

Może zacznę od dwóch ostatnich parametrów, otóż są to wymiary bitmapy, kolejno szerokość i wysokość. Pierwszym parametrem jest tutaj kontekst urządzenia, z jakim ma być kompatybilna bitmapa, tzn. bitmapa pobierze parametry z kontkestu, jest to ważne, tutaj powinniśmy podać kontekst tego urządzenia, na którym ma być wyświatlana bitmapa, zwykle będzie to uchwyt do kontekstu okna.

Mamy także możliwość utworzenia bitmapy z parametrami, które sami określimy, ale trzeba pamiętać, że aby było można wyświetlić daną bitmapę, musi ona mieć takie parametry jak kontekst urządzenia na której będzie wyświetlona. Oto funkcja:

HBITMAP CreateBitmap(INT nWidth,INT nHeight,UNIT cPlanes,UNIT cBitsPerPel,CONST LPVOID lpvBits);

Jak widać funkcją tworzymy bitmapę od zera, z uwzględnieniem jej wszystkich parametrów(cech). Pierwsze dwa parametry są jak w przypadku poprzedniej funkcji, odpowiednio szerokość i wysokość. Parametr cPlanes określa przestarzałą ilość płatów, obecnie coś takiego jest nieużywane i zawsze podajemy tu wartość 1. W parametrze cBitsPerPel podajemy głębię kolorów(format piksela) bitmapy, możemy podać 8, 16, 24, czy 32, jeżeli chcemy używać kanału alpha. Ostatni parametr jest ciekawy, to wskaźnik do struktury bitów, którymi ma zostać wypełniona bitmapa, innymi słowy jest to binarna zawartość bitmapy, jeżeli narazie nie chcemy jej inicjować, możemy podać 0. Jeszcze raz przypominam, że tworząc bitmapę tą funkcją, parametry które podamy, muszą się zgadzać z kontekstem urządzenia, w przeciwnym razie mogą wystąpić trudności z wyświetleniem bitmapy w danym kontekście.

...
COLORREF zawartosc[
4
]={RGB(
0
,
21
,
45
),RGB(
234
,
132
,
99
),RGB(
255
,
255
,
57
),RGB(
45
,
221
,
187
)};
HBITMAP bitmapa=CreateBitmap(
2
,
2
,
1
,
24
,(LPVOID)zawartosc);
...

Wczytywanie bitmapy

Teraz zajmiemy się otwieraniem bitmapy. Przy otwieraniu mamy dwie możliwości, mianowicie możemy otwierać bitmapę z zasobów programu lub z pliku na dysku. Funkcja otwierająca, którą poznamy to LoadImage().

HANDLE LoadImage(HINSTANCE hInst,LPCTSTR lpszName,UINT uType,INT cxDesired,INT cyDesired,UINT fuLoad);

HINSTANCE hInst
W tym parametrze podajemty uchwyt do programu, jeżeli otwieramy bitmapę z zasobów, należy tutaj podać uchwyt do programu, w którym się znajduje otwierana bitmapa.

LPCTSTR lpszName
W tym parametrze podajemy ścieżkę do pliku, jeżeli otwieramy z dysku, a jeżeli z zasobów nazwę zasobu.

UINT uType
Funkcja ta jest unierwsalna i oprócz wczytywania bitmapy, obsługije także wczytywanie ikon i kursorów. Tutaj określamy rodzaj wczytywanego obiektu: IMAGE_BITMAP dla bitmpay, IMAGE_ICON dla ikony, IMAGE_CURSOR dla kursora.

INT cxDesired i INT cyDesired
Przy wczytywaniu kursora lub ikony, należy tutaj podać wymiary jakie chcemy uzyskać, przy wczytywaniu bitmapy parametry te są ignorowane, możemy podać 0.

UINT fuLoad
Tutaj podajemy dodatkowe flagi, każdą z nich możemy połączyć operatorem sumy bitowej:
LR_CREATEDIBSECTION - podając ją wczytywana bitmapa, będzie miała orginalne kolory, gdyż normlanie funkcja ta dopasowuje format kolorów bitmapy do ekranu.
LR_MONOCHROME - flaga ta zredykuje głębię kolorów bitmapy do tryby monochromatycznego(czarno-biały)
LR_LOADFROMFILE - gdy chcemy odczytać bitmapę z pliku, należy użyć tej flagi.
LR_LOADTRANSPARENT - funkcja ta sprawdza pierwszy piksel w obrazku, a następnie każdy taki sam kolor piksela wczytuje jako przezroczysty.
LR_LOADDEFAULTSIZE - w przypadku ikony i kursora, flaga ta powoduje, że rozmiary ich będą niezmienione(domyślne).
LR_SHARED - przy kolejnym załadowniu tej samej bitmapy, otrzymujemy uchwyt na ten sam obiekt w pamięci.

Zwracana wartość
Funkcja zwraca uchwyt wczytanego obiektu, a jeżeli się nie powiedzie zwraca 0. Uchwyt obiektu trzeba rzutować, gdyż jest zwracany w uchwycie uniwersalnym.

HBITMAP bitmapa=(HBITMAP)LoadImage(
0
,
"bitmapa.bmp"
,IMAGE_BITMAP,
0
,
0
,LR_LOADFROMFILE);

Kontekst pamięciowy

Wczytanie lub utworzenie bitmapy to dopiero początek. Mając bitmapę w pamięci, aby cokolwiek z nią zrobić musimy stworzyć dla niej kontekst. Kontekst to miejsce po jakim rysujemy oraz jego narzędzia. Tworząc kontekst od okna miejscem po jakim rysujemy jest właśnie powierzchnia tego okna, aby rysować na bitmapie, trzeba także stworzyć dla niej kontekst, w którym będziemy rysować właśnie na niej przy pomocy narzędzi w tym kontekście. Taki kontkest, który nie jest związany z żadnym oknem, tzn. nie jest nigdzie wyświetlany, nazywa się kontkestem pamięciowym, gdyż jest tylko w pamięci.

Kontekst pamięciowy stworzymy sobie funkcją CreateCompatibleDC():

HDC CreateCompatibleDC(HDC hdc);

Nasz kontekst pamięciowy z bitmapą, możemy utworzyć od innego istniejacego kontekstu, gdyż musi byc on kompatybilny z nim, najlepiej podać tutaj uchwyt do kontekstu okna, na którym będzie ta bitmapa w przyszłości wyświetlana.

HDC hdc_pamieciowy=CreateCompatibleDC(hdc);

Po utworzeniu kontekstu pamięciowego i załadowaniu bitmapy do pamięci, trzeba je połączyć, tzn. umieścić bitmapę w naszym kontekście pamięciowym. Robimy to doskonale znaną funkcją SelectObject().

DeleteObject(SelectObject(hdc_pamieciowy,bitmapa));

Po powiązaniu bitmapy z kontekstem pamieciowym, nie trzeba jej zwalniać jako obiektu, gdyż zostanie ona zwolniona razem z całym kontekstem, gdy będziemy niszczyć nasz kontekst pamięciowy.

Oczywiście po zakończeniu pracy z bitmapą należy zwolnić kontkest pamięciowy funkcją DeleteDC(HDC hdc).

Operacje na bitmapie

Tutaj już mówiąc o operacjach na bitmapie, mam na myśli bitmapę jak cały kontekst, gdyż przed chwilą powiązaliśmy ją z kontekstem pamięciowym, dlatego, że w GDI wszelkie operacje, dokonuje się poprzez kontekst urządzenia i właśnie dlatego utworzyliśmy dla naszej bitmapy kontekst. W kontekście tym obszar po, którym rysujemy jest bitmapa, odwołując się do kontekstu, możemy edytować bitmapę tak jak edytujemy okno.

W poprzedniej lekcji poznaliśmy funkcje rysujące na oknie, otóż okno to w rzeczywistości bitmapa, która jest wyświetlana na ekranie monitora. Teraz nasz kontkest z bitmapą różni się od kontekstu pobranego z okna, tylko tym, że mu w kontekście mamy bitmapę.

Zauważmy, że we wszystkiech funkcjach rysujących z poprzedniej lekcji podajemy uchwyt do kontekstu urządzenia, myślę, że wiesz do czego zmierzam, otóż gdy podabmy w tych funkcjach uchwyt do naszego kontekstu pamieciowego z bitmapą, będziemy rysować właśnie na naszej bitmapie. Na tym właśnie polegają operacje na bitmapie, możemy sobie na niej narysować kwadrat, trójkąt czy cokolwiek innego, przy użyciu funkcji, które poznaliśmy w poprzedniej lekcji.

Operacja wypełniania kolorem

Myślę, że każdy spotkał się z systemowym programem graficznym Paint. Otóż spójrzmy na niego, jest to program doskonale prezętujący możliwości GDI, ale nie o tym chciałem mówić, otóż, chyba każdy zna narzędzie w paincie, które służy do wylewania koloru, ma iknokę pojemniczka, z którego wylewa się farba. Polega to na tym, że wybierany jest piksel na danej bitmapie, zmieniany jest jego kolor i teraz wszystkie sąsiednie piksele o takim samym kolorze, także zostaną zmienione na dany kolor.

Teraz przedstawię funkcję, która to robi, efekt może być używany oprócz bitmap, także na oknach(bo okno to bitmapa), ale jest to raczej operacja typowa dla bitmap.

BOOL ExtFloodFill(HDC hdc,INT nXStart,INT nYStart,COLORREF crColor,UNIT fuFillType);

HDC hdc
Pierwszy parametr to tradycyjnie uchwyt do kontekstu, może być to uchwyt do kontekstu pamięciowego bitmapy, lub zwyczajny konteklst okna.

INT nXStart i INT nYStart
Są to współrzędne punktu, od którego ma zacząć się cała operacja wypełnienia.

COLORREF crColor i UNIT fuFillType
Najpierw zająłem się ostatnim parametrem, jest to rodzaj wypełnienia:
FLOODFILESURFACE - ten tryb działa tak jak w paincie, wszystkie sąsiednie piksele zostają wypełnione, kolorem aktualnego pędzla(może to byc pędzel z bitmapy lub wzór). W tym trybie zmienna crColor, musi mieć takie sam kolor, jak piksel, który jest początkiem operacji, można go pobrać funkcją GetPixel().
FLOODFILLBORDER - Jest to tryb odwrotny do poprzedniego, w tamtym trybie kolor w parametrze crColor, był warunkiem kontynuowania jego wypełnienia, natomiast w tym trybie jest on kryretium zatrzymania wypełniania. W tym trybie funkcja maluje całą bitmapę, aż nie zostanie zamknięta obramowaniu danego koloru, podanego w parametrze crColor. Tej funkcji paint nie posiada.

Wyświetlenie bitmapy na oknie

Stworzyliśmy już kontkest pamięciowy z bitmapą, wykonywaliśmy na niej operacje graficzne, tylko co z tego skoro jeszcze nie potrafimy wyświetlić efektów naszej pracy.

Technicznie wyświetlenie bitmapy polega na przekopiowniu zawartości bitmapy z kontekstu pamięciowego do kontekstu okna, które juz jest wyświetlane w windowsie. Możemy do tego użyć funkcji BitBlt():

BOOL BitBlt(HDC hdcDes,INT nXDes,INT nYDes,INT nWidth,INT nHeight,HDC hdcSrc,INT nXSrc,INT nYSrc,DWORD dwRop);

Na pierwszy rzut okna, funkcja może przyprawić o zawrót głowy, ale w rzeczywistości jest bardzo prosta.

HDC hdcDes
Pierwszy z kontekstów, określa przeznaczenie(destiantion) bitmapy, czyli miejsce, gdzie ma zostać przekopiowana, zwykle podamy tutaj uchwyt do okna, na którym ma zostać wyświetlona.

INT nXDes i INT nYDes
Te dwa parametry to współrzędne lewego górnego wierzchołka bitmapy, określają położenie bitmapy na nowym celu, czyli oknie.

INT nWidth i INT nHeight
Są to rozmiary kopiowanego fragmentu bitmapy, oczywiste chyba, że jeżeli chcemy przekopiować całą bitmapę, podajemy jej wymiary, rozmiar ten będzie jednocześnie wielkością wyświetoną na oknie.

HDC hdcSrc
Skoro pierwszy określa cel, to ten musi określać źródło bitmapy, czyli kontekst pamięciowy, z którego zostanie przkopiowana.

INT nXSrc i INT nYSrc
Te parametry określają to co nXDes i nYDes,z tym, że te określają położenie źródła kopiowanej bitmapy, jeżeli chcemy skopiować cały obraz, podajemy 0,0.

DWORD dwRop
Ten parametr określa sposób łączenia bitów bitmapy, z bitami powierzchni, która zostanie przykryta na kontekście przeznaczenia(oknie). Można tu podać wiele róznych kombinacji łączenia pikseli obu kontekstów, ja podam tylko jeden, który jest zwykle wykorzystywany, wykorzystywanie reszty to raczej rzadkość. Otóż to flaga SRCCOPY, czyli przykrycie powierzchni okna bitmapą.

W GDI jest jeszcze jedna funkcja, która kopiuje bitmape na okno, różni sie ona tym, że potrafi przesklaować bitmapę podczas kopiownaia.

BOOL StretchBlt(HDC hdcDes,INT nXDes,INT nYDes,INT nWidthDes,INT nHeightDes,
HDC hdcSrc,INT nXScr,INT nYSrc,INT nWidthSrc,INT nHeightSrc,DWORD dwRop);

Większość parametrów odpowiada funkcji BitBlt(), różnią się jedynie wymiary bitmap, otóż tam podawaliśmy wymiar w jendym miejscu, tutaj oddzielnie podajemy wymairy obszaru kopiowanego z bitmapy, a oddzielnie wymairy obrazu wklejanrgo na okno, gdyż funkcja potafi przeskalować bitmapę, jednak nie należy się spodziewać oszałamiających efektów skalowania.

Przezroczyste wyświetlanie

W GDI jest jeszcze jedna funkcja do kopiowania bitmapy z kontekstu pamięciowego do kontekstu okna wyświetlanego. Jak można się domyślać, po nagłówku, że funkcja ta potrafi kopiować przezroczyście, czyli nie kopiować pewnych pikseli, zostawiając w ich miejscu te, które są na oknie. Funkcja ta TransparentBlt(), została wprowadzona dopiero w Windows 98.

Aby było można skorzystać z tej funkcji, trzeba dodać bibliotekę do linkowanych modułów w projekcie, w dziale "Kontrolki", jest dokładnie opisane jak to zrobić, bibliotekę dodajemy poprzez dopisanie:
-lmsimg32

Dodatkowo w pliku nagłówkowym, jest wymóg, aby WINVER był większy lub równy 0x0500, dlatego musimy jeszcze zadeklarować na samym początku kodu, jeszcze przed dołączeniem plików nagłówkowych:
#define WINVER 0x0600

BOOL TransparentBlt(HDC hdcDes,INT nXDes,INT nYDes,INT nWidthDes,INT nHeightDes,
HDC hdcSrc,INT nXScr,INT nYSrc,INT nWidthSrc,INT nHeightSrc,COLORREF crTransparent);

Funkcja w budowie bardzo podobna do StretchBlt(), co oznacza, że także potrafi skalować bitmapę, jedynie zmienił się ostatni parametr. Teraz zamiast podawać rodzaju operacji bitowej, podajemy wartość koloru. Kolor ten to taki jakby wyznacznik koloru przezroczystego, otóż gdy funkcja, znajdzie piksel o tym kolorze zostanie on potraktowany jako przezroczysty i nie zostanie przekopiowany, tak właśnie powstanie efekt przezroczystości.

Ci, którzy mieli ochotę zaszaleć z bitmapami wykorzystując kanały alpha(przezroczystość), muszę niestety zmartwić, GDI jest generalnie przygotowane do pracy z 24 bitowymi bitmapami, jendak posiada ono małe wsparcie dla kanału alpha, jednak efekty stosowania tychże funkcji są mizerne, często bitmapy po takich operacjach mają zły kontrast, dlatego lepiej jest uzywać 24 bitowego foramtu, a efekty przezroczystości wykorzystywać takimi funkcjami jak TransparentBlt().

Pobranie atrybutów bitmapy

Gdy otwieramy bitmapę z pliku na dysku, nie zawsze wiemy jakie ma ona wymairy, aby się dowiedzieć trzeba utworzyć strukturę BITMAP i umiescić w niej informacje.

struct BITMAP
{
  INT bmType;
//typ bitmapy, zwykle będzie tutaj 0

  INT bmWidth;
//szerokość bitmapy w pikselach

  INT bmHeight;
//wysokość bitmapy w pikselach

  INT bmWidthBytes;
//określa liczbę bajtów w każdej rastrowanej linii

  BYTE bmPlanes;
//określa ilość płatów kolorów, obecnie sie tego nie stosuje

  BYTE bmBitsPixel;
//określa długość piksela(format piksela)

  LPVOID bmBits;
//jest to wskaźnik do zawartości binarnej bitmapy

}

Do pobrania informacji do tej struktury możemy użyć funkcji GetObject():

...
HBITMAP bitmapa=(HBITMAP)LoadImage(
0
,
"bitmapa.bmp"
,IMAGE_BITMAP,
0
,
0
,LR_LOADFROMFILE);
BITMAP dane_bitmapy;
GetObject(bitmapa,sizeof(BITMAP),&dane_bitmapy);
...

Przykładowy program

To wiemy już teoretycznie jak pracuje się z bitmapami, teraz spójrzmy jak wygląda to w kodzie, program, który otwora bitmapę z pliku i wyświetla ją na oknie:

#include <windows.h>

HBITMAP bitmapa;
//uchwyt naszej bitmapy

BITMAP info_bitmapy;
//struktura inforamcyjna bitmapy


LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

INT WINAPI WinMain(HINSTANCE hInst,HINSTANCE,LPSTR lStart,INT nShow)
{
  
//wczytywanie bitmapy z dysku i pobranie informacji

  bitmapa=(HBITMAP)LoadImage(
0
,
"bitmapa.bmp"
,IMAGE_BITMAP,
0
,
0
,LR_LOADFROMFILE);
  if(bitmapa==
0
)
  {
    MessageBox(
0
,
"Nie można otworzyć pliku bitmap.bmp z folderu z aplikacją"
,
"Brak pliku"
,MB_ICONERROR);
    return
0
;
//zamkniecie programu w razie braku pliku bitmapy

  }
  GetObject(bitmapa,sizeof(BITMAP),&info_bitmapy);
//pobieramy informacje o bitmapie, potrzebujemy jej wymiarów


  WNDCLASSEX wc;
  wc.hInstance=hInst;
  wc.lpszClassName=
"Klasa okna"
;
  wc.lpfnWndProc=WndProc;
  wc.style=CS_DBLCLKS;
  wc.cbSize=sizeof(WNDCLASSEX);
  wc.hIcon=LoadIcon(
0
,IDI_APPLICATION);
  wc.hIconSm=LoadIcon(
0
,IDI_APPLICATION);
  wc.hCursor=LoadCursor(
0
,IDC_ARROW);
  wc.lpszMenuName=
0
;
  wc.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
  wc.cbWndExtra=
0
;
  wc.cbClsExtra=
0
;
  if(RegisterClassEx(&wc)==
0
) return
0
;
  HWND Okno=CreateWindowEx(
0
,
"Klasa okna"
,
"Tytuł okna"
,WS_OVERLAPPEDWINDOW,
50
,
50
,
800
,
600
,
0
,
0
,hInst,
0
);
  MSG msgs;
  ShowWindow(Okno,nShow);
  UpdateWindow(Okno);
  while(GetMessage(&msgs,
0
,
0
,
0
))
  {
    TranslateMessage(&msgs);
    DispatchMessage(&msgs);
  }
  
//po zakończeniu pracy programu zwalniamy bitmapę

  DeleteObject(bitmapa);
  return msgs.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd,UINT msg,WPARAM wPar,LPARAM lPar)
{
  HDC hdc,hdcBitmapy;
//zmienne na 2 konteksty

  PAINTSTRUCT ps;
  switch(msg)
  {
    case WM_PAINT:
    hdc=BeginPaint(hwnd,&ps);
//kontekst okna uzyskujemy w momencie odrysowywania

    hdcBitmapy=CreateCompatibleDC(hdc);
//teraz musimy utworzyć kontekst pamięciowy dla bitmapy

    bitmapa=(HBITMAP)SelectObject(hdcBitmapy,bitmapa);
//zamieniamy konteksty wiążąc bitmapę z kontekstem
    //tutaj możemy przeprowadzać jakieś operacje graficzne z naszym kontekstem bitmapy
    //uwaga te operacje będą przeprowadzane co każde odświeżenie okna
    //wyświetlenie bitmapy na oknie, operacja przekopiowania

    BitBlt(hdc,
0
,
0
,info_bitmapy.bmWidth,info_bitmapy.bmHeight,hdcBitmapy,
0
,
0
,SRCCOPY);
    bitmapa=(HBITMAP)SelectObject(hdcBitmapy,bitmapa);
//z powrotem zamieniamy bitmapy w kontekście

    DeleteDC(hdcBitmapy);
//usuwamy kontekst pamięciowy bitmapy

    EndPaint(hwnd,&ps);
      break;
    case WM_DESTROY:
      PostQuitMessage(
0
);
      break;
    default:
      return DefWindowProc(hwnd,msg,wPar,lPar);
  }
  return
0
;
}

Aby ten program poprawnie działał, potrzebuje w swoim folderze bitmapy o nazwie "bitmapa.bmp". Jeżeli będzie poprawnie otwierany przez nasz program, powinniśmy zobaczyć ta bitmapę na naszym oknie.

Kilka słów o *.jpg i innych skompresowanych formatach

Tutaj niestety muszę wielu zmartwić, ale GDI samo w sobie nie obsługuje tych formatów, trzeba użyć jakiejś innej zewnętrznej biblioteki lub samemu obsłużyć te formaty plików. Być może na ten temat kiedyś zrobię lekcję.