Mazanie po kontrolce
Bez względu na to, jak duże możliwości można uzyskać przez wykorzystywanie standardowych kontrolek do tworzenia własnych, i tak wiem, że co innego przyciągnęło cię do tego artykułu :-). Zapewne chciałbyś po prostu zmienić wygląd tych standardowych, kanciastych, szarych, nudnych kontrolek windowsowych. Oczywiście, jest to możliwe.
Spróbujmy stworzyć swój własny przycisk. Jego wygląd będzie określony przez przygotowaną wcześniej bitmapę. Żeby było ciekawiej, zmienimy też kształt tego przycisku na okrągły. Moja bitmapka do tego przycisku wygląda tak:
Bitmapa ta będzie wspólna dla wszystkich przycisków tworzonej przez nas klasy, tak więc dobrze by było przechowywać ją (a dokładniej: uchwyt do niej) w dodatkowej pamięci klasy (wspomnieliśmy sobie o tej pamięci wcześniej). Zasady korzystania z pamięci klasy są podobne, jak w przypadku pamięci okna.
Jak pamiętamy z odcinka o grafice, rysowanie bitmapy polega na jej kopiowaniu z kontekstu pomocniczego na kontekst ekranu. Ten pierwszy oczywiście musimy sobie stworzyć sami i podobnie jak bitmapę, będziemy jego uwchyt przechowywać w pamięci klasy, żeby nie trzeba było tego kontekstu tworzyć za każdym razem, gdy chcemy narysować kontrolkę.
Zapewne zauważyłeś, że bitmapa do naszego przycisku ma urocze, pedalsko różowe tło. To oczywiście nie w ramach poprawności politycznej, tylko aby stworzyć do tej bitmapy maskę. Maska, jak pamiętamy, to również uchwyt typu
HBITMAP i również trzeba ją zapamiętać dla całej klasy. Dlatego właśnie potrzebujemy
16 bajtów dodatkowej pamięci (
4*3+4).
W poprzednim odcinku kursu rejestrowaliśmy nową kontrolkę bezpośrednio z funkcji
WinMain. Nie jest to może niedopuszczalne, ale i niezbyt eleganckie, gdyż kontrolki tworzy się z reguły w oddzielnym pliku, a najlepiej w ogóle w osobnym module DLL. Dlatego dobrze by było stworzyć jakąś funkcję, która będzie odwalała całą robotę związaną z przygotowaniem nowej kontrolki, to jest rejestrację jej klasy oraz inicjalizację (wczytanie bitmapy, stworzenie maski, stworzenie kontekstu pomocniczego, umieszczenie tego wszystkiego w pamięci klasy).
Wreszcie jeszcze jedna uwaga: do tej pory korzystaliśmy z superclassingu, a więc braliśmy istniejącą klasę i rozszerzaliśmy jej możliwości. Tym razem zrobimy wszystko sami - od zera. Nie będzie pobierania informacji o klasie, więc wszystkie pola struktury
WNDCLASS musimy teraz wypełnić samodzielnie. Do dzieła zatem:
BOOL RejestrujPrzycisk( HINSTANCE hInst )
{
WNDCLASS wc;
wc.style = 0; wc.lpszMenuName = NULL;
wc.lpszClassName = "BitmapowyPrzycisk";
wc.hInstance = hInst;
wc.lpfnWndProc = ControlProc;
wc.cbWndExtra = 8;
wc.cbClsExtra = 16;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground =( HBRUSH ) GetStockObject( NULL_BRUSH );
if( !RegisterClass( & wc ) ) return FALSE;
HBITMAP hbmBmp =( HBITMAP ) LoadImage( NULL, "przycisk.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE ),
hbmMask = CreateBitmapMask( hbmBmp, RGB( 255, 0, 255 ) );
HDC hdcMem = CreateCompatibleDC( NULL );
HWND hTmp = CreateWindowEx( 0, "BitmapowyPrzycisk", NULL, WS_POPUP,
0, 0, 0, 0, NULL, NULL, hInst, NULL );
SetClassLong( hTmp, 0,( LONG ) hdcMem );
SetClassLong( hTmp, 4,( LONG ) hbmBmp );
SetClassLong( hTmp, 8,( LONG ) hbmMask );
return TRUE;
}
Część kodu odpowiedzialna za rejestrowanie nowej klasy nie jest dla nas niespodzianką - przerabialiśmy to już w poprzednim odcinku kursu (nie mówiąc już o tworzeniu okna we wprowadzeniu do WinAPI). Wczytywanie bitmapy to też nie nowość - było to już w odcinku o GDI (tam też znajduje się "przepis" na funkcję
CreateBitmapMask). Drobnego komentarza może wymagać natomiast funkcja
CreateWindowEx. Co ona tu robi? Ano, korzystamy tutaj z usług
SetClassLong, aby zapamiętać uchwyty w pamięci klasy, zaś funkcji tej nie wystarczy niestety sama nazwa klasy - potrzebny jest uwchyt dowolnego okna tej klasy. Dlatego też tworzymy takie okno (do niczego innego nie jest ono wykorzystywane, dlatego też użyliśmy takich nietypowych parametrów dla
CreateWindowEx). Taka mała niedogodność WinAPI.
Zarezerwowaliśmy też po
8 bajtów dla każdego okna tworzonej przez nas klasy
BitmapowyPrzycisk. Będzie tam przechowywana informacja, czy przycisk jest aktualnie wciśnięty - aby kod odpowiedzialny za rysowanie przycisku wiedział, którą połówkę bitmapy ma w danym momencie pokazać. Oczywiście jest to spora rozrzutność - jeden bajt (plus cztery nieużywane, jak zwykle) wystarczyłby, ale nie mamy powodu, żeby tak skąpić (a na wartościach 32-bitowych łatwiej się operuje). Powinniśmy zainicjalizować tę pamięć w
WM_CREATE:
case WM_CREATE:
{
SetWindowLong( hwnd, 0,( LONG ) FALSE );
BITMAP bmInfo;
HBITMAP hbmBmp =( HBITMAP ) GetClassLong( hwnd, 4 );
GetObject(( HGDIOBJ ) hbmBmp, sizeof( BITMAP ), & bmInfo );
SetWindowPos( hwnd, NULL, 0, 0, bmInfo.bmWidth / 2, bmInfo.bmHeight, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER );
HRGN hRgn = CreateEllipticRgn( 0, 0, bmInfo.bmWidth / 2 - 1, bmInfo.bmHeight - 1 );
SetWindowRgn( hwnd, hRgn, FALSE );
}
break;
Robimy tu przy okazji trochę więcej rzeczy. Pobieramy wymiary bitmapy wyświetlanej na naszym przycisku, a następnie dostosowujemy całe okno (przycisk) do tego wymiaru. Dzięki temu niezależnie od tego, jakie parametry poda użytkownik naszej kontrolki przy wywoływaniu
CreateWindowEx, przycisk zawsze będzie miał te same rozmiary (chyba, że użytkownik "ręcznie" zmieni jego rozmiary, czemu możemy z kolei zapobiec, obsługując komunikat
WM_SIZE). Wreszcie tworzymy sobie region w kształcie koła i przypisujemy ten region naszemu przyciskowi. Sprawia to, że nie są rysowane fragmenty okna spoza podanego regionu, a także - że kliknięcie poza tym regionem nie powoduje wysłania komunikatu do kontrolki (trafia on do okna-rodzica). Zauważ, że nie zapamiętujemy nigdzie uchwytu tego regionu - odkąd jest on przypisany do okna, możemy go pobrać przez
GetWindowRgn.
Kolejnym krokiem w tworzeniu naszego przycisku będzie obsługa kliknięć myszą. Musimy w związku z tym zrobić trzy rzeczy. Po pierwsze - zmieniamy stan przycisku w jego pamięci okna (wciśnięty/niewciśnięty), odrysowujemy przycisk (wywołujemy niejawnie
WM_PAINT przez
InvalidateRgn) oraz przechwytujemy mysz (o tym zaraz).
Wspomniałem o przechwytywaniu myszy. Po co to? Otóż nasz przycisk ma działać tak, jak standardowe przyciski Windows - kiedy użytkownik trzyma wciśnięty przycisk myszy nad kontrolką, a następnie kursor mu się ześlizguje poza obręb kontrolki, przycisk się "wyciska". Żeby otrzymać taki sam efekt, musimy jakoś sprawić, żeby komunikat
WM_LBUTTONUP został wysłany kontrolce także w tym momencie, gdy kursor wyjeżdża poza jej obręb. Dlatego właśnie w
WM_LBUTTONDOWN wywołujemy
SetCapture - od tego momentu do kontrolki docierają WSZYSTKIE komunikaty związane z myszą, bez względu na to, gdzie jest akurat kursor. W obsłudze
WM_LBUTTONUP "oddajemy" myszkę z powrotem systemowi dzięki
ReleaseCapture i wszystko wraca wówczas do normy:
case WM_LBUTTONDOWN:
{
SetWindowLong( hwnd, 0,( LONG ) TRUE );
InvalidateRgn( hwnd, NULL, TRUE );
SetCapture( hwnd );
}
break;
case WM_LBUTTONUP:
{
SetWindowLong( hwnd, 0,( LONG ) FALSE );
InvalidateRgn( hwnd, NULL, TRUE );
ReleaseCapture();
SendMessage( GetParent( hwnd ), WM_COMMAND,( WPARAM ) MAKELONG(( WORD ) GetWindowLong( hwnd, GWL_ID ),
BN_CLICKED ),( LPARAM ) hwnd );
}
break;
Obydwa komunikaty są zbudowane podobnie, ale
WM_LBUTTONUP pełni jeszcze jedną, dodatkową funkcję - wysyła komunikat
WM_COMMAND do okna-rodzica. Dzięki temu możemy przypisać naszemu przyciskowi jakieś zdarzenie i obsłużyć je w komunikacie
WM_COMMAND, tak jak dla standardowego przycisku.
To oczywiście jeszcze nie koniec, jeśli chodzi o mysz. Musimy przecież obsłużyć to przechwycone ześlizgnięcie się kursora z przycisku. Żeby przekonać się, czy rzeczywiście dany ruch myszą jest tym ześlizgnięciem, sprawdzamy, czy kursor znajduje się w regionie okna. Region ten należy pobrać funkcją
GetWindowsRgn. Funkcja ta żąda jednak podania uchwytu do dowolnego, istniejącego już regionu, który następnie zostanie zastąpiony kopią regionu pobieranego (trochę zakręcone, ale trudno). Dlatego właśnie tworzymy tu sobie taki tymczasowy, kilkupikselowy region.
Trzeba zwrócić uwagę na to, że współrzędne regionu są wyrażane względem okna (przycisku), natomiast współrzędne kursora, pobierane funkcją
GetCursorPos - względem ekranu. Stąd konwersja za pomocą
ScreenToClient. Teraz dopiero możemy sprawdzić, czy kursor jest nad kontrolką, do czego wykorzystujemy funkcję
PtInRegion. Jeśli kursor znajdzie się poza przyciskiem, to wysyłamy komunikat
WM_LBUTTONUP (co sprawia, że przycisk się "wyciska") Natomiast ten drugi warunek w instrukcji
if służy do tego, żeby przycisk rysowany był tylko wtedy, gdy rzeczywiście zmienia się jego stan, co eliminuje możliwe miganie.
case WM_MOUSEMOVE:
{
POINT cur;
HRGN hRgn = CreateRectRgn( 0, 0, 1, 1 );
GetCursorPos( & cur );
ScreenToClient( hwnd, & cur );
GetWindowRgn( hwnd, hRgn );
if( !PtInRegion( hRgn, cur.x, cur.y ) &&( BOOL ) GetWindowLong( hwnd, 0 ) )
SendMessage( hwnd, WM_LBUTTONUP, wParam, lParam );
}
break;
Pora zająć się najważniejszą częścią zadania, czyli właściwym kodem rysującym przycisk. Potrzebujemy w tym doniosłym momencie wszystkich informacji, jakie wpakowaliśmy do pamięci klasy, a więc uchwytów: do bitmapy, do maski i do kontekstu pomocniczego. Potrzebujemy też "wyciągnąć" z pamięci okna, czy przycisk jest aktualnie wciśnięty, czy też nie. Następnie pobieramy wymiary bitmapy, no i wreszcie rysujemy za pomocą
BitBlt.
Dodam jeszcze, że szerokość bitmapy dzielimy przez
2, ponieważ za każdym razem rysujemy tylko połówkę bitmapy (raz jedną, raz drugą).
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint( hwnd, & ps );
HBITMAP hbmBmp, hbmMask;
HDC hdcMem;
hdcMem =( HDC ) GetClassLong( hwnd, 0 );
hbmBmp =( HBITMAP ) GetClassLong( hwnd, 4 );
hbmMask =( HBITMAP ) GetClassLong( hwnd, 8 );
BITMAP bmInfo;
GetObject(( HGDIOBJ ) hbmBmp, sizeof( BITMAP ), & bmInfo );
BOOL bWcisniety =( BOOL ) GetWindowLong( hwnd, 0 );
WORD x =( bWcisniety ) ? bmInfo.bmWidth / 2
: 0;
SelectObject( hdcMem, hbmMask );
BitBlt( hdc, 0, 0, bmInfo.bmWidth / 2, bmInfo.bmHeight, hdcMem, x, 0, SRCAND );
SelectObject( hdcMem, hbmBmp );
BitBlt( hdc, 0, 0, bmInfo.bmWidth / 2, bmInfo.bmHeight, hdcMem, x, 0, SRCPAINT );
EndPaint( hwnd, & ps );
}
break;
Oczywiście wszystkie podane wyżej komunikaty dotyczą
ControlProc i tam też mamy je umieścić. Pora wytestować, cośmy tutaj zmajstrowali. Użycie naszej pachnącej świeżością kontrolki polegać będzie na tym, że wywołamy sobie funkcję
RejestrujPrzycisk, a następnie będziemy tworzyć kolejne przyciski funkcją
CreateWindowEx:
if( !RejestrujPrzycisk( hThisInstance ) )
{
MessageBox( g_hwnd, "Nie udało się zarejestrować klasy przycisku.", "Yh...", MB_ICONSTOP );
DestroyWindow( g_hwnd );
}
HWND hControl = CreateWindowEx( 0, "BitmapowyPrzycisk", NULL,
WS_CHILD | WS_VISIBLE,
5, 5, 32, 32, g_hwnd,
( HMENU ) IDC_BUTTON1, hThisInstance, NULL );
if( hControl == NULL )
{
MessageBox( g_hwnd, "Nie udało się stworzyć przycisku.", "Yh...", MB_ICONSTOP );
DestroyWindow( g_hwnd );
}
HWND hControl2 = CreateWindowEx( 0, "BitmapowyPrzycisk", NULL,
WS_CHILD | WS_VISIBLE,
50, 5, 32, 32, g_hwnd,
( HMENU ) IDC_BUTTON2, hThisInstance, NULL );
if( hControl2 == NULL )
{
MessageBox( g_hwnd, "Nie udało się stworzyć przycisku 2.", "Yh...", MB_ICONSTOP );
DestroyWindow( g_hwnd );
}
To wystarczy, by po uruchomieniu aplikacji ukazała nam się para pięknych (no, nie przesadzajmy może - takich sobie), okrągłych, kolorowych przycisków. Stworzyliśmy je całkiem
samodzielnie, nie korzystając w ogóle z klas dostarczonych razem z Windowsem. Teraz pozostaje tylko umieścić nasz kod w jakimś DLL-u i mamy gotowy komponent do swoich przyszłych aplikacji:
Możemy jeszcze obsłużyć sobie
WM_COMMAND (tym razem już w procedurze głównego okna), żeby sprawdzić, czy jest on wysyłany prawidłowo:
case WM_COMMAND:
{
if( HIWORD( wParam ) == BN_CLICKED )
{
char buf[ 255 ];
wsprintf( buf, "Wciśnięty przycisk o identyfikatorze %hu.", LOWORD( wParam ) );
MessageBox( hwnd, buf, "Test", MB_ICONINFORMATION );
}
}
break;
Powinno być OK :-). Oczywiście trudno jeszcze nazwać naszą kontrolkę profesjonalnie zrobioną. Brakuje jeszcze wielu rzeczy, które powinna ona posiadać, ale to już zadanie dla ciebie ;-).