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

Obsługa myszy i klawiatury

[lekcja] Rozdział 3. Procedura obsługi komunikatów - funkcja DefWindowProc i jej znaczenie; obsługa klawiszy myszki, podwójnego kliknięcia oraz komunikatów związanych z przemieszczaniem myszki, a także zmiana kursora myszki. W rozdziale znajduje się również omówienie funkcji do ręcznego wysyłania komunikatów SendMessage jak i ręcznego ustawiania współrzędnych myszki za pomocą funkcji SetCursorPos. Nieodłączną część niniejszego rozdziału stanowi również obsługa klawiatury.

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:

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

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

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

C/C++
bool Przyc = false;
 
Mamy zmienną, dodajemy obsługę komunikatów:

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

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

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

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

C/C++
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.
Poprzedni dokument Następny dokument
Kontrolki Pliki