Obsługa urządzeń wejściowych
W Windowsie zazwyczaj się coś dzieje. Oprócz błędów krytycznych, zapętleń, wyczerpywania zasobów systemowych, blue screenów tudzież zwykłej niestabilności zdarza się też czasem, że Windows wykonuje coś pożytecznego. Polega to ogólnie na tym, że użytkownik wprowadza jakieś dane i otrzymuje inne dane w zamian. Zjawisko to nazywamy interakcją. Aby było to możliwe, generowane są tzw.
zdarzenia (
events). Zdarzeniem może być poruszenie myszą, wciśnięcie klawisza, wybranie jakiejś pozycji menu, zamknięcie lub przesunięcie okna itp.
Każde zdarzenie w Windows jest skojarzone z odpowiednim komunikatem. Żeby obsłużyć jakieś zdarzenie (przypisać mu konkretne działanie, np. najeżdżamy kursorem myszy na obiekt - kształt kursora się zmienia), musimy dopisać odpowiedni kod do procedury obsługi komunikatów. Procedura ta może mieć różne nazwy (zależy od fantazji programisty), ale najczęściej nazywa się
WindowProcedure (procedurę o takiej nazwie generuje domyślnie Dev-C++) albo krócej
WndProc. Ja będę raczej używał tej pierwszej - dłuższa, ale dla początkujących być może bardziej wymowna.
Przyjrzyjmy się bliżej tej procedurze:
LRESULT CALLBACK WindowProcedure( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )
{
switch( message )
{
case WM_CLOSE:
{
DestroyWindow( hwnd );
}
break;
case WM_DESTROY:
{
PostQuitMessage( 0 );
}
break;
default:
return DefWindowProc( hwnd, message, wParam, lParam );
}
return 0;
}
Tak mniej więcej wygląda domyślna procedura obsługi komunikatów, wygenerowana przez Dev-C++ jeśli stworzymy w nim nowy projekt windowsowy. Na razie obsługuje ona tylko dwa komunikaty:
WM_CLOSE, wysyłany przez system w przypadku gdy użytkownik zamknie okno programu (reakcją jest zniszczenie zamkniętego okna funkcją
DestroyWindow), oraz
WM_DESTROY wysyłany przez system w momencie niszczenia okna (reakcją jest wysłanie komunikatu o zakończeniu działania programu). Jak nietrudno się domyślić, wysłanie tego pierwszego komunikatu w tym przypadku powoduje automatycznie wysłanie tego drugiego, zaś z kolei ten drugi spowoduje w linii prostej wyjście z programu.
Procedura obsługi komunikatów powinna zawsze zwrócić
0 w przypadku poprawnego obsłużenia komunikatu. Do procedury
WindowProcedure wysyłany jest każdy komunikat, jaki trafi się podczas działania programu, więc jeśli programista nie przewiduje obsługi danego komunikatu (tj. nie jest mu to potrzebne), musi ten komunikat przekazać (w stanie nienaruszonym ;-)) do domyślnej funkcji obsługi -
DefWindowProc (która zresztą najczęściej nie robi z tym komunikatem nic konkretnego, ale takie traktowanie jest wymagane).
Jak widać, wewnątrz funkcji
WindowProcedure mamy do dyspozycji cztery parametry, które muszą nam wystarczyć do obsłużenia danego komunikatu. Pierwszy (tutaj
hwnd) to uchwyt naszego głównego okna - chyba nie trzeba tłumaczyć, po co jest. Drugi (tutaj
message) to kod komunikatu - używamy go najczęściej tylko w głównej kontrukcji
switch-case do wybrania właściwego fragmentu kodu do danego komunikatu. Pozostałe parametry (
wParam i
lParam) są dodatkową informacją o komunikacie, i tak na przykład w przypadku komunikatu o kliknięciu myszą będą one zawierały kod konkretnego przycisku myszy, który został wciśnięty przez usera oraz współrzędne wskaźnika myszy w chwili wciśnięcia.
W poprzedniej części tego kursu, poświęconej kontrolkom, zobaczyliśmy jak się obsługuje podstawowe komunikaty związane z tymi kontrolkami. Poznaliśmy też niektóre aspekty korzystania z parametrów
wParam i
lParam, przekonując się przy okazji w praktyce, jak ważne one są. Teraz dowiemy się, jak obsługiwać komunikaty związane z myszą i klawiaturą.
Obsługa kliknięć myszą
Aby nasz kochany, szary gryzoń zaczął żyć własnym życiem, trzeba dodać obsługę kilku komunikatów myszy. Zacznijmy od zwykłego kliknięcia. Dla systemu kliknięcie składa się z dwóch faz: wciśnięcia danego przycisku myszy i zwolnienia go. Dzięki takiemu rozróżnieniu możliwe są operacje typu "przeciągnij-i-upuść" (nawiasem mówiąc jedna z najbardziej wkurzających cech Windows, przyczyna wielu zaginięć plików, które później odnajdują się w zaskakujących miejscach). Którą z tych dwóch faz uznasz za właściwe kliknięcie, to już tylko od ciebie zależy. Możesz także akceptować tylko pełne kliknięcie: zapamiętywać w jakiejś zmiennej, że user wcisnął przycisk myszy w obrębie danej kontrolki i później gdy go zwolni, a wskaźnik myszy będzie nadal nad tą kontrolką, wywołać jakieś działanie.
Windows domyślnie rozróżnia trzy przyciski myszy (lewy, prawy i środkowy). Wciśnięcie tych przycisków wywołuje odpowiednio komunikaty:
WM_LBUTTONDOWN,
WM_RBUTTONDOWN,
WM_MBUTTONDOWN. Jeśli interesują cię jakieś dodatkowe przyciski, to musisz sięgnąć do odpowiednich bibliotek (np. DirectInput z pakietu DirectX) lub szukać innych sposobów ich zaprogramowania. Jak się zapewne domyślasz, zwolnienie przycisków powoduje wysłanie komunikatów
WM_LBUTTONUP,
WM_RBUTTONUP i
WM_MBUTTONUP.
Kompletne pojedyncze kliknięcie na danym obiekcie musimy sobie więc wykryć sami, ale na szczęście podwójne kliknięcia w całości zależą od ustawień systemowych, więc też i system je wykrywa, wysyłając nam wtedy odpowiedni komunikat, np.
WM_LBUTTONDBLCLK (mam nadzieję, że nie będziesz miał problemów z rozszyfrowaniem tego skrótu ;-)). Jest jednak mały haczyk - podwójne kliknięcia wykrywane są tylko dla okien, które należą do klasy z ustawionym stylem
CS_DBLCLKS. Musimy więc zajrzeć do kodu rejestrowania klasy okna i upewnić się, czy ten styl jest faktycznie ustawiony. Fragment ten powinien wyglądać mniej więcej tak:
wincl.style = CS_DBLCLKS;
Warto od razu wspomnieć, że jeśli piszesz jakąś grę (lub inny program), gdzie trzeba dużo i szybko klikać, to pewnie zauważysz, że na niektóre kliknięcia program ten po prostu nie reaguje. Winowajcą może być właśnie ta flaga -
CS_DBLCLKS, która powoduje, że część kliknięć "znika", czego w większości przypadków nie chcemy - w takim przypadku należy usunąć tę flagę.
Najlepiej jest pokazać działanie komunikatów myszy, bawiąc się w rysowanie po okienku, tak że jeśli jeszcze nie wiesz nic na temat grafiki w WinAPI, radzę rzucić okiem na jedną z następnych części tego kursu, coby wszystko w tym przykładzie było jasne. Napiszemy sobie programik, który łączy nam liniami punkty, w których klikniemy z lewym górnym rogiem okienka. Ponadto przy każdym dwukrotnym kliknięciu będzie stawiał kółeczko. Wystarczy nam dodać obsługę komunikatów
WM_LBUTTONDOWN i
WM_LBUTTONDBLCLK: case WM_LBUTTONDOWN:
{
HDC hdc = GetDC( hwnd );
LineTo( hdc, LOWORD( lParam ), HIWORD( lParam ) );
ReleaseDC( hwnd, hdc );
}
break;
case WM_LBUTTONDBLCLK:
{
HDC hdc = GetDC( hwnd );
Ellipse( hdc, LOWORD( lParam ) - 3, HIWORD( lParam ) - 3, LOWORD( lParam ) + 3, HIWORD( lParam ) + 3 );
ReleaseDC( hwnd, hdc );
}
break;
Jedyna prawdziwa nowość w tym przykładziku to wyrażenia
LOWORD(lParam) i
HIWORD(lParam). Otóż komunikat
WM_LBUTTONDOWN, podobnie jak inne komunikaty myszy, niesie ze sobą w argumencie
lParam informację o współrzędnych wskaźnika myszy w chwili wysłania tego komunikatu (czyli tutaj w chwili kliknięcia). Współrzędna X jest przechowywana w dolnym słowie zmiennej
lParam, natomiast współrzędna Y - w górnym słowie. Do "wyciągnięcia" współrzędnych (czyli podziału lParam na dwie połówki) używamy więc makr
LOWORD i
HIWORD.
Jak widać na tym przykładzie, Windows wysyła wprawdzie komunikaty dla pojedynczych i podwójnych kliknięć osobno, ale jeśli wykonamy podwójne kliknięcie, to zostaną wysłane obydwa te komunikaty. Dlatego jeśli chcemy zaprogramować zarówno pojedyncze klinięcie, jak i podwójne, aby każde z nich miało przypisaną inną akcję, musimy być ostrożni.
Poruszanie myszą
Oprócz klikania, myszą można poruszać (to ci dopiero nowość, nieprawdaż? ;-)). Za ruchy myszy odpowiedzialny jest komunikat
WM_MOUSEMOVE, wysyłany za każdym razem, gdy jakaś rolka w myszy choćby drgnie - ten to dopiero ma w Windowsie przechlapane! Pomyśl sobie, ile razy jest on wywoływany w ciągu dnia! :-)
Argumenty komunikatu
WM_MOUSEMOVE pełnią identyczną rolę jak w komunikatach obsługujących kliknięcia. Napiszmy więc sobie teraz jeszcze fajniejszy program do rysowania po okienku. Tym razem będzie to już prawie program graficzny z prawdziwego zdarzenia ;-). Biega nam o to, aby rysowany był pojedynczy punkt za każym razem, gdy użytkownik wciśnie lewy przycisk myszy, a także (czy nawet przede wszystkim) gdy user przesuwa mysz, jeśli przycisk jest wciśnięty. Potrzeba nam więc obsługi wszystkich trzech komunikatów
WM_LBUTTONDOWN,
WM_LBUTTONUP,
WM_MOUSEMOVE, a także globalnej zmiennej, która będzie mówiła nam, czy w danej chwili przycisk jest wciśnięty czy nie:
Mamy zmienną, dodajemy obsługę komunikatów:
case WM_LBUTTONDOWN:
{
Przyc = true;
SendMessage( hwnd, WM_MOUSEMOVE, wParam, lParam );
}
break;
case WM_LBUTTONUP:
Przyc = false;
break;
case WM_MOUSEMOVE:
if( Przyc )
{
HDC hdc = GetDC( hwnd );
SetPixel( hdc, LOWORD( lParam ), HIWORD( lParam ), RGB( 255, 0, 0 ) );
ReleaseDC( hwnd, hdc );
}
break;
No i proszę, możemy sobie rysować ;-). Myślę, że wszystko powinno być tu jasne, no, może opócz tej funkcji
SendMessage. Nietrudno się jednak chyba domyślić, że służy ona do "sztucznego" wywoływania komunikatów, czyli używamy jej gdy chcemy symulować jakieś zdarzenie, które "tak naprawdę" nie miało miejsca. W tym przypadku gdy użytkownik kliknie myszą w danym punkcie, zostanie "sztucznie" wywołany komunikat
WM_MOUSEMOVE, czyli narysuje się w tym miejscu punkt, nawet jeśli użytkownik wcale nie poruszył myszą, a tylko wcisnął lewy przycisk.
Kursor myszy
Ta strzałeczka, która lata sobie po całym ekranie jest całkiem fajowa, ale od czasu do czasu dobrze byłoby zmienić ją np. na klepsydrę czy inny kształt, albo i całkiem schować. Możemy się w tym celu posłużyć funkcją
SetCursor. Jej jedyny parametr to uchwyt nowego kursora. Uchwyt taki możemy zdobyć od funkcji
CreateCursor (która tworzy nowy kursor "od zera"), albo też od funkcji
LoadCursor (wczytuje kursor z pliku zasobów). Na razie polecam znacznie prostszą funkcję
LoadCursor, która poza ładowaniem kursora z zasobów naszego programu potrafi również pobrać jeden ze standardowych kursorów Windows:
HCURSOR StaryKursor;
StaryKursor = SetCursor( LoadCursor( NULL, IDC_WAIT ) );
Powyższy przykładzik zmienia wskaźnik myszy na klepsydrę. Jak widać, funkcja
SetCursor zwraca uchwyt do poprzednio używanego kursora (jeśli nie było żadnego, to zwraca
NULL), który najlepiej jest przechować w odpowiedniej zmiennej (tutaj
StaryKursor) i przy zakończeniu programu ustawić z powrotem jako aktualny kursor. Pierwszy argument funkcji
LoadCursor musi zawsze być
NULL, jeśli ładujemy jakiś standardowy kursor Windowsa, w przeciwnym wypadku (jeśli ładujemy kursor z zasobów programu), argument ten musi zawierać uchwyt do programu (
HINSTANCE).
Drugi argument funkcji
LoadCursor może również przybierać wartości:
IDC_ARROW - zwykła strzałka,
IDC_CROSS - krzyż,
IDC_IBEAM - kursor edycji tekstu,
IDC_APPSTARTING - strzałka z małą klepsydrą itd.
Oprócz funkcji
LoadCursor, do wczytania kursora możemy jeszcze użyć funkcji
LoadImage, która pozwoli nam wczytać kursor z samodzielnego pliku - kursora (*.cur), czy nawet ikony (*.ico) lub bitmapy (*.bmp). Trzeba jednak będzie w tym przypadku zastosować jawną konwersję obrazka zwróconego przez
LoadImage na typ
HCURSOR.
Zwróć uwagę, że pliki *.ico czy *.bmp nie zawierają współrzędnych tzw. hotspot-a, czyli punktu, który jest uznawany jako "czubek strzałki", a więc hotspot jest domyślnie ustawiany na lewy górny róg. Z tego względu te rodzaje plików nie bardzo nadają się do przechowywania kursorów.
|
Możemy również podać
NULL jako argument dla
SetCursor - stosujemy to wówczas, gdy chcemy wskaźnik myszy po prostu schować.
Jak uzyskać uchwyt do aktualnie wybranego kursora? Proste - używamy funkcji
GetCursor (nie ma ona żadnych argumentów).
Ostatnią rzeczą, którą jeszcze warto wiedzieć na temat wskaźnika myszy, może być poruszanie nim... bez udziału myszy ;-). Aby przemieścić kursor w dowolne miejsce na ekranie... no, może nie na ekranie, tylko na oknie - używamy
SetCursorPos:
if( !SetCursorPos( 100, 150 ) )
MessageBox( hwnd, "Nie można ruszyć kursora (może się przylepił?)",
"Błąd", MB_ICONEXCLAMATION );
W ten sposób możemy np. zaprogramować poruszanie myszą przy pomocy klawiatury, co z pewnością docenią użytkownicy naszych aplikacji, gdy mysz im się popsuje ;-). Oczywiście w tym celu powinieneś przeczytać następny rozdział...
Obsługa klawiatury
Generalnie rzecz ujmując, mysza to naprawdę miłe stworzonko, ale figę nam po niej, jeśli robimy grę w stylu Robaka. Poza tym klawiatura jest łatwiejsza w użyciu, jeśli chcemy szybko włączyć jakąś opcję czy wywołać komendę menu - nie trzeba w nic celować strzałką, co bywa kłopotliwe zwłaszcza po spożyciu kilku... napojów. Nie mówiąc już o tym, że myszy czasami się psują. Krótko mówiąc: potrzebujemy obsługi klawiatury!
Metod jest kilka, omówię tylko tę najprostszą i zarazem najbardziej przydatną. Wciśnięcie klawisza powoduje wysłanie komunikatu
WM_KEYDOWN lub
WM_KEYUP - analogicznie jak dla mychy. Sęk w tym, żeby wydedukować, który klawisz został wciśnięty (jak wiadomo, jest ich na klawiaturze sporo). Kod klawisza przekazywany jest w argumencie
wParam. Trzeba tylko zastosować konstrukcję
switch-case. Kodom klawiszy odpowiadają stałe rozpoczynające się od przedrostka
VK (od Virtual Key). Niestety, nie są one zbyt intuicyjne (
VK_RETURN - klawisz Enter,
VK_MENU - klawisz Alt,
VK_NEXT - klawisz Page Down itd.).
Autorzy SDK użytego w Dev-C++ zapomnieli najwyraźniej umieścić definicji stałych od VK_A do VK_Z. Na szczęście mają one kody odpowiadające kodom ASCII dużych liter, np. A zakodowane jest jako 0x41 (65 dziesiątkowo). Jeśli nie chce ci się samodzielnie poprawiać twórców SDK do Dev-a, to zajrzyj do działu [[Download|Download]] i ściągnij plik keys.zip - zawiera on plik nagłówkowy z definicjami klawiszy A-Z, wystarczy go tylko dołączyć wiadomą dyrektywą.
|
Pozostaje tylko pokazać, jak to w praktyce wygląda:
case WM_KEYDOWN:
{
switch(( int ) wParam )
{
case VK_RETURN:
MessageBox( hwnd, "Wciśnięto Entera", "Yeah", MB_ICONINFORMATION );
break;
case VK_ESCAPE:
DestroyWindow( hwnd );
break;
}
}
break;
Chyba katastrofy by nie było, gdybyśmy pominęli tę konwersję parametru
wParam na
int, ale tak było w MSDN, więc przepisałem ;-). Funkcja
DestroyWindow służy do niszczenia podanego okienka, jak sama nazwa zresztą wskazuje. Jest to niezły sposób na zakończenie programu, o ile mamy w kodzie obsługi komunikatu
WM_DESTROY wywołanie funkcji
PostQuitMessage (ten fragment jest generowany przez Dev-C++ razem z kodem tworzącym okno główne). Jeśli tego nie masz w swoim kodzie, to samo zniszczenie głównego okna spowoduje dość niezręczną sytuację - program trzeba będzie wywalić z pamięci przez Ctrl+Alt+Del :-/.
Trochę współdziałania
Niezliczone ilości opcji w programach sprawiają, że setki kombinacji klawiaturowych plus mysza z czterema przyciskami i dwoma scrollerami przestają wystarczać. Dlatego ktoś wpadł na pomysł, że można by połączyć myszę z klawiaturą - stąd czasami można wyczytać w instrukcji, że aby coś w danej grze czy programie zrobić, musisz np. przytrzymać Ctrl i kliknąć myszą. To dość fajna sprawa, więc postanowiłem pokazać, jak to się robi w WinAPI.
Koncepcja jest bardzo prosta. Jak pamiętamy (prawda, że pamiętamy?), komunikaty typu
WM_LBUTTONCLICK czy
WM_MOUSEMOVE przekazywały nam w swoich argumentach
lParam pozycję myszy. A drugi argument -
wParam - daje nam informację o specjalnych klawiszach, które były wciśnięte tuż przed wygenerowaniem komunikatu. Te specjalne klawisze mogą być klawiszami myszy (stałe
MK_LBUTTON,
MK_MBUTTON,
MK_RBUTTON), ale mogą też być klawiszami Ctrl (stała
MK_CONTROL) lub Shift (
MK_SHIFT). Oto jak to wykorzystać:
case WM_LBUTTONDOWN:
{
if( wParam & MK_CONTROL )
MessageBox( hwnd, "Wciśnięty jest Ctrl", NULL, MB_ICONINFORMATION );
if( wParam & MK_SHIFT )
MessageBox( hwnd, "Wciśnięty jest Shift", NULL, MB_ICONINFORMATION );
}
break;
Zauważ, że np. dla komunikatu
WM_LBUTTONDOWN zmienna
wParam nigdy nie powinna być RÓWNA
MK_CONTROL, nawet jeśli w czasie kliknięcia przytrzymany był Ctrl! Zmienna
wParam zawiera KOMBINACJĘ aktualnie wciśniętych klawiszy, a więc jeśli przytrzymamy Ctrl i klikniemy lewym przyciskiem, to
wParam będzie zawierał wartość
MK_CONTROL | MK_LBUTTON. Dlatego też nie możemy stosować tutaj konstrukcji
switch do sprawdzenia, który klawisz na klawiaturze był przytrzymany, a za to musimy stosować operator
&.
Parę słów o przechwytywaniu
Jedna z windowsowych reguł mówi, że tylko jedno okno może w danym momencie otrzymywać komunikaty pochodzące od myszy. Dlatego jeśli już mu się uda pokonać inne konkurencyjne okienka i otrzyma do obsłużenia jakieś myszkowe zdarzenie, to mówimy o tym oknie, że przechwyciło mysz.
Jak już wiesz, wszelkie kontrolki również są oknami. Dlatego właśnie jeśli np. klikniesz lewym przyciskiem myszy na przycisku, to główne okno twojego programu NIE otrzyma ani komunikatu
WM_LBUTTONDOWN, ani
WM_LBUTTONUP. Otrzyma za to
WM_COMMAND, wygenerowane przez przycisk. Dzieje się tak dlatego, że przycisk przechwytuje mysz, wewnętrzna procedura okna-przycisku (każde okno ma swoją własną procedurę do obsługi komunikatów!) obsługuje
WM_LBUTTONDOWN i
WM_LBUTTONUP, stwierdza, że nastąpiło pełnoprawne klinknięcie na przycisku i wówczas wysyła do swojego okna-rodzica (czyli naszego głównego okna) komunikat
WM_COMMAND.
Z powyższego wynika, że nie możemy w znany nam sposób obsłużyć "myszowych" zdarzeń dla kontrolek. Na szczęście nie do końca jest to prawdą; gdy już trochę więcej dowiesz się o WinAPI, poczytaj artykuł poświęcony subclassingowi.