Porządki w procedurze okna
Pewnie zdążyłeś już zauważyć, że gdy pisze się w WinAPI jakiś bardziej skomplikowany program, procedura okna (lub procedury, bo na jednej się zwykle nie kończy) strasznie się rozrastają, a w pewnym momencie poruszanie się po tym kodzie i szukanie w nim czegokolwiek staje się katorgą. O ileż przyjemniej byłoby, gdyby komunikaty obsługiwało się tak, jak w językach opartych na obiektowości, tj. z wykorzystaniem delegatów – każde zdarzenie jest obsługiwane przez osobną funkcję.
Niestety, o tym możemy zapomnieć – WinAPI było projektowane w czasach, gdy nikt (lub prawie nikt) o takich sposobach obsługi zdarzeń jeszcze nie słyszał.
No, nie jest aż tak do końca tragicznie. Mamy jeszcze plik nagłówkowy
<windowsx.h>, a w nim definicje tak zwanych łamaczy komunikatów (ang. 'message crackers'). Jest to zbiór bardzo prostych, acz bardzo ułatwiających życie makr. Dzięki nim pisanie procedury okna sprowadza się właściwie do wyliczenia komunikatów, które chcemy obsługiwać, a sama obsługa zostaje przeniesiona do osobnych funkcji – każda z tych funkcji jest odpowiedzialna za inny komunikat.
Żeby zobaczyć jak to wygląda w praktyce, przyjrzyjmy się najprostszej procedurze okna:
LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
case WM_COMMAND:
{
}
break;
case WM_CLOSE:
DestroyWindow( hwnd );
break;
case WM_DESTROY:
PostQuitMessage( 0 );
break;
default:
return DefWindowProc( hwnd, msg, wParam, lParam );
}
return 0;
}
Mamy tu obsługę trzech komunikatów, przy czym nas najbardziej interesuje
WM_COMMAND. Wszystko jest super, gdy mamy np. dwa przyciski, ale moglibyśmy ich mieć 100 i więcej, a wówczas procedura okna nie wyglądałaby zbyt ciekawie. Używając łamaczy komunikatów upraszczamy ją następująco:
LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
switch( msg )
{
HANDLE_WM_COMMAND( OnCommand );
HANDLE_WM_CLOSE( OnClose );
HANDLE_WM_DESTROY( OnDestroy );
default:
return DefWindowProc( hwnd, msg, wParam, lParam );
}
return 0;
}
Oczywiście nic się samo nie zrobi i musimy teraz napisać funkcje
OnCommand,
OnClose i
OnDestroy. Niemniej procedura okna skróciła się znacznie, o co nam właśnie chodziło. Teraz będzie w niej tylko rozdzielanie komunikatów do poszczególnych funkcji-handlerów i nie będziemy się wreszcie gubić w gąszczu case'ów, gdzie w dodatku bardzo łatwo o popełnienie trudnego do wykrycia błędu składni.
Spróbujmy więc napisać przykładową funkcję
OnCommand. Otwieramy w tym celu plik
<windowsx.h> (jeśli mamy dobre IDE i właściwie zainstalowany SDK, to wystarczy na wpisanej wcześniej nazwie pliku kliknąć prawym przyciskiem myszy i wybrać "Otwórz" z menu kontekstowego) i szukamy definicji
HANDLE_WM_COMMAND. Nad nią mamy w komentarzu gotowy nagłówek dla naszej funkcji. Kopiujemy go sobie:
void OnCommand( HWND hwnd, int id, HWND hwndCtl, UINT codeNotify )
{
}
Jak widać, oprócz wydzielenia obsługi komunikatu do osobnej funkcji, zyskaliśmy coś jeszcze. Zamiast topornej pary parametrów
lParam i
wParam, które zawsze się nam mylą i zmuszają nas do licznych rzutowań, operacji na pojedynczych bitach i innych przyjemności, mamy "normalną" listę parametrów o ładnych, sensownych nazwach i typach. Również typ wartości zwracanej jest dostosowany do wymogów danego komunikatu (w przypadku
WM_COMMAND akurat wartość zwracana nie ma większego znaczenia, więc mamy
void).
Zaleta samego wydzielenia funkcji jest oczywista: by odnaleźć miejsce, gdzie coś obsługujemy, po prostu wybieramy odpowiednią funkcję w naszym IDE, zamiast grzebać w liczącej parę tysięcy linii procedurze okna, która teraz będzie miała najwyżej kilkadziesiąt linii.
Przekierowywanie komunikatów
Podobne problemy, jakie doprowadziły do powstania łamaczy komunikatów, występują w WinAPI praktycznie na każdym kroku. Jeśli nie brzydzisz się makrami, możesz wykorzystać zawartość pliku
windowsx.h na znacznie większą skalę. Kolejnym zastosowaniem tamtejszych makrodefinicji jest obejście niewygód związanych z użyciem funkcji
SendMessage (i pokrewnych). Jak doskonale wiemy, przyjmuje ona parametry
lParam i
wParam... I tutaj podobnie jak z obsługą komunikatu, musimy ciągle zastanawiać się: który jest który, który co przechowuje i w których bitach. Plik
windowsx.h zawiera jednak zestaw makr przekierowujących (po jednym dla każdego komunikatu).
I tak na przykład, skoro już obsłużyliśmy sobie komunikat
WM_COMMAND, to możemy w pewnym momencie chcieć wysłać taki komunikat, by zasymulować np. wciśnięcie klawisza na dialogu. Gdy korzystamy z makra przekierowującego, wygląda to tak:
FORWARD_WM_COMMAND( hwnd, IDC_BUTTON1,
GetDlgItem( hwnd, IDC_BUTTON1 ), BN_CLICKED, SendMessage );
Gdybyśmy zaś chcieli "po staremu" wysłać komunikat bezpośrednio przez
SendMessage, musielibyśmy zrobić tak:
SendMessage( hwnd, WM_COMMAND, MAKEWPARAM( IDC_BUTTON1, BN_CLICKED ),
( LPARAM ) GetDlgItem( hwnd, IDC_BUTTON1 ) );
Niby tekstu tyle samo, ale dodatkowo mamy konwersję, sklejanie dwóch wartości w jedną... W dodatku jak zwykle nie pamiętamy, czy uchwyt jest w
lParam, czy
wParam. Gdy korzystamy z
FORWARD_WM_COMMAND (i podobnych makr), nasze IDE podpowiada nam, który parametr co oznacza, a żadnymi konwersjami i pakowaniem bitów się nie przejmujemy. W miejscu ostatniego parametru makra, czyli tam, gdzie u nas jest
SendMessage, możemy wstawić też
PostMessage czy
SendNotifyMessage – zależy, czego w danym momencie potrzebujemy.
Makra dla kontrolek i inne
Na łamaczach i "przekierowywaczach" ułatwienia drzemiące w
windowsx.h się nie kończą. Jest tam też bogaty zestaw wszelakich innych makr rozmaitego zastosowania, dzięki którym będzie nam się wygodniej pisało i nasze programy będą czytelniejsze. W WinAPI i tak prawie wszystkie funkcje są tak naprawdę makrami, więc co nam szkodzi dołożyć kolejny poziom makr :-).
Każda z podstawowych kontrolek dorobiła się własnej rodziny makr, które są znacznie poręczniejsze, niż wysyłanie komunikatów bezpośrednio za pomocą
SendMessage czy nawet wywoływania przydatnych, lecz niezbyt trafnie nazwanych funkcji typu
EnableWindow czy
SetWindowText. Oto kilka przykładów:
Static_Enable( hMyLabel, TRUE );
Button_SetText( hMyButton, "OK" );
ListBox_AddString( hMyList, "Element listy" );
Oprócz podstawowych kontrolek, podobne makra mają też kontrolki z grupy 'Common Controls' (tyle, że w innym pliku).
Garść innych przydatnych makr:
HINSTANCE hInst = GetWindowInstance( hwnd );
WNDPROC OldProc = SubclassWindow( hwnd, NewProc );
if( IsMinimized( hwnd ) ) return;
Pierwszy przykład pokazuje, jak dobrać się do uchwytu instancji dla danego okna. Gdyby nie było tego makra, musielibyśmy skorzystać z
GetWindowLongPtr, co jest o tyle niefajne, że musielibyśmy pamiętać mało intuicyjną nazwę stałej
GWLP_HINSTANCE, którą trzeba podać jako parametr. Dodatkowo męczylibyśmy się z konwersjami, które makro załatwia za nas.
Drugi przykład uświadamia, że ten cały subclassing to w gruncie rzeczy bardzo prosta operacja :-). Nie wyglądała na taką, gdy używaliśmy
SetWindowLongPtr, prawda?
Ostatni przykład to najprostsza, najbanalniejsza "zmiana nazwy" funkcji. Wszyscy używają w odniesieniu do okien terminów 'minimized' i 'maximized', więc na pierwszy rzut oka nie jest łatwo zgadnąć, co robią funkcje
IsIconic i
IsZoomed... Stąd pomysł na takie makro.
Wielkim polem do popisu dla fanatyków makr jest GDI. Nazwy funkcji tej biblioteki zostały chyba żywcem wycięte z któregoś ze skeczy Monty Pythona, a ich interfejsy zostały zaprojektowane przez facetów, którzy wcześniej programowali kalkulatory. Na szczęście wymiar absurdu można i tutaj nieco zredukować. Co powiecie, żeby nowy pędzelek tworzyć sobie tak:
HBRUSH hMyBrush = CreateSolidBrush( RGB( 0, 0, 0 ) );
HBRUSH hOldBrush = SelectBrush( hDC, hMyBrush );
DeleteBrush( hOldBrush );
No i proszę – żadnego zbędnego rzutowania typów, żadnych głupich nazw w stylu
SelectObject, które robiłyby nam niepotrzebne złudzenia, że mamy do czynienia z podejściem obiektowym.
Widzimy, że dzięki pomocy makr z
<windowsx.h> nasz program stał się mniejszy, ładniejszy i łatwiej się po nim poruszać przez IDE. Co więcej, nie musimy już tak bardzo zaprzątać sobie głowy pokręconymi parametrami funkcji WinAPI, dzięki czemu nasze mózgi mogą skupić się na sprawach istotniejszych, na przykład na tym, co nasz program ma robić. Kryje się tu tylko jedna pułapka:
jeśli lubimy sobie ponarzekać na beznadziejne pomysły ludzi z Microsoftu (a lubimy), to teraz mamy trochę mniej argumentów.