Subclassing pojedynczego okna
Zapewne zdążyłeś już zauważyć, że wszelkie kontrolki twojej aplikacji mogą się okazać w pewnych sytuacjach upierdliwą przeszkodą. Jeśli na przykład tworzysz sobie edytor tekstu i chcesz, by wciśnięcie F12 powodowało zapisanie aktualnie otwartego dokumentu do pliku, to kontrolka
EDIT, która przecież jest sercem twojego programu, w momencie otrzymania fokusa będzie przechwytywała wszelkie komunikaty od klawiatury (w tym również ten wywołany przez naciśnięcie F12) i interpretowała je po swojemu (np. strzałki będą poruszały kursorem, Enter - przejście do następnego wiersza itp.). W ten sposób twój klawisz odpowiedzialny za quick-save dokumentu będzie "działał" tylko wtedy, gdy pole tekstowe (EDIT) nie będzie miało fokusa.
W skrócie: nie mamy kontroli nad komunikatami przechwyconymi przez kontrolkę! Taka sytuacja jest czasami dobra, a czasami zła (jak w przykładzie powyżej). W tym drugim przypadku mamy jednak sposób, żeby temu zaradzić: jest to właśnie subclassing. Z definicji:
Subclassing jest to zmiana wskaźnika do procedury okna dla pojedynczego okna lub całej klasy okien. |
Może to brzmieć nieco zawile (jak większość definicji), ale w gruncie rzeczy jest bardzo proste :-). Każde okno ma swoją własną procedurę okna, czego zresztą doświadczyliśmy na własnej skórze już w pierwszym odcinku kursu WinAPI (musieliśmy dla naszego głównego okna napisać procedurę okna i podać wskaźnik do niej przy rejestracji klasy okna). Jeśli mamy do czynienia z dwoma oknami tej samej klasy, to współdzielą one tę samą procedurę okna, np. wszystkie okna klasy
EDIT (pola tekstowe) korzystają z procedury okna, która sprawia, że wciskanie klawiszy powoduje wyświetlanie w polu tekstowym literek, że działa zaznaczanie tekstu myszą, że kliknięcie prawym przyciskiem myszy wywołuje menu kontekstowe do edycji itp. Mimo to mamy możliwość zmiany procedury okna dla każdego okna z osobna.
Zmiana procedury okna powoduje powstanie podklasy (ang. subclass) dla klasy tego okna. |
Wniosek z tych rozważań jest prosty - podmieniając domyślną procedurę okna danej kontrolki możemy zmusić ją do interpretowania przechwyconych przez nią komunikatów w taki sposób, jaki NAM najbardziej odpowiada.
Pora zabrać się wreszcie do roboty. Naszym zadaniem będzie zmuszenie pola tekstowego (załóżmy, że już je stworzyliśmy pod uchwytem
g_hText), żeby reagowało jakoś na wciśnięcie klawiszy. Dodamy sobie tzw. sprzężenie zwrotne (ktoś jeszcze pamięta czasy Atari? ;-)), czyli wkurzający sygnał dźwiękowy po każdym wciśnięciu klawisza. Na początku spróbujemy zrobić to "normalnie", czyli dopisując odpowiedni kod do głównej procedury okna:
case WM_KEYDOWN:
{
Beep( 0, 0 );
}
break;
Jak widać (lub raczej słychać), wciśnięcie każdego klawisza daje nam sygnał dźwiękowy, o ile pole tekstowe nie ma fokusa. Jeśli spróbujemy cokolwiek w nim napisać i dopiero wtedy wcisnąć coś, sygnału nie będzie. Przyczynę już znamy - pole tekstowe przechwytuje wszystkie naciśnięcia klawiszy i nie chce ich oddać do głównego okna. Zaraz mu powiemy, co o tym myślimy.
Zacznijmy od napisania nowej procedury okna dla pola tekstowego - jej zadaniem będzie wywołanie funkcji
Beep w przypadku naciśnięcia klawisza (czyli to samo, co robi obecnie główna procedura okna):
LRESULT CALLBACK NewWndProc( HWND hwnd, UINT mesg, WPARAM wParam, LPARAM lParam )
{
switch( mesg )
{
case WM_KEYDOWN:
{
Beep( 0, 0 );
}
break;
}
return 0;
}
Na razie nie jest to zbyt dobry kod, ponieważ skopiowaliśmy to, co już mieliśmy w innym miejscu napisane (chodzi o
Beep) - to zły nawyk :-). Nie przejmujemy się tym jednak w tej chwili i idziemy dalej. Musimy teraz "podmienić" procedurę okna pola tekstowego. Służy do tego funkcja
SetWindowLong. Pamiętaj, żeby wywołać ją już PO utworzeniu pola tekstowego:
SetWindowLong( g_hText, GWL_WNDPROC,( LONG ) NewWndProc );
Hmmm, podejrzane - nasze pole tekstowe diabli wzięli! Gdzie się podziało? Bez obaw, jest ciągle tam, gdzie wcześniej, tylko po prostu się nie narysowało :-). Powód - "zabierając" mu domyślną procedurę okna pozbawiliśmy je jednocześnie całej funkcjonalności; teraz jest więc właściwie bezużyteczne. Stąd wniosek, że aby dokonać prawidłowego subclassingu, musimy nie tylko napisać nową procedurę okna, ale też zachować "starą" i wywołać ją w odpowiednim momencie, żeby pole tekstowe mogło sobie obsłużyć pozostałe, nie interesujące nas komunikaty (a niezbędne do jego normalnego funkcjonowania).
Do "ręcznego" wywoływania procedury okna służy funkcja
CallWindowProc. Oczywiście, aby wywołać "starą" procedurę okna pola tekstowego, musimy gdzieś zapamiętać wskaźnik do niej. Tak więc musimy ten wskaźnik zadeklarować (jako zmienną globalną najlepiej):
...oraz poprawić wywołanie SetWindowLong:
g_OldWndProc =( WNDPROC ) SetWindowLong( g_hText, GWL_WNDPROC,( LONG ) NewWndProc );
Teraz dysponujemy już wskaźnikiem do domyślnej procedury okna dla wszystkich pól tekstowych Windowsa i nie zawahamy się go użyć ;-). Wprowadźmy w tym celu korektę do naszej nowej procedury okna:
LRESULT CALLBACK NewWndProc( HWND hwnd, UINT mesg, WPARAM wParam, LPARAM lParam )
{
switch( mesg )
{
case WM_KEYDOWN:
{
Beep( 0, 0 );
}
break;
}
return CallWindowProc( g_OldWndProc, hwnd, mesg, wParam, lParam );
}
Od tej pory wszystko zaczęło działać tak, jak sobie tego życzyliśmy. Pozostaje tylko zadbać o jeden szczegół: otóż jak wspomniałem wcześniej, zrobiliśmy małe powtórzenie (dwa razy wywołujemy funkcję
Beep w odpowiedzi na to samo zdarzenie - osobno dla głównego okna, osobno dla pola tekstowego). Powinniśmy zmienić to tak, żeby wciśnięcie klawisza w polu tekstowym było przekazywane do procedury głównego okna aplikacji. Teraz już wiemy, jak to można zrobić - posłużymy się po raz drugi znaną już funkcją
CallWindowProc:
LRESULT CALLBACK NewWndProc( HWND hwnd, UINT mesg, WPARAM wParam, LPARAM lParam )
{
switch( mesg )
{
case WM_KEYDOWN:
{
CallWindowProc( WndProc, hwnd, mesg, wParam, lParam );
}
break;
}
return CallWindowProc( g_OldWndProc, hwnd, mesg, wParam, lParam );
}
Oczywiście zakładam tutaj, że twoja główna procedura okna nazywa się właśnie
WndProc. Dzięki powyższemu zabiegowi uzyskaliśmy po prostu to, że od tej pory pole tekstowe nie zachowuje naciśnięć klawiszy wyłącznie dla siebie, lecz przekazuje je najpierw do głównego okna, a dopiero potem samo je sobie obsługuje (jeśli musi).
Taki rodzaj subclassingu, któremu poddajemy tylko pojedyncze okienka, nazywamy subclassingiem wystąpienia (instance subclassing).
Tworzenie podklasy okien
Gdybyśmy po użyciu subclassingu wystąpienia na klasie
EDIT stworzyli następne pola tekstowe, to zachowywałyby się już one normalnie (czyli nie wydawałyby żadnych głupich dźwięków ;-)). Czasami może nam jednak zależeć na czymś więcej - zmodyfikowaniu całej klasy kontrolek, np. właśnie pól tekstowych, tak aby wydawały one dźwięki podczas pisania.
Możemy to uczynić dzięki funkcji
SetClassLong. Funkcja ta zmienia parametry klasy, tak więc zmiany będą dotyczyć tylko tych okien, które zostaną utworzone PO modyfikacji klasy. Taki rodzaj subclassingu nazywamy '''globalnym subclassingiem'''.
Argumenty funkcji
SetClassLong są w zasadzie takie same, jak w przypadku
SetWindowLong, zmienia się jednak nieco ich znaczenie. Przede wszystkim parametry klasy to zupełnie co innego, niż parametry okna. Dzięki
SetClassLong możemy więc zmienić np. kolor tła okna, domyślny kursor, menu, ikonę, style klasy (nie mylić ze stylami okna). Jedyna wspólna rzecz z funkcją
SetWindowLong to możliwość zmiany procedury okna, i tę właśnie możliwość wykorzystamy do globalnego subclassingu.
Warto zwrócić uwagę, że
SetClassLong nie pobiera nazwy klasy, którą modyfikujemy, lecz uchwyt do okna tej klasy (który pośrednio również identyfikuje klasę). Tak więc...
Aby dokonać globalnego subclassingu, musimy dysponować uchwytem do już utworzonego okna danej klasy. |
Jeżeli akurat nie mamy takiego okna, to możemy je sobie stworzyć. Nie musi ono być widoczne, tak więc jeśli będzie nam w aplikacji potrzebne wyłącznie do subclassingu, to "zapominamy" dać mu flagę
WS_VISIBLE. Nie musimy też martwić się zbytnio o pozostałe jego parametry ani tym bardziej o rozszerzone style (możemy więc skorzystać z nieco prostszego
CreateWindow zamiast
CreateWindowEx) - potrzebujemy tylko jakiegokolwiek okna tej klasy - w tym przypadku pola tekstowego:
g_hDummy = CreateWindow( "EDIT", NULL, WS_CHILD , 0, 0, 0, 0, hwnd, NULL,
hThisInstance, NULL );
Następnym krokiem będzie zastąpienie "starej" procedury okna nową:
g_OldWndProc =( WNDPROC ) SetClassLong( g_hDummy, GCL_WNDPROC,( LONG ) NewWndProc );
Od tej pory każde nowo utworzone okienko klasy
EDIT będzie wydawało irytujące dźwięki przy pisaniu ;-). Sprawdźmy zresztą:
CreateWindowEx( WS_EX_CLIENTEDGE, "EDIT", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER,
50, 50, 150, 25, hwnd, NULL, hThisInstance, NULL );
CreateWindowEx( WS_EX_CLIENTEDGE, "EDIT", NULL, WS_CHILD | WS_VISIBLE | WS_BORDER,
50, 85, 150, 25, hwnd, NULL, hThisInstance, NULL );
Wszystko się zgadza; mamy dwa pola tekstowe, a obydwa wydają dźwięki, nawet jeśli usuniemy z procedury
WndProc wywołanie funkcji
Beep. Warto jeszcze zadbać o to, żeby nam się system nie wykrzaczył, co teoretycznie można spowodować, zapominając o usunięciu subclassingu z systemowej klasy
EDIT:
case WM_DESTROY:
{
SetClassLong( g_hDummy, GCL_WNDPROC,( LONG ) g_OldWndProc );
PostQuitMessage( 0 );
}
break;
W przypadku subclassingu wystąpienia nie musieliśmy tego robić, ponieważ tam eksperymentowaliśmy tylko na pojedynczych oknach, których żywot kończył się wraz z działaniem naszej aplikacji. Tym razem modyfikujemy całą klasę, a więc wprowadzone przez nas zmiany mogą dotyczyć całego systemu.
Na koniec jeszcze jedna przestroga:
Ten przykład to jeszcze jeden z serii "dzieci, nie róbcie tego w domu!". Microsoft nie zaleca modyfikowania systemowych klas okien, takich jak EDIT. Jeśli chcemy masowo tworzyć okna o zmienionym przez nas zachowaniu, powinniśmy korzystać z superclassingu.
|
Superclassing
Oprócz "psucia" istniejących już klas możemy na ich podstawie utworzyć własną, całkowicie nową klasę (przypomina to nieco mechanizm dziedziczenia w C++). To jest właśnie '''superclassing'''. Jest on techniką stuprocentowo bezpieczną (w przeciwieństwie do subclassingu), ponieważ taka superklasa "widoczna" jest tylko w obrębie naszej aplikacji i na resztę systemu w żaden sposób nie wpływa.
Zabawę w superclassing zaczynamy od pobrania informacji o klasie bazowej - na przykład
EDIT. Robi się to przy pomocy
GetClassInfo bądź
GetClassInfoEx. Funkcje te wypełniają żądanymi informacjami odpowiednią strukturę - w pierwszym przypadku
WNDCLASS, w drugim -
WNDCLASSEX. Jak być może pamiętasz z pierwszego odcinka kursu, jedyną właściwie różnicą pomiędzy tymi strukturami jest brak w tej pierwszej pola odpowiedzialnego za małą ikonkę, tak więc jako lenie patentowane wybieramy drogę na skróty i bierzemy funkcję
GetClassInfo:
WNDCLASS wc;
GetClassInfo( hThisInstance, "EDIT", & wc );
Jako się rzekło, struktura ma zostać wypełniona aż po brzegi niezbędnymi informacjami. No, może z tymi brzegami to lekka przesada, ponieważ aż trzy pola struktury nie są przez
GetClassInfo w ogóle ruszane:
lpszMenuName, lpszClassName, hInstance. Powody są w sumie oczywiste; możesz się ich sam domyślić. Wszystkie te trzy pola wypełniamy sami:
wc.lpszMenuName = NULL;
wc.lpszClassName = "Nowy, lepszy EDIT ;-)";
wc.hInstance = hThisInstance;
Pole
lpszClassName ma zawierać nową nazwę klasy - tu wpisz, co chcesz. Z kolei pole
hInstance powinno zawierać uchwyt wystąpienia naszej aplikacji. Menu nie mamy, więc
lpszMenuName możemy po prostu olać.
Pozostaje najważniejsza część zadania, czyli wstawienie nowej procedury okna (użyjemy tej zdefiniowanej w poprzednich przykładach, wprowadzającej dźwięki towarzyszące klawiszom), a także zapamiętanie wskaźnika do "starej" procedury:
g_OldWndProc = wc.lpfnWndProc;
wc.lpfnWndProc = NewWndProc;
Struktura z informacjami o nowej klasie jest już gotowa do użycia; możemy teraz tę klasę zarejestrować. Proces ten jest nam już znany z poczatków kursu:
if( !RegisterClass( & wc ) )
{
MessageBox( hwnd, "Nie udało się zarejestrować nowej klasy.", "Yh...", MB_ICONSTOP );
DestroyWindow( hwnd );
}
Jeśli wszystko poszło OK, to dysponujemy od tego momentu klasą o wdzięcznej nazwie
"Nowy, lepszy EDIT ;-)" i możemy tworzyć kontrolki tej klasy, podając taką właśnie nazwę jako argument funkcji
CreateWindowEx. Jeśli natomiast utworzymy kontrolkę, podająć nazwę
"EDIT", to utworzone zostanie najzwyklejsze pole tekstowe, bez żadnych dźwięków. Zauważ, że w przeciwieństwie do globalnego subclassingu nie musimy wykonywać tutaj żadnego sprzątania, czyli przywracać "starej" procedury okna klasie przy kończeniu działania aplikacji, ponieważ klasa ta będzie "odrejestrowana" automatycznie (jak wszystkie klasy zarejestrowane przez naszą aplikację).