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

Grafika

[lekcja] Rozdział 5. Podstawowe mechanizmy do rysowania po kontekście urządzenia. Rysowanie linii, prostokątów, elips i okręgów oraz figur o innych kształtach. Ponadto pobieranie kontekstu urządzenia i jego zwalnianie, tworzenie pióra i pędzla oraz ich obsługa.

Konteksty, zasoby, uchwyty...

Co jak co, ale rysowanie w Windows przy pomocy WinAPI jest wyjątkowo przegięte. Porzuć wszelkie nadzieje na to, aby np. linię można było narysować jednym rozkazem, tak jak to jest np. w Visual Basicu, a także w większości bibliotek graficznych pod DOS-a. Takie są jednak wymogi nowoczesnych systemów operacyjnych...

Jednak jest też druga strona medalu. Pod takim DOS-em, żeby cokolwiek narysować, musieliśmy wiedzieć absolutnie wszystko o karcie graficznej, monitorze itp. W Windows nie musimy wiedzieć prawie nic! System robi to wszystko za nas. Nie ma się więc co zniechęcać, jedziemy z tym koksem.

Wszelkie operacje graficzne w Windowsach są związane z tzw. kontekstem urządzenia - po angielsku Device Context, a w skrócie DC. Cokolwiek byśmy chcieli narysować, musimy podać uchwyt do kontekstu urządzenia (Handle to Device Context - HDC). Brzmi to trochę jak bełkot, więc odwołując się do intuicji: kontekst urządzenia to zazwyczaj po prostu okno, w którym rysujemy. Od tej pory będę nazywał kontekst urządzenia w skrócie: HDC.

To była pierwsza ważna sprawa, a druga to tak zwane zasoby systemu - coś takiego pewnie ci się już obiło o uszy, nieprawdaż? Pisząc jakikolwiek program dla Windows korzystamy z tychże zasobów i musimy uważać, aby ich nie wyczerpać, gdyż to prowadzi w linii prostej do zwisów, wysypek, błędów krytycznych i innych wesołych rzeczy. Dlatego też schemat robienia czegokolwiek w WinAPI wygląda tak: pobierz zasoby - zrób coś - oddaj zasoby. Robiliśmy tak już chociażby z naszym głównym okienkiem programu, natomiast przy rysowaniu jest to szczególnie ważne i wyjątkowo często się korzysta z tego schematu.

Rysujemy linię

Pobieranie i zwalnianie kontekstu urządzenia

Nasze pierwsze, wielkie zadanie: narysować linię prostą o danym kolorze. Jak już wspomniałem, będzie nam potrzebny uchwyt do kontekstu urządzenia. Ponieważ naszą uroczą linijkę chcemy narysować w oknie, kontekstem urządzenia będzie właśnie to okienko. Dysponujemy uchwytem do tego okienka, jednak to jeszcze nie jest to - uchwyt do okienka jest typu HWND, natomiast do kontekstu urządzenia - typu HDC. Aby uzyskać HDC naszego okna, musimy skorzystać z funkcji GetDC:

C/C++
HDC hdcOkno;
hdcOkno = GetDC( hwnd );

Kiedy już skończymy rysowanie, będzie trzeba zwolnić HDC naszego okna. Jeśli tego bowiem nie zrobimy, żadna inna aplikacja nie będzie mogła nic na naszym okienku narysować. Oczywiście, w większości przypadków nie zechcemy, by jakaś tam obca aplikacja mazała nam po okienku, no ale etykieta programistyczna obowiązuje :-). Korzystamy więc z funkcji ReleaseDC, przyjmującej jako argumenty: uchwyt okna (HWND) oraz uchwyt kontekstu (HDC):

C/C++
ReleaseDC( hwnd, hdcOkno );

Ustawianie pozycji rysowania

W WinAPI zwykle nie podajesz funkcjom rysującym, skąd mają zacząć rysowanie - zaczynają je od domyślnego punktu, którym jest zazwyczaj ostatnio narysowany punkt. Taka sytuacja jest czasami bardzo wygodna, ale też czasami chcielibyśmy wiedzieć, skąd zaczynamy rysowanie :-). Trzeba więc przenieść ten domyślny punkt startowy na odpowiednie miejsce. Możesz to sobie wyobrazić jako wielki pędzel, który zawisł nad ekranem w punkcie, w którym skończyło się ostatnio malowanie :-). Żeby ponownie użyć tego pędzla, musimy go przesunąć tam, gdzie chcemy teraz rysować. Służy do tego funkcja PrzenieśPędzel, tfu, MoveToEx :-). Jako parametry podajemy: HDC, nowe współrzędne, adres bufora, w którym zapamiętane zostaną stare współrzędne (na wypadek, gdybyśmy kiedyś jeszcze mieli z nich skorzystać). Bufor ten musi być typu POINT.

C/C++
POINT stary_punkt;
MoveToEx( hdcOkno, 10, 10, & stary_punkt );

Przenieśliśmy nasz pędzel na pozycję (10, 10). Poprzednia pozycja pędzla została zapamiętana w zmiennej stary_punkt. Po skończonym rysowaniu możemy przenieść pędzel z powrotem na starą pozycję, ale nie będziemy się aż tak fatygować :-).

Funkcja LineTo

Pora wreszcie narysować naszą linię. Zatrudnimy w tym celu funkcję LineTo, rysującą linię od domyślnej pozycji startowej do podanego punktu. Niestety, ktoś skopał robotę i ostatni piksel nie jest rysowany, więc zawsze musimy podawać większe współrzędne, niż wskazuje intuicja. Wiocha, ale cóż poradzić... :-/

C/C++
LineTo( hdcOkno, 21, 10 );

No i jest linia - rozciąga się od punktu (10, 10) aż do punktu (20, 10) :-). Pewnie się dziwisz, dlaczego podałem 10, a nie 11 (a mówiłem wcześniej, że współrzędne mają być większe). No i tutaj właśnie wychodzi niekompetencja człowieka odpowiedzialnego za stworzenie funkcji LineTo :-P. Jak wspomniałem wyżej, ostatni piksel nie jest rysowany, więc gdybyśmy podali jako punkt końcowy (21, 11), to nie zostałby narysowany punkt o współrzędnych (21, 11), czyli rysowanie skończyłoby się na (20, 11). Trochę pokręcone... Mówiłem, że wiocha :-P.

Tworzenie pióra

Jeśli nie jesteś akurat daltonistą, a twój monitor działa sprawnie i ma normalne ustawienia jasności i kontrastu, to pewnie zauważyłeś, że linia jest czarna :-). Co zrobić, aby była np. czerwona? Można poprzestawiać kabelki w monitorze, ale jeszcze lepiej po prostu powiedzieć Windowsowi, żeby rysował na czerwono, a nie na czarno. Możesz jednak zapomnieć o jakiejś prostej funkcji typu SetColor. Twórcy Windowsa wykombinowali sobie, żeby jedną funkcją ustawiać nie tylko kolor, ale i wzór pióra, którym rysujemy. Nie wzięli chyba pod uwagę, że cokolwiek rzadko korzysta się z innych wzorów, niż domyślny, ale to już szczegół :-/. Linie rysuje się piórem, dlatego musimy sobie takowe stworzyć - pióro to inaczej obiekt typu HPEN:

C/C++
HPEN CzerwonePioro;
CzerwonePioro = CreatePen( PS_SOLID, 1, 0x0000FF );

Funkcja CreatePen, czyli nasza podręczna fabryka piór, ma trzy argumenty. Pierwszy z nich to styl pióra. Zazwyczaj chcemy rysować linie proste, wtedy używamy (jak wyżej) PS_SOLID. Inne style pióra to np. PS_DOT (linia przerywana) lub PS_DASH (też przerywana, ale dłuższe kreski). Drugi parametr to grubość linii w pikselach. Wreszcie trzeci to kod koloru, jaki ma mieć nasz pędzel. Jest on podany w postaci szesnastkowej. Nie można by było w dziesiątkowej? Ano można by było, ale jeśli zrobimy to w hexach, łatwiej będzie rozszyfrować ten kolor, bowiem jest on w formacie '''BBGGRR'''. Tak więc 0x0000FF oznacza, że bierzemy FF (czyli 255) czerwonego, 0 zielonego i 0 niebieskiego koloru - w ten sposób powstaje kolor czerwony. Możesz sprawdzić w Paincie :-). Na tej samej zasadzie możemy zrobić kolor zielony - 0x00FF00, biały - 0xFFFFFF, szary - 0x808080 itp.

Wybór pióra

Mamy już stworzony pędzel, ale to dopiero połowa sukcesu :-). Bo widzisz, jeśli artysta grafik ma w pudełku choćby i pięćdziesiąt piór, to i tak nic nie narysuje, dopóki nie weźmie jednego do ręki :-). Aby wziąć pióro w łapę, używamy funkcji SelectObject, służącej do przypisywania danemu kontekstowi danego obiektu GDI:

C/C++
SelectObject( hdcOkno, CzerwonePioro );

...i już od tej pory wszystko, co narysujemy na hdcOkno, będzie narysowane na czerwono. Oczywiście chodzi o te fragmenty rysunku, do których używa się pióra, czyli o wszelkie kontury. Ale to nie wszystko. Przecież żeby wziąć pióro, musimy mieć wolną rękę, natomiast windowsowy malarz zawsze trzyma jakieś pióro w łapie! Już sprawdziliśmy w praktyce, że wcześniej trzymał czarne, więc musimy je najpierw gdzieś wyrzucić :-). Tworzymy więc nowe pudełko na to "stare", czarne pióro:

C/C++
HPEN Pudelko; // :-)

Funkcja SelectObject zwraca nam obiekt danego typu, który wcześniej był przypisany do danego HDC. Na przykład jeśli użyjemy SelectObject do przypisania do okienka nowego, czerwonego pióra, funkcja zwróci uchwyt do starego (czarnego) pióra. Łapiemy więc stare pióro za ten uchwyt i wrzucamy do naszego pudełka :-).

C/C++
Pudelko =( HPEN ) SelectObject( hdcOkno, CzerwonePioro );

Musieliśmy tu dokonać konwersji do typu HPEN, ponieważ funkcja SelectObject operuje na różnych typach obiektów, nie tylko HPEN.

Niszczenie pióra

Po co malarzowi zużyte pióro? Tylko miejsce zajmuje w pracowni. A więc skoro skończyliśmy rysowanie, musimy zniszczyć stworzone pióra na amen :-). Wprawdzie windowsowe pióra nie zużywają się, ale zasoby systemowe - owszem, tak. Dlatego pod koniec procedury rysującej warto zrobić tak:

C/C++
DeleteObject( CzerwonePioro );

Oczywiście nie wolno nam niszczyć pióra, który jeszcze jest w ręce malarza, gdyż mogłoby to malarzowi wielce zaszkodzić :-). Pióro trzeba najpierw wyrzucić. Ponieważ zaś windowsowy malarz musi ciągle trzymać jakieś piórko w ręce, dajemy mu to czarne, które wcześniej wrzuciliśmy do pudełka:

C/C++
SelectObject( hdcOkno, Pudelko );
DeleteObject( CzerwonePioro );

Kompletny schemat rysowania linii

No to zbierzmy wszystko do kupy:

C/C++
HDC hdcOkno = GetDC( hwnd );
HPEN CzerwonePioro, Pudelko;
POINT stary;
CzerwonePioro = CreatePen( PS_SOLID, 1, 0x0000FF );
Pudelko =( HPEN ) SelectObject( hdcOkno, CzerwonePioro );
MoveToEx( hdcOkno, 10, 10, & stary );
LineTo( hdcOkno, 21, 10 );
SelectObject( hdcOkno, Pudelko );
DeleteObject( CzerwonePioro );
ReleaseDC( hwnd, hdcOkno );

Uff, jest tego trochę... Tyle zachodu, żeby jedną krótką linię narysować! Ale za to jeśli teraz będziemy chcieli dorysować drugą linię, wystarczy dorzucić dodatkową instrukcję LineTo. Oczywiście, jeśli zamarzymy sobie, że druga linia ma być zielona, trzeba będzie stworzyć dodatkowe, zielone pióro:

C/C++
HDC hdcOkno = GetDC( hwnd );
HPEN PioroCzerw, PioroZiel, Pudelko;
POINT stary;
PioroCzerw = CreatePen( PS_SOLID, 1, 0x0000FF );
PioroZiel = CreatePen( PS_SOLID, 1, 0x00FF00 );
Pudelko =( HPEN ) SelectObject( hdcOkno, PioroCzerw );
MoveToEx( hdcOkno, 10, 10, & stary );
LineTo( hdcOkno, 21, 10 );
SelectObject( hdcOkno, PioroZiel );
LineTo( hdcOkno, 31, 10 );
SelectObject( hdcOkno, Pudelko );
DeleteObject( PioroCzerw );
DeleteObject( PioroZiel );
ReleaseDC( hwnd, hdcOkno );

Zauważ, że przypisanie zielonego pióra nie wymaga już "wyrzucania do pudełka" czerwonego. Nie miałoby to większego sensu, bowiem uchwyt do czerwonego pióra już przecież mamy. Tylko kiedy wybieramy piórko po raz pierwszy, musimy uzyskać uchwyt do domyślnego "starego" (czarnego) pióra, które używane było wcześniej, zanim nasza aplikacja została uruchomiona.

Linia zielona zostanie narysowana od punktu, na którym skończyliśmy rysowanie czerwonej, do punktu (30,10). Jeśli chcemy zacząć rysowanie zielonej z innego punktu, musimy dopisać dodatkową instrukcję MoveToEx.

Aha, jeszcze jedno. Powinno to być oczywiste, ale rysować możemy tylko po istniejącym i widzialnym oknie, a więc powyższy kod umieszczamy PO instrukcjach CreateWindowEx (tworzącej nasze okno) i ShowWindow (czyniącej je widzialnym). Nie próbuj też wstawiać tego kodu w inne funkcje, niż WinMain (bo nasz hwnd jest tam niewidoczny, chyba że stworzysz specjalną oddzielną zmienną typu HWND), a zwłaszcza nie próbuj dodawać kodu w takiej postaci do obsługi komunikatu WM_PAINT (dlaczego - o tym kiedy indziej).

Łatwiejsze definiowanie kolorów

Podawanie numerów kolorów w postaci szesnastkowej jest całkiem niezłym sposobem, jeśli umiemy w locie przeliczać liczby z systemu dziesiątkowego. Jeśli akurat nie umiemy, a nie mamy pod ręką odpowiedniego programiku (albo nam się nie chce go włączać), można jeszcze skorzystać z makra RGB do utworzenia żądanego koloru. RGB ma trzy argumenty, oznaczające ilość składnika czerwonego, zielonego i niebieskiego, jaki używamy do stworzenia naszego nowego koloru. I tak na przykład RGB(255, 255, 255) zwraca nam kolor biały, a RGB(0, 0, 0>) - czarny. Zwracana wartość jest typu COLORREF. Oto przykład tworzenia zielonego pióra za pomocą RGB:

C/C++
PioroZiel = CreatePen( PS_SOLID, 1, RGB( 0, 255, 0 ) );

Mamy jeszcze dodatkowe trzy makra, dzięki którym z konkretnego koloru możemy "wyciągnąć", ile jest w nim czerwonego, zielonego i niebieskiego składnika. Są to odpowiednio: GetRValue, GetGValue, GetBValue.

Figury geometryczne

Dysponując funkcją rysującą linie, możemy sobie stworzyć np. prostokąt, jednak byłoby to ździebko niewygodne :-). Po co się męczyć, WinAPI ma cały zestaw funkcji do rysowania figur...

Prostokąty

Funkcja Rectangle służy do rysowania prostokątów, jak sama nazwa wskazuje :-). Ramka takiego prostokąta jest rysowana aktualnym piórem (podobnie jak linie w poprzednim rozdziale), a reszta - wypełniana za pomocą aktualnego pędzla (brush). Narysujemy sobie śliczny, zielony kwadracik z czerwoną, przerywaną ramką:

C/C++
HDC hdcOkno = GetDC( hwnd );
HBRUSH PedzelZiel, Pudelko;
HPEN OlowekCzerw, Piornik;
PedzelZiel = CreateSolidBrush( 0x00FF00 );
OlowekCzerw = CreatePen( PS_DOT, 1, 0x0000FF );
Pudelko =( HBRUSH ) SelectObject( hdcOkno, PedzelZiel );
Piornik =( HPEN ) SelectObject( hdcOkno, OlowekCzerw );
Rectangle( hdcOkno, 10, 10, 100, 100 );
SelectObject( hdcOkno, Pudelko );
SelectObject( hdcOkno, Piornik );
DeleteObject( OlowekCzerw );
DeleteObject( PedzelZiel );
ReleaseDC( hwnd, hdcOkno );

Ponieważ chcemy narysować kwadracik dokładnie wypełniony kolorem zielonym, użyliśmy funkcji CreateSolidBrush. Dzięki temu nasz pędzelek rysuje na całym podanym obszarze bez odrywania się od powierzchni :-). Gdybyśmy chcieli tym pędzelkiem wymalować kwadracik zakreskowany, skorzystamy z funkcji CreateHatchBrush, natomiast jeśli będziemy chcieli narysować prostokąt w ciapki, i to właśnie takie ciapki, jakie wcześniej sobie opracowaliśmy, użyjemy funkcji CreatePatternBrush, podając jej jako argument uchwyt do bitmapy z ciapkami :-). Na razie jednak chcemy zwykłe, pełne tło, więc piszemy CreateSolidBrush.

Zauważ, że funkcja Rectangle pobiera również współrzędne punktu, od którego zaczynamy rysować nasz prostokąt, a więc nie rysujemy od ostatniej, domyślnej pozycji. Poza tym ta ostatnia pozycja po narysowaniu prostokąta nie zmienia się, więc przy rysowaniu prostokątów używanie MoveToEx jest bez sensu.

Koła i elipsy

Dzięki funkcji Ellipse możemy sobie, jak sama nazwa znów wskazuje, namalować elipsę. Rysowanie odbywa się (tak jak w przypadku prostokąta) przy użyciu aktualnie wybranego pióra (obwódka) i pędzla (wypełnianie wnętrza figury). Parametry funkcji to: uchwyt okna oraz kolejne cztery współrzędne prostokąta, opisanego na elipsie, którą chcemy narysować. Oczywiście, jeśli podane współrzędne będą opisywać kwadrat, narysowana elipsa będzie kołem, i o to nam zresztą najczęściej chodzi:

C/C++
HDC hdcOkno = GetDC( hwnd );
HBRUSH PedzelZiel, Pudelko;
HPEN OlowekCzerw, Piornik;
PedzelZiel = CreateSolidBrush( 0x00FF00 );
OlowekCzerw = CreatePen( PS_DOT, 1, 0x0000FF );
Pudelko =( HBRUSH ) SelectObject( hdcOkno, PedzelZiel );
Piornik =( HPEN ) SelectObject( hdcOkno, OlowekCzerw );
Ellipse( hdcOkno, 10, 10, 100, 100 );
SelectObject( hdcOkno, Pudelko );
SelectObject( hdcOkno, Piornik );
DeleteObject( OlowekCzerw );
DeleteObject( PedzelZiel );
ReleaseDC( hwnd, hdcOkno );

Fajne kółko? (Windows 98)
Fajne kółko? (Windows 98)

Wielokąty

Warto by jeszcze wiedzieć, jak się rysuje wielokąty. Jeśli zależy nam tylko na niewypełnionych wielokątach (same kontury), to możemy skorzystać z funkcji Polyline. Jeśli zaś użyjemy funkcji Polygon, to figura powstała przez połączenie podanych przez nas punktów zostanie dodatkowo wypełniona przy pomocy aktualnego pędzla. Obydwie te funkcje wymagają podania tablicy punktów (wierzchołków wielokąta). Musi to być talica o elementach typu POINT. Struktura POINT jest wyjątkowo prosta, ma tylko dwie składowe: x i y. Jako jeden z argumentów funkcji Polygon (i Polyline) podajemy też liczbę wierzchołków, które bierzemy pod uwagę (zazwyczaj jest to po prostu liczba elementów tablicy). Poniższy przykład pokazuje, jak narysować taką śmieszną gwiazdkę:

C/C++
// Robimy tablicę współrzędnych wierzchołków
POINT Wierzch[ 5 ];
Wierzch[ 0 ].x = 50; Wierzch[ 0 ].y = 0;
Wierzch[ 1 ].x = 25; Wierzch[ 1 ].y = 100;
Wierzch[ 2 ].x = 100; Wierzch[ 2 ].y = 50;
Wierzch[ 3 ].x = 0; Wierzch[ 3 ].y = 50;
Wierzch[ 4 ].x = 75; Wierzch[ 4 ].y = 100;

// Przygotowujemy wszystko do rysowania
HDC hdc = GetDC( hwnd );
HBRUSH Pedzel, Pudelko;
HPEN Pioro, Piornik;
Pedzel = CreateSolidBrush( 0x00FFFF );
Pioro = CreatePen( PS_SOLID, 1, 0x000000 );
Pudelko =( HBRUSH ) SelectObject( hdc, Pedzel );
Piornik =( HPEN ) SelectObject( hdc, Pioro );

// Wywalamy naszą gwiazdę na ekran
Polygon( hdc, Wierzch, 5 );

// ...i sprzątamy po sobie
SelectObject( hdc, Pudelko );
SelectObject( hdc, Piornik );
DeleteObject( Pioro );
DeleteObject( Pedzel );
ReleaseDC( hwnd, hdc );

Dosyć niezgrabne, ale też fajne (Windows 98)
Dosyć niezgrabne, ale też fajne (Windows 98)
 
Dobra, tyle funkcji rysujących figury starczy na początek. Pozostałe, jeśli będziesz ich potrzebował, możesz sobie znaleźć np. w MSDN.

Pojedyncze punkty

Jeśli chcemy narysować coś bardziej skomplikowanego, niż tylko prosta figura, będziemy potrzebowali funkcji, ustawiającej kolor pojedynczego pixela. Nazywa się ona SetPixel, a jej parametry to: HDC, współrzędne x i y, kolor. Wykorzystując tę funkcję możemy sobie np. narysować graffiti przy pomocy spray'u ;-). Póki co jednak, warto by ten spray najpierw wypróbować, więc narysujemy sobie nim tylko zwykłe kółko. Aby kształt naszej nasprayowanej figury przypominał koło, wykorzystamy równanie koła, znane większości z was z matematyki:

C/C++
#include <stdlib.h>

HDC hdc = GetDC( hwnd );
for( int y = 0; y < 150; y++ )
for( int x = 0; x < 150; x++ )
if(( x - 75 ) *( x - 75 ) +( y - 75 ) *( y - 75 ) < 75 * 75 && rand() % 2 == 1 )
     SetPixel( hdc, x, y, 0x000000FF );

ReleaseDC( hwnd, hdc );

Graffiti w WinAPI ;-) (Windows 98)
Graffiti w WinAPI ;-) (Windows 98)
 
W przykładziku tym bierzemy sobie po kolei wszystkie punkty z kwadratu 150x150 pikseli, sprawdzamy czy dany punkt leży wewnątrz koła i z prawdopodobieństwem 1:2 (jeśli funkcja rand zwróci 1) malujemy piksela na czerwono.

Kolorowanie każdego z pikseli osobno daje nam wiele innych możliwości, np. taką:

C/C++
HDC hdc = GetDC( hwnd );
for( int y = 0; y < 150; y++ )
for( int x = 0; x < 150; x++ )
if(( x - 75 ) *( x - 75 ) +( y - 75 ) *( y - 75 ) < 75 * 75 )
     SetPixel( hdc, x, y, RGB( x, 0, 200 - x ) );

ReleaseDC( hwnd, hdc );

Niczego sobie to kółeczko... ;-) (Windows 98)
Niczego sobie to kółeczko... ;-) (Windows 98)
 
Uzyskaliśmy w ten sposób efekt przejścia od jednego koloru do drugiego. Gdybyśmy w makrze RGB podali zmienną y zamiast x, to uzyskalibyśmy takie samo przejście, tylko w pionie. Oczywiście efekt taki dla obszarów większych niż 255 pikseli trochę trudniej zaprogramować, a jeszcze trudniej jeśli chcemy uzyskać przejście między dwoma konkretnymi kolorami, dowolnie wybranymi z palety, ale wszystko jest do zrobienia jeśli się trochę pomyśli ;-).

I to wszystko jeśli chodzi o rysowanie najprostszych kształtów...
Poprzedni dokument Następny dokument
Pliki Bitmapy