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

Własne kontrolki, cz. 2

[lekcja]

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 z przyciskiem
Bitmapa z przyciskiem

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:

C/C++
BOOL RejestrujPrzycisk( HINSTANCE hInst )
{
   
WNDCLASS wc;
   
wc.style = 0; // stylowy kibel? ;-)
   
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;
   
   
// wczytanie bitmapy, stworzenie maski i kontekstu dla całej klasy
   
HBITMAP hbmBmp =( HBITMAP ) LoadImage( NULL, "przycisk.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE ),
   
hbmMask = CreateBitmapMask( hbmBmp, RGB( 255, 0, 255 ) );
   
HDC hdcMem = CreateCompatibleDC( NULL );
   
   
// tworzymy tymczasowe okno nowej klasy
   
HWND hTmp = CreateWindowEx( 0, "BitmapowyPrzycisk", NULL, WS_POPUP,
   
0, 0, 0, 0, NULL, NULL, hInst, NULL );
   
   
// zapamiętanie kontekstu i bitmap w dodatkowej pamięci okna
   
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:

C/C++
case WM_CREATE:
{
   
// inicjalizacja stanu przycisku - na niewciśnięty
   
SetWindowLong( hwnd, 0,( LONG ) FALSE );
   
// pobierz wymiary bitmapy
   
BITMAP bmInfo;
   
HBITMAP hbmBmp =( HBITMAP ) GetClassLong( hwnd, 4 );
   
GetObject(( HGDIOBJ ) hbmBmp, sizeof( BITMAP ), & bmInfo );
   
// zmień rozmiar przycisku na taki sam, jak ma bitmapa
   
SetWindowPos( hwnd, NULL, 0, 0, bmInfo.bmWidth / 2, bmInfo.bmHeight, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER );
   
// ustaw okrągły kształt
   
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:

C/C++
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();
   
// Wyślij komunikat WM_COMMAND
   
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.

C/C++
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ą).

C/C++
case WM_PAINT:
{
   
PAINTSTRUCT ps;
   
HDC hdc = BeginPaint( hwnd, & ps );
   
   
// wczytaj uchwyty z dodatkowej pamięci klasy
   
HBITMAP hbmBmp, hbmMask;
   
HDC hdcMem;
   
hdcMem =( HDC ) GetClassLong( hwnd, 0 );
   
hbmBmp =( HBITMAP ) GetClassLong( hwnd, 4 );
   
hbmMask =( HBITMAP ) GetClassLong( hwnd, 8 );
   
   
// pobierz wymiary bitmapy
   
BITMAP bmInfo;
   
GetObject(( HGDIOBJ ) hbmBmp, sizeof( BITMAP ), & bmInfo );
   
   
// sprawdź, czy przycisk jest wciśnięty
   
BOOL bWcisniety =( BOOL ) GetWindowLong( hwnd, 0 );
   
WORD x =( bWcisniety ) ? bmInfo.bmWidth / 2
       
: 0;
   
   
// rysowanie bitmapy z maską
   
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:

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

Nasze dwa okrągłe przyciski w całej okazałości (Windows 98)
Nasze dwa okrągłe przyciski w całej okazałości (Windows 98)

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:

C/C++
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 ;-).
Poprzedni dokument Następny dokument
Własne kontrolki, cz. 1 Kolory kontrolek