Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: 'NO NAME'
Biblioteki C++

Łamacze komunikatów

[lekcja] Rozdział 19. Rozdział poświęcony makrom ułatwiającym tworzenie aplikacji okienkowych.

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:
C/C++
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:
C/C++
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:
C/C++
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:
C/C++
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:
C/C++
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:
C/C++
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:
C/C++
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:
C/C++
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.
Poprzedni dokument Następny dokument
Rejestr Zasoby