Rozdział 7 Mysz jest urządzeniem wskazującym. Ma co najmniej jeden przycisk (zazwyczaj dwa lub trzy). Wyszła zwycięsko z konfrontacji z innymi wejściowymi urządze- niami wskazującymi, takimi jak ekrany dotykowe czy pióra świetlne. Mysz wraz ze swoimi odmianami, takimi jak trackball zwykle stosowany w komputerach przenośnych, stała się jedynym urządzeniem wejściowym, któremu udało się na dobre zadomowić na rynku komputerów PC. Jednak na samym początku perspektywy myszy wcale nie wyglądały zbyt dobrze. Twórcy wczesnych wersji systemu Windows czuli, że nie mogą wymagać od użyt- kowników kupienia nowego urządzenia po to tylko, aby mogli oni korzystać z ich produktu. Dlatego też uznali mysz za urządzenie dodatkowe i umożliwili wyko- nywanie wszelkich operacji za pomocą klawiatury. (Jeżeli na przykład przeczytasz teksty podpowiedzi dotyczących Kalkulatora, zorientujesz się, w jaki sposób po- szczególnym jego klawiszom i funkcjom przypisane zostały odpowiedniki z kla- wiatury). Niezależru dostawcy oprogramowania również zachowali się w ten sam sposób: każda z funkcji ich pro,gramów mogła zostać wykonana z klawiatury. Wcze- śniejsze wydania tej książki również propagowały podobne podejście. Teoretycznie, najnowsze wersje systemu Windows wymagają obecności myszy. A przynajmniej taka informacja widnieje na opakowaniu. Możesz jednak odłą- czyć mysz od komputera, a system i tak wystartuje poprawnie (pojawi się jedy- nie komunikat, że mysz nie została podłączona). Jednak korzystanie z systemu bez pomocy myszy przypomina granie na pianinie palcami nóg (przynajmniej na początku), nic nie stoi jednak na przeszkodzie, abyś w ten sposób pracował. Dlatego też ciągle jestem zwolennikiem umieszczania w programach odpowied- ników klawiaturowych wszystkich funkcji dostępnych za pośrednictwem myszy. Uźytkownicy, którzy szczególnie sprawnie poshxgują się klawiaturą, zwykle nie lubią zbyt często odrywać od niej rąk. Przypuszczam również, że każdemu z nas wielokrotnie zdarzyło się już "zgubić" kursor myszy na zagraconym biurku lub mieć zdecydowanie zbyt mało miejsca na podkładce, aby sprawnie operować myszą. Dodanie do programu możliwości uruchamiania funkcji programu za pomocą klawiatury nie wymaga zbyt dużo wysiłku, a może znacznie ułatwić życie tym użytkownikom, którzy wolą ten sposób komunikacji z komputerem. Podczas gdy klawiatura wykorzystywana jest zwykle do wprowadzania i maru- pulowania danymi, mysz pozwala na łatwe rysowanie oraz przetwarzanie obiek- tów graficznych. Dlatego też większość programów przykładowych, które znaj- dziesz w tym rozdziale, ma coś wspólnego z grafiką. Będziesz przy tym korzy- stał w nich z tego, czego nauczyłeś się w rozdziale 5. 254 Część I Podstawy Podstawy W Windows 98 możesz posługiwać się myszą z jednym, dwoma lub trzema przy- ciskami. Zamiast myszą możesz również poshxgiwać się joystickiem lub piórem świetlnym. We wcześniejszych wersjach Windows, aby nie dyskryminować tych, którzy mieli mysz z jednym przyciskiem, unikano posługiwania się drugim i trze- cim. Jednakże wraz z upływem czasu obecność drugiego przycisku stała się stan- dardem, nie ma więc już żadnej podstawy do jego unikania. Obecnie standardo- wo umożliwia on wyświetlenie na ekranie "menu kontekstowego". Pojawia się ono w oknie poza standardowym paskiem menu lub w związku z przeciąganiem (którym zajmiemy się wkrótce). Jednakże programy nie powinny zakładać, że drugi przycisk myszy będzie istniał w każdej sytuacji. Teoretycznie możesz sprawdzić, czy mysz jest obecna, posługując się znajomą już funkcją GetSystemMetrics: fMouse = GetSystemMetrics (SM MOUSEPRESENT) ; Zmienna lVlouse przyjmie wartość TRUE (niezerową), jeżeli mysz została zain- stalowana, natomiast 0 - w przeciwnym wypadku. Jednak w Windows 98 funk- cja ta zawsze zwraca wartość TRUE, niezależnie od tego, czy mysz jest podłączo- na, czy nie. W Windows NT funkcja działa poprawnie. Jeżeli chcesz się dowiedzieć, ile przycisków ma mysz, wykonaj: cButtons = GetSystemMetrics (SM CMOUSEBUTTONS) ; Również ta funkcja powinna zwrócić wartość 0, jeżeli mysz nie została zainstalo- wana. Jednak w Windows 98 zostanie w tej sytuacji zwrócona wartość 2. Ci z użytkowników, którzy posługują się lewą ręką, mogą zmienić funkcje przy- pisane poszczególnym przyciskom myszy, korzystając z możliwości apletu Mysz znajdującego się w Panelu sterowania. Mimo że aplikacja może sprawdzić, czy przyciski zostały przełączone, wywołując funkcję GetSystemMetrics z parametrem SM SWAPBUTTON, tak naprawdę nie jest to konieczne. Przycisk, który użytkow- nik naciska palcem wskazującym, zawsze traktowany jest jako lewy nawet, jeżeli fizycznie znajduje się po prawej stronie myszy. Jeżeli jednak piszesz program śle- dzący wykorzystywanie myszy, konieczne może okazać się narysowanie na ekra- nie jej obrazu i w tej sytuacji informacja o przełączeniu przycisków może okazać się potrzebna. Użytkownik może również zmieniać parametry pracy myszy posługując się aple- tem Mysz znajdującym się w Panelu sterowariia. Z poziomu aplikacji ten sam efekt możesz osiągnąć, wywołując funkcję SystemParameterslnfo. Kilka szybkich definicji Gdy użytkownik w Windows poruszy myszą, system przemieści wyświetlaną na ekranie niewielką bitmapę. Nosi ona nazwę kursora myszy. W jego obrębie znaj- duje się ostrze (ang. hot spot). Od tej pory, zawsze kiedy będę mówił o położeniu kursora myszy na ekranie, będę miał na myśli położenie tego punktu. Windows pozwala programom posługiwać się kilkoma predefiniowanymi kur- sorami myszy. Najbardziej typowym jest pochylona strzałka o nazwie IDC AR- Rozdział 7: Mysz 255 ROW (identyfikator ten, podobnie jak i pozostałe, zdefiniowany został w pliku WINLTSER.H). Punkt aktywny umieszczony został na końcu jej grota. Kursor IDC CROSS (posłużymy się nim w zaprezentowanym dalej programie BLOK- OUT) ma punkt aktywny w miejscu przecięcia prostopadłych linii. Z kolei kur- sor IDC WAIT ma kształt klepsydry; jest zwykle wykorzystywany przez program, gdy chce on poinformować użytkownika, że jest czymś zajęty. Programiści mogą również definiować swoje własne kursory. Jak to zrobić, dowiesz się w rozdziale 10. Domyślny kursor okna deklarowany jest w strukturze klasy okna: wndclass.hCwrsor = LoadCursor (NULL, IDC ARROW) : A oto terminy określające czynności, które możesz wykonać za pomocą myszy: ł Kliknięcie - naciskanie i zwalnianie przycisku myszy. ł Dwukrotne kliknięcie - dwukrotne naciśnięcie i zwolnienie przycisku myszy w krótkim odstępie czasu. ł Ciągnięcie - poruszanie myszą z wciśniętym jednym z przycisków. W przypadku myszy z trzema przyciskami, przyciski noszą nazwę lewy, środko- wy oraz prawy. Związane z nimi identyfikatory, zdefiniowane w pliku nagłówko- wym, to LBUTTON, MBUTTON oraz RBUTTON. Mysz dwuprzyciskowa posiada jedynie przycisk lewy i prawy, natomiast jednoprzyciskowa - wyłącznie lewy. Komunikaty myszy dla obszaru roboczego W poprzednim rozdziale zobaczyłeś, w jaki sposób system Windows wysyła ko- munikaty klawiaturowe wyłącznie do okna, które ma fokus wejściowy. Komuni- katy myszy zachowują się odmiennie: procedura okna otrzymuje takie komuni- katy za każdym razem, gdy kursor myszy znajdzie się nad obszarem okna lub gdy zostanie w nim kliknięty którykolwiek z przycisków. Dzieje się to nawet wtedy, gdy okno nie jest aktywne ani nie ma fokusu. W systemie Windows zde- finiowano 21 komunikatów związanych z myszą. Jednak 11 z nich nie dotyczy obszaru roboczego. Noszą one nazwę "komunikatów nie związanych z obsza- rem roboczym" (ang. nonclient-area messages), a aplikacje Windows zwykle je igno- rują. Gdy kursor myszy przesuwa się nad obszarem roboczym, do procedury okna wy- syłany jest komunikat WM MOUSEMOVE. Jeżeli we wnętrzu obszaru robocze- go zostanie naciśnięty lub zwolniony przycisk myszy, procedura okna otrzyma jeden z komunikatów przedstawioych w poniższej tabeli: Przycisk Naciśnięty Zwolniony Naciśnięty (drugie kliknięcie) Lewy WM LBUTTONDOWN 4VMLBUTTONUP WM LBUTTONDBLCLK Środkowy WMMBUTTONDOWN WM MBUTTONUP WM MBUTTONDBLCLK Prawv WMRBUTTONDOWN WM RBUTTONUP WMRBUTTONDBLCLK 2 Część i: Podstawy i Procedura okna otrzyma komunikaty MBUTTON tylko wtedy, gdy wykorzysty- wana jest mysz z trzema przyciskami, natomiast RBUTTON - gdy wykorzysty- wana jest mysz z dwoma przyciskami. Do procedury okna zostanie wysłany ko- munikat DBLCLK (podwójne kliknięcie) tylko wtedy, gdy klasa okna została zde- finiowana w sposób umożliwiający ich odbieranie (zobacz podrozdział "Kliknię- cia dwukrotne"). Do wszystkich wymienionych wyżej komunikatów pozycja bieżąca myszy jest przekazywana za pomocą parametrtz lParam. Młodsze jego słowo zawiera współ- rzędną x, natomiast starsze - współrzędną y, przy czym punktem odniesienia dla obu jest lewy górny róg ekranu. Obie wartości możesz pobrać, posługując się ma- krami LOWORD oraz HIWORD: x g LOWORD flParam) ; y = HIWORD (lParam) ; Wartość parametru wParam zawiera informacje o stanie przycisków myszy oraz klawiszy [Shift] i [Ctrl]. Stan poszczególnych bitów tego parametru możesz spraw- dzić, korzystając z makr zdefiniowanych w pliku nagłówkowym WINUSER.H. Prefiks "MK" pochodzi od mouse key (przycisk myszy}. MICLBUTTON Naciśnięty jest lewy przycisk myszy MK MBUTfON Naciśnięty jest środkowy przycisk myszy MIfRBUTTON Naciśnięty jest prawy przycisk myszy MK SHIFT Naciśnięty jest klawisz [Shiftl MK CONTROL Naciśnięty jest klawisz [CtrII Na przykład, jeżeli odbierzesz komunikat WM LBUTTONDOWN, a wparam & MKSHIFT ma wartość TRUE, oznacza to, że gdy użytkownik przycisnął lewy przycisk my- szy, naciśnięty był również klawisz [Shift]. W trakcie przesuwania kursora myszy nad obszarem roboczym okna, system Win- dows nie generuje komunikatu WMMOUSEMOVE dla każdego możliwego po- łożenia kursora myszy. To, ile twój program odbierze komunikatów WM MO- USEMOVE, zależy od elektroniki samej myszy oraz od prędkości, z jaką proce- dura okna jest w stanie przetwarzać komunikaty związane z myszą. Innymi sło- wy, Windows nie zapełnia kolejki nie przetworzonymi komunikatami WM MO- USEMOVE. O tym, jaka jest rzeczywista częstotliwość pojawiania się komunika- tów, przekonasz się po uruchomieniu programu CONNECT opisanego poniżej. Jeżeli klikniesz lewym przyciskiem myszy w obszarze roboczym okna, które nie jest aktywne, Windows uaktywni je, a następnie prześle do jego procedury komu- nikat WMLBUTTONDOWN. Dlatego też za każdym razem, gdy twój program odbierze komunikat WMLBUTTONDOWN, możesz śmiało założyć, że jego okno jest aktywne. Może się jednak tak zdarzyć, że do okna zostanie wysłany komuni- kat WMLBUTTONUP, aIe nie zostanie poprzedzony komunikatem WM LBUT TONDOWN. Stanie się tak, gdy Iewy przycisk myszy zostanie naciśnięty nad jed- nym oknem, a zwolniony nad innym. Podobnie, procedura okna może odebrać ko- munikat tNM-LBUTTONDOWN bez odpowiadającego mu komunikatu 257 R^zdział 7: Mysz WMLBUTTONUP, jeżeli przycisk zostanie zwolniony po przesunięciu myszy do innego okno. ' Są dwa odstępstwa od tej reguły: ł Procedura okna może "przechwycić mysz". Będzie wtedy otrzymywała zwią- zane z nią komunikaty, nawet jeżeli kursor znajdzie się nad obszarem robo- czym innego okna. O tym, w jaki sposób przechwycić mysz, dowiesz się nie- co dalej. Jeżeli na ekranie wyświetlane jest systemowe modalne okno komunikatu (ang. . system modal message box) lub systemowe modalne okno dialogowe (ang. sys- tem modal dialog box), żaden inny program nie może odbierać komunikatów myszy. W obu przypadkach nie jest również możliwe przełączenie do innego okna. Przykładem systemowego modalnego okna dialogowego może być to, które pojawia się, gdy zamierzasz zakończyć pracę komputera. Proste przetwarzanie komunikatów myszy: przykład Przedstawiony na rysunku 7-1 program CONNECT przetwarza kilka prostych komunikatów związanych z myszą. Pozwala ci się także zorientować, w jaki spo- sób system Windows przesyła te komunikaty do twoich programów. CONNECT.C /* CONNECT.C - Demo myszy - ldczenie punktów liniami (c) Charles Petzold. 1998 */ llinclude 4ldefine MAXPOINTS 1000 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) : int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC7 = TEXT ("Connect") : HWND hwnd : MSG msg : WNDCLASS wndclass : wndclass.style = CS HREDRAW CS VREDRAW : wndclass.lpfnWndProc = WndProc : wndclass.cbClsExtra = 0 : wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL IDI PPLICATION) : wndclass.hCursor = LoadCursor (NULL, IDC ARROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEBRUSH) wndclass.lpszMenuName = NULL : wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) i 258 ' Część I: Podstawy (ciąg dalszy ze strony 257) MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBICONERROR) ; I i hwnd = CreateWindow (szAppName, TEXT ("Connect-the-Points Mouse Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; i while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; I i LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static POINT ptCMAXPCINTS] ; static int iCount ; HDC hdc ; int i, j : PAINTSTRUCT ps ; switch (message) ( case WM_LBUTTONDOWN: iCount = 0 ; ' InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_MOUSEMOVE: if (wParam & MK LBUTTON && iCount < 1000) ( pt[iCount ].x = LOWORD (lParam) ; pt[iCount++],y = HIWORD (lParam) ; hdc = GetDC (hwnd) ; SetPixel (hdc, LOWORD (lParam), HIWORD (lParam), 0) ; ReleaseDC (hwnd, hdc) ; , return 0 ; case WMLBUTTONUP: InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; , case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rozdział l: Mysz SetCursor (LoadCursor (NULL, IDC WAIT)) : ShowCursor (TRUE) ; for (i = 0 ; i < iCount - 1 ; i++) for (j = i + 1 ; j < iCount ; j++) .x tCi].y, NULL) ; MoveToEx (hdc, ptCi] , p LineTo (hdc, ptCj].x, ptCj].y) : ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC RROW)) : EndPaint (hwnd. &ps) : return 0 ; case WM DESTROY: PostOuitMessage (0) ; return 0 ; i return DefWindowProc (hwnd, message, wParam, lParam) : ) Rysunek 7-1. Program CONNECT Program CONNECT przetwarza trzy komunikaty myszy: WM LBUTTONDOWN - CONNECT usuwa zawartość obszaru roboczego. WM MOUSEMOVE - jeżeli naciśnięty jest równocześnie lewy przycisk myszy, CONNECT rysuje w obszarze roboczym czarną kropkę oraz zapamiętuje jej współ- rzędne. WMLBUTTONUP - program CONNECT rysuje połączenia pomiędzy wszyst- kimi kropkami zaznaczonymi w obszarze roboczym. Czasem uda się w ten spo- sób stw orzyć ładny wzór (zobacz rysunek 7-2). Rysunek 7-2. Zawartość okna programu CONNECT 260 c:ęść I: Podstawy ; ; Posługiwanie się programem CONNECT jest bardzo proste. Umieść kursor my- szy nad obszarem roboczym, naciśnij jej lewy przycisk, przesuń w dowolny spo- sób jej kursor, a następnie zwolnij przycisk. Program działa najlepiej, jeżeli ma połączyć kilka kropek położonych na krzywej. Program CONNECT posługuje się trzema funkcjami GDI, które opisałem w roz- dziale 5: SetPixel rysuje czarny punkt po każdym odebraniu komunikatu 4VM-MO- I USEMOVE, o ile oczywiście naciśnięty był lewy przycisk myszy. (Jeżeli korzy- stasz z morutora pracującego w trybie o dużej rozdzielczości, punkty te mogą być niemal niewidoczne). Z kolei rysowanie linii wymaga wywołania funkcji Move- ToEx oraz LineTo. Jeżeli trzymając naciśnięty lewy przycisk myszy, przesuniesz jej kursor poza ob- szar roboczy, CONNECT nie będzie mógł połączyć punktów, ponieważ nie od- bierze komunikatów WM-LBUTTONUP Gdy przesuniesz kursor myszy ponow- nie nad obszar roboczy, a następnie naciśniesz ponownie lewy przycisk, zawar- tość obszaru roboczego zostanie usunięta. Jeżeli mimo przesunięcia kursora my- szy poza obszar roboczy chcesz kontynuować rysowanie, naciśnij lewy przycisk myszy, a następnie przesuń kursor nad obszar roboczy. Program CONNECT jest w stanie zapamiętać informacje o położeniu co najwy- żej tysiąca punktów. Jeżeli założymy, że liczba punktów wynosi P, to liczba linii , które muszą zostać narysowane, wynosi P x (P - 1) / 2. Jeżeli okaże się, że użyt- kownik zaznaczył tysiąc punktów, konieczne będzie narysowanie około 500 000 linii, co może zająć minutę lub dwie, zależnie od tego, jakim komputerem dyspo- nujesz. Ponieważ Windows 98 jest systemem wielozadaniowym, możesz w tym czasie pracować z innymi programami. Natomiast nie możesz zrobić w nim nic innego (na przykład przesunąć okno lub zmienić jego rozmiar). W rozdziale 20 zajmiemy się metodami radzenia sobie z problemami tego typu. Ponieważ programowi CONNECT narysowanie wszystkich linii może zająć tro- chę czasu, to w trakcie obsługi komunikatu WM_PAINT zmierńa kształt kursora na klepsydrę, a po zakończeniu obsługi - przywraca kształt początkowy. Wyma- ga to dwukrotnego wywołania funkcji SetCursor dla dwóch standardowych kur- sorów. CONNECT również dwukrotnie wywohzje funkcję ShowCursor: raz z pa- rametrem TRUE, a za drugim razem - z parametrem FALSE. Tymi wywołaniami bardziej szczegółowo zajmę się w dalszej części tego rozdziału pt. "Emulacja my- szy za pomocą klawiatury . Czasem na określenie tego, co robi program po odebraniu informacji o przemiesz- czeniu kursora myszy, używa się słowa "śledzenie". Nie oznacza to oczywiście, że w procedurze okna program wchodzi w pętlę i usiłuje wywnioskować, gdzie w danym momencie znajduje się wskaźnik myszy. Zamiast tego procedura okna przetwarza komunikaty myszy tak szybko, jak to tylko możliwe, a następnie prze- kazuje sterowanie do systemu Windows. Obsługa klawisry specjalnych Gdy program CONNECT odbierze komurukat WMMOUSEMOVE, wykonuje bi- narną operację AND na zawartości wParam oraz MK LBUTTON. Pozwala mu to zorientować się, czy naciśnięty został lewy przycisk myszy. Parametru wParam Rozdział 7: Mysz 261 można użyć również wtedy, gdy chcesz przekonać się, czy naciśnięty został kla- wisz [Shift] albo [CtrI]. Możesz przy tym poshxżyć się następującą konstrukcją: if (wParam & MK SHIFT) ( if (wParam & MK CONTROL) ( Cklawisze CShiftJ i CCtrlJ są naciśnięteJ ) else ( Cnaciśnięty jest klawisz CShiftJJ ) else ( if (wParam & MKCONTROL) ( [naciśnięty jest klawisz CCtrlJJ 1 else { ani CCtrlJJ Cnie jest nacinięty ani CShiftJ, Jeżeli w swoich programach zamierzasz poshxgiwać się zarówno lewym, jak i pra- wym przyciskiem myszy, a ponadto chcesz, aby mogli z tych programów rów- nież korzystać ci, którzy poshzgują się myszą z jednym przyciskiem, powinieneś tak napisać program, aby jednoczesne naciśnięcie klawisza [Shift] oraz lewego przycisku myszy było równoznaczne z naciśnięciem prawego przycisku myszy. Kod obsługujący komunikaty myszy mógłby wtedy wyglądać tak: case WMLBUTTONDOWN: if (!(wParam & MK SHIFT)) ( Cobsluga Iewego przycisku myszyJ return 0 ; // Kontynuacja przetwarzania case WM_RBUTTONDOWN: Cobsługa prawego przycisku myszy] return 0 ; Opisana w rozdziale 6 funkcja Windows GetKeyState może również zwrócić in- formacje dotyczące przycisków myszy oraz klawiszy [Shift[ i [Ctrl], jeżeli jako jej parametr podany zostanie jeden z następujących wirtualnych kodów klawiszy: VK_LBUTTON, VK RBUTTON, VK MBUTTON, VK SHIFT oraz VK CON- TROL. Przycisk lub klawisz jest naciśnięty, jeżeli wartość zwrócona przez Get- KeyState jest ujemna. Ponieważ funkcja ta zwraca informację o stanie przycisków i klawiszy dotyczącą aktualnie przetwarzanego komunikatu, jest ona dość do- brze z nim zsynchronizowana. Jak z pewnością pamiętasz, nie można korzystać z funkcji GetKeyState w odniesieniu do klawiszy, które dopiero mają być wciśnię- te. Nie można również czekać w ten sposób na naciśnięcie przycisku. Dlatego nigdy nie powinieneś pisać takich poleceń jak to: while (GetKeyState (VKLBUTTON) >= 0) ; // ŹLE !!! 262 Część I: Podstawy Funkcja GetKeyState tylko wtedy zwróci informacje o naciśniętym przycisku, je- żeli został on naciśnięty już wcześniej. Kliknięcia dwukrotne Kliknięcie dwukrotne to dwa kliknięcia rozdzielone krótką przerwą. Aby dwa kliknięcia mogły być ze sobą połączone, muszą nastąpić w niezbyt odległych od siebie obszarach (domyślnie, między jednym kliknięciem a drugim kursor my- szy może przesunąć się o około jedną szerokość znaku czcionki systemowej i o pół jego wysokości) oraz w niezbyt długim czasie noszącym nazwę szybkości dwu- krotnego kliknięcia (ang. double-click speed). Szybkość tą możesz modyfikować poshxgując się apletem Mysz znajdującym się w Panelu sterowania. Jeżeli chcesz, aby twoja procedura okna otrzymywała komunikaty związane z dwukrotnym kliknięciem, to zanim zarejestrujesz klasę okna za pomocą funk- cji IZegisterClass, w polu stylu struktury opisującej klasę musisz podać identyfika- tor CS DBLCLKS: wndclass.style = CS HREDRAW CSVREDRAW CSDBLCLKS ; Jeżeli natomiast nie włączysz identyfikatora CS DBLCLKS, a użytkownik dwu- krotnie kliknie lewym przyciskiem myszy, procedura okna otrzyma następujące komunikaty: WMLBUTTONDOWN WM LBUTTONUP 4VMLBUTTONDOWN WM LBUTTONUP Może się również okazać, że pomiędzy wymienionymi powyżej komunikatami procedura okna odbierze również inne komunikaty. Jeżeli zamierzasz zaimple- mentować swoją własną obsługę dwukrotnego kliknięcia, za pomocą funkcji Get- MessageTime możesz pobrać czas, na podstawie którego wyznaczysz odstęp po- między kliknięciami. Funkcją tą dokładniej zajmiemy się w rozdziale 8. Jeżeli do stylu rejestrowanej klasy okna włączysz identyfikator CS_DBLCLKS , w przypadku pojawienia się dwukrotnego kliknięcia procedura okna odbierze na- stępującą serię komunikatów: WMLBUTTONDOWN WMLBUTTONUP WMLBUTTONDBLCLK WMLBUTTONUP Komunikat WM LBUTTONDBLCLK zastąpił po prostu WMLBUTTONDOWN z poprzedniego przykładu. Obsługa dwukrotnego kliknięcia jest znacznie łatwiejsza, jeżeli pierwsze kliknię- cie wykonuje tę samą operację co kliknięcie jednokrotne. Drugie kliknięcie (ko- munikat 4VMLBUTTONDBLCLK) odpowiedzialne jest wtedy za operację do- datkową w porównaniu do pierwszego kliknięcia. Zwróć na przykład uwagę, w jaki sposób mysz wykorzystywana jest do wybierania plików z listy w Eksplo- Rozdział 7: Mysz 263 ratorze Windows. Pojedyncze kliknięcie zaznacza plik. Powoduje to odwrócenie kolorów liter i tła jego nazwy. Z kolei dwukrotne kliknięcie wykonuje dwie ope- racje: w odpowiedzi na komunikat odpowiadający kliknięciu plik zostaje zazna- czony, podczas gdy drugi komunikat, tym razem odpowiadający dwukrotnemu kliknięciu, powoduje otworzenie zaznaczonego pliku. To bardzo proste. Nieste- ty, wszystko się komplikuje, jeżeli przy podwójnym kliknięciu nie wykonuje tej samej czynności, co jednokrotne kliknięcie. Komunikaty myszy nie związane z obszarem roboczym Omówione do tej pory 10 komunikatów myszy pojawia się, gdy jej kursor znaj- duje się wewnątrz obszaru roboczego okna. Jeżeli natomiast kursor znajduje się poza obszarem roboczym, ale w dalszym ciągu w oknie, system Windows wysy- ła do procedury okna odmienny zestaw komunikatów. Obszary te to: pasek tytu- łu, menu oraz paski przewijania. Zwykle nie musisz zajmować się komunikatami spoza obszaru roboczego. Po pro- stu musisz przekazać je do Def4VindowProc, a całą pracę wykona za ciebie Win- dows. W tym sensie komunikaty te podobne są do komunikatów klawiaturowych WM-SYSKEYDOWN, WM SYSKEYUP oraz WM-SYSCHAR. Komunikaty myszy nie związane z obszarem roboczym są niemal dokładnym odpowiednikiem komunikatów związanych z tym obszarem. Ich identyfikatory zawierają litery "NC" (ang. nonclient), co oznacza, że nie dotyczą one obszaru roboczego. Jeżeli kursor myszy przesuwany jest poza obszarem roboczym, do procedury okna wysłany zostaje komunikat WM NCMOUSEMOVE. Przyciski myszy generują komunikaty przedstawione w poniższej tabeli: Przycisk Naciśnięty Zwolniony Naciśnięty (drugie kliknięcie) Lewy WM IVCLBUTTONDOWN WMNCLBUTTONUP WMNCLBUTTONDBLCLK Środkowy WMNCMBUTTONDOWN WM NCMBUTTONUP WMNCMBUTTONDBLCLK Prawy WM NCRBUTTONDOWN WM NCRBUTTONUP WM NCRBUTTONDBLCLK Parametry wParam oraz lParam przekazywane razem z komunikatami myszy nie dotyczącymi obszaru roboczego różnią się jednak od wykorzystywanych przez komunikaty związane z tym obszarem. Parametr wParam wykorzystywany jest do identyfikacji obszaru, w którym naciśnięty został przycisk lub przesunięty kur- sor myszy. Identyfikatory poszczególnych obszarów rozpoczynają się od liter "HT" (ang. hit-test - test na trafienie) i zostały zdefiniowane w pliku nagłówko- wym WINUSER.H. Młodsze słowo parametru lParam wykorzystywane jest do przekazywania współ- rzędnej x kursora, natomiast starsze - współrzędnej y. Jednakże są to współrzęd- ne ekranowe, a nie współrzędne obszaru roboczego, jak miało to miejsce w przy- - Część i: Podstawy padku komunikatów związanych z obszarem roboczym. W przypadku współ- rzędnych ekranowych, w lewym górnym rogu ekranu zarówno współrzędna x, jak i y mają wartość 0. Wartość współrzędnej x zwiększa się w miarę przesuwa- nia się w prawo, natomiast współrzędnej y - w miarę przesuwania się w dół. (Zobacz rysunek 7-3). Możliwe jest przekształcanie współrzędnych ekranowych na współrzędne obszaru roboczego i odwrotnie za pomocą dwóch funkcji systemu Windows: ScreenToClient (hwnd, &pt) ; ClientToScreen (hwnd, &pt) ; Parametr pt jest strukturą POINT. Obie funkcje przekształcają wartości przeka- zywane za pomocą tej struktury bez zachowywania ich poprzednich wartości. Zwróć uwagę, że jeżeli punkt znajduje się poza obszarem roboczym lub po jego lewej stronie, współrzędne obszaru roboczego będą miały wartości ujemne. Wędne ekranu v Rysunek 7-3. Współrzędne ekranowe oraz współrzędne obszaru roboczego Komunikat testu na trafienie obrzeża okna Jeżeli jeszcze się nie zgubiłeś, to wiesz, że do tej pory przedstawiłem już 20 z 21 komunikatów związanych z myszą. Ostatnim z nich jest WMNCHITTEST. Jego nazwa pochodzi od angielskiego nonclient hit test (test na trafienie obrzeża okna). Komunikat ten poprzedza każdy inny komunikat myszy dotyczący zarówno ob- szaru roboczego, jak i obrzeża okna. Za pomocą parametru lParam przekazywa- ne są ekranowe współrzędne x i y kursora myszy. Natomiast wParam nie jest wy- korzystywany. Rozdział 7: klysz 265 Większość aplikacji przekazuje ten komurukat do DeWndowProc. Następnie Win- dows wykorzystuje go do wygenerowania wszystkich pozostałych komunikatów związanych z myszą. W przypadku komunikatów myszy spoza obszaru robo- czego wartość zwrócona przez funkcję DetNindowProc przekazywana jest do pro- cedury okna jako parametr wParam. Może to być dowolna wartość parametru wPa- ram towarzysząca komunikatom myszy nie związanym z obszarem roboczym lub jedna z następująych: HTCLIENT Obszar roboczy HTNOWHEftE Żadne okno HTTRANSPARENT Okno przykryte przez inne HTERROR Def iNindowProc generuje krótki sygnał dźwiękowy Jeżeli po przetworzeniu komunikatu WM NCHITTEST funkcja DeftNindowProc zwróci wartość HTCLlENT, system Windows dokonuje konwersji ze współrzęd- nych ekranowych na współrzędne obszaru roboczego, a następnie generuje ko- munikat myszy. Jeżeli pamiętasz jeszcze, w jaki sposób zablokowaliśmy wszystkie systemowe hxnkcje klawiatury, to zapewne się zastanawiasz, czy można zrobić coś podob- nego z komunikatami myszy. Oczywiście! Jeżeli dodasz do procedury okna na- stępujące linie: case WM NCHITTEST: return (LRESULT) HTNOWHERE ; spowodujesz, że w twoim oknie zostaną zablokowane wszystkie komunikaty myszy zarówno związane z obszarem roboczym, jak i z nim nie związane. Kiedy mysz znajdzie się nad obszarem okna, włączając w to ikonę menu systemowego, przyciski modyfikujące wielkość okna oraz ikonę zamykającą okno, przyciski my- szy nie będą po prostu działać. Komunikaty generujące komunikaty System Windows posługuje się komunikatem WM NCHITTEST do wygenerowa- nia pozostałych komunikatów myszy. Pomysł tworzera jednych komunikatów na podstawie innych jest w tym systemie dość powszechnie stosowany. Rozważmy pewien przykład. Jak z pewnością wiesz, dwukrotne kliknięcie ikony menu syste- mowego powoduje zamknięcie okna. Dwukrotne kliknięcie generuje serię komu- nikatów WMNCHTITEST. Ponieważ kursor myszy znajduje się nad ikoną menu systemowego, funkcja Def 4VindowProc zwraca wartość HTSYSMENU, a Windows umieszcza w kolejce komunikat WMNCLBUTTONDBLCLK, przy czym jego pa- rametrowi wParam nadana zostaje wartość HTSYSMENU. Procedura okna przekazuje zwykle ten komunikat do DefWindowProc. Gdy z ko- lei DeftNindowProc odbierze komunikat WM NCLBUTTONDBLCLK z parame- trem wParam równym HTSYSMENU, umieszcza w kolejce komunikat WM SY- SCOMMAND z wParam równym SC CLOSE. (Ten sam komunikat WM SY SCOMMAND generowany jest również wtedy, gdy użytkownik wybierze pole- cenie Zamknij z menu systemowego). Po raz kolejny procedura okna przekazuje otrzymany komunikat do Def4VindowProc. Z kolei DefWindowProc w odpowiedzi wysyła do funkcji okna komunikat WM CLOSE. 266 Część I: Podstawy R Jeżeli przed zamknięciem program wymaga potwierdzenia użytkownika, to w procedurze okna powinien zostać przechwycony komunikat WM_CLOSE. W przeciwnym bowiem razie trafi on do funkcji Def tNindowPrac, która w odpo- wiedzi wywoła fixnkcję DestroyWindow. Zadaruem tej funkcji jest wysłanie do pro- cedury okna komunikatu WM DESTROY, gdzie zwykle zostaje on obsłużony w następujący sposób: case WM_DESTROY: PostOuitMessage (0) ; return 0 ; Funkcja PostQuitMessage powoduje, że Windows umieszcza w kolejce komuni- kat WMQUIT. Nie dotrze on nigdy do procedury okna, ponieważ po jego po- braniu funkcja GetMessage zwróci wartość 0, co spowoduje zakończenie wykony- wania pętli obshzgi komunikatów programu. Testowanie trafienia w twoich programach Wcześniej wspomniałem, w jaki sposób Eksplorator Windows reaguje na kliknięcia oraz na dwukrotne kliknięcia. Zwykle program (lub kontrolka listy wykorzysty- wana przez Eksplorator Windows) musi dokładnie określić, który plik lub kata- log został wskazany przez użytkownika. Operacja ta nosi miano testowania trafienia (ang. hit-testing). Dokładnie w ten sam sposób, w jaki DefWindowProc po otrzymaniu komunikatu WM_NCHITTEST sprawdza, który element okna został trafiony, procedura okna musi określić, który element wewnątrz jej obszaru roboczego został trafiony. Dlatego też procedura okna musi wykonać pewne obliczenia, posługując się współrzędnymi x i y kur- sora przekazanymi za pomocą parametru lParam. Przykład hipotetyczny Oto przykład. Załóżmy, że twój program ma wyświetlić w kilku kolumnach listę alfabetycznie posortowanych nazw plików. Zwykle powinieneś skorzystać z wi- doku listy, ponieważ potrafi on wykonać za ciebie całą pracę związaną z testo- waniem trafienia. Załóżmy jednak, że z jakiegoś powodu nie jest to możliwe. Musisz zrobić to sam. Załóżmy również, że posortowane alfabetycznie nazwy plików przechowywane są w postaci tablicy wskaźników do napisów o nazwie szFileNames. Załóżmy ponadto, że lista plików wyświetlana jest od górnej krawędzi obszaru roboczego, który ma wielkość zapamiętaną w cxClient oraz cyClient. Poszczegól- ne kolumny mają szerokość cxColWidth punktów, natomiast znaki wykorzysty- wanej przez nas czcionki mają wysokość cyChar punktów. Liczba plików, które możesz wyświetlić w jednej kolumnie, wynosi: iNumInCol = cyClient / cxColWidth ; Gdy program otrzymuje komurukat wywołany kliknięciem myszą, współrzędne jej kursora cxMouse oraz cyMouse możesz pobrać z parametru IParam. Następnie, poshzgując się przedstawionym niżej wyrażeniem, możesz wyznaczyć kolumnę, którą wskazał użytkownik: Rozdział 7: Mysz iColumn = cxMouse /cxColWidth ; Położenie nazwy pliku względem początku kolumny to iFromTop = cyMouse /cyChar ; Możesz teraz wyznaczyć indeks w tablicy szFileNames. iIndex = iColumn * iNumCol + iFromTop ; Jeżeli wartość iIndex jest większa niż liczba nazw plików przechowywanych w ta- blicy, oznacza to, że użytkownik kliknął pusty obszar okna. W wielu przypadkach testowanie trafienia jest bardziej złożone, niż w przedstawio- nym przykładzie. Gdy wyświetlasz rysunek składający się z wielu części, musisz określić położenie każdej z nich. Testowanie trafienia polega bowiem na przekształ- ceniu współrzędnych na interesujący cię obiekt. Zadanie to może okazać się szcze- gólnie trudne w przypadku edytorów tekstu posługujących się czcionką o zmiennej szerokości, ponieważ należy cofnąć się i znaleźć pozycję znaku w napisie. Przykładowy program Przedstawiony na rysunku 7-4 program CHECKEIZI demonstruje prosty test na trafienie. Dzieli on obszar roboczy na 25 prostokątów tworzących tablicę o wy- miarach 5 na 5. Jeżeli klikniesz myszą dowolny z prostokątów, zostanie w nim wpisany znak X. Jeżeli klikniesz go po raz kolejny, znak ten zostanie usunięty. CHECKERl.C /* CHECKERl.C - Test trafień myszy, wersja 1 (c) Charles Petzold, 1998 */ ilinclude 4idefine DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("Checkerl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CS llREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL IDC RROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEBRUSH) wndclass.lpszMenuName = NULL ; wnfclass.lpszClassName = szAppName ; Część t: Podstawy (ciąg dalszy ze strony 267) if (!RegisterClass (&wndclass)) , MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBICONERROR) ; J hwnd = CreateWindow (szAppName, TEXT ("Checkerl Mouse Hit-Vest Demo"), ; WS_OVERLAPPEDWINDOW, ; CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) , TranslateMessage (&msg) ; DispatchMessage (&msg) ; 1 return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL fState[DIVISIONS](DIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; RECT rect ; switch (message) case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM LBUTTONDOWN : x = LOWORD (lParam) / cxBlock ; y = HIWORD (lParam) l cyBlock ; if (x < DIVISIONS && y < DIVISIONS) ( fState [x][y] ^= 1 ; rect.left = x * cxBlock ; rect.top = y * cyBlock ; rect.right = (x + 1) * cxBlock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; Rozdział 7: Mysz 269 else MessageBeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) f Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxBlock, (y + 1) * cyBlock) ; if (fState [x][y]) 1 MoveToEx (hdc, x * cxBlock, y * cyBlock, NULL) ; LineTo (hdc, (x+1) * cxBlock. (y+1) * cyBlock) ; MoveToEx (hdc. x * cxBlock, (y+1) * cyBlock, NULL) ; LineTo (hdc, (x+1) * cxBlock, y * cyBlock) ; l EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-4. Program CHECKER1 Na rysunku 7-5 przedstawione zostało okno programu CHECKERl. Wszystkie prostokąty narysowane przez program mają takie same wymiary. Szerokość i wy- sokość przechowywane są w zmiennych cxBlock oraz cyBlock. Ich wartości wy- znaczane są za każdym razem, gdy zmienia się wielkość okna. W ramach obsłu- gi komunikatu WMLBUTTONDOWN do określenia, który z prostokątów zo- stał wskazany, wykorzystywane są współrzędne kursora myszy. Następnie zmie- niany jest stan prostokąta przechowywany w tablicy fState. Na koniec zostaje unie- ważniony obszar roboczy, co z kolei powoduje wygenerowanie komunikatu WM PAINT. 270 Część I: Podstawy Rysunek 7-5. Okno programu CHECKERI Jeżeli okaże się, że wysokość lub szerokość okna nie jest podzielna przez 5, nie- wielki pasek po lewej stronie lub u dołu nie zostanie pokryty przez żaden z pro- stokątów. Kliknięcie jednego z tych obszarów traktowane jest jako błąd i sygnali- zowane za pomocą MessageBeep. Gdy CHECKER1 otrzyma komunikat WM PAINT, odświeży cały obszar robo- czy, posługując się funkcją GDI ftectangle. Jeżeli flaga fState odpowiadająca dane- mu prostokątowi jest ustawiona, program rysuje dwie przekątne prostokąta, wy- wohzjąc funkcje MoveToEx oraz LineTo. W trakcie obsługi komunikatu WMPA- INT program CHECKER1 nie sprawdza, czy dany prostokąt znalazł się w unie- ważnionym obszarze roboczym. Jedna z możliwych metod sprawdzania polega na budowaniu struktury RECT dla każdego prostokąta (można w tym celu wy- korzystać kod zbliżony do tego, który pojawił się w obsłudze komunikatu WMLBUTTONDOWN). Następnie, wywołując funkcję Intersetftect, należałoby sprawdzić, czy dany prostokąt ma część wspólną z unieważnionym obszarem (dostępny jako ps.rcPaint). Emulacja myszy za pomocą klawiatury Do poshzgiwania się programem CHECKER1 niezbędna jest mysz. Wkrótce uzu- pełnimy go o interfejs klawiatury tak, jak zrobiliśmy to w programie SYSMETS opisanym w rozdziale 6. Ponieważ jednak tym razem mysz wykorzystywana jest do wskazywania obiektów, konieczne będzie zatroszczenie się o wyświetlanie i przemieszczanie kursora myszy. Nawet jeżeli mysz nie jest zainstalowana, Windows w dalszym ciągu może wy- świetlać jej kursor. Jest z nim związane coś, co w tym systemie nosi nazwę "licz- nika wyświetleń" (ang. display count). Jeżeli mysz jest zainstalowana, jego począt- kową wartością jest 0, w przeciwnym wypadku jest to -1. Kursor myszy wyświe- tla się tylko wtedy, gdy wartość liczruka nie jest ujemna. Licznik możesz zwięk- szyć, wywołując: Rozdział 7: Mysz 271 ShowCursor (TRUE) ; natomiast zmniejszyć za pomocą: ShowCursor (FALSE) ; Zanim wywołasz funkcję ShowCursor, nie musisz sprawdzać, czy mysz jest zain- stalowana. Jeżeli niezależnie od jej obecności chcesz wyświetlić kursor, po prostu zwiększ wartość licznika, wywołując ShowCursor. Teraz zmniejszenie jego warto- ści spowoduje ukrycie kursora, jeśli mysz nie jest zainstalowana. A jeśli mysz była obecna w systemie, kursor pozostanie na ekranie System Windows zarządza informacją o aktualnym położeniu kursora nawet wtedy, gdy mysz nie została zainstalowana. Może nawet wyświetlić kursor w do- wolnym punkcie ekranu. Pozostanie on tam tak dhzgo, jak dhzgo jawnie go nie przeniesiesz. Położenie kursora możesz pobrać, wywołując: GetCursorPos (&pt) gdzie pt jest strukturą POINT. W jej polach umieszczone zostają współrzędne x i y myszy. Pozycję kursora możesz ustawić, wywohxjąc SetCursorPos (x, y) ; W obu przypadkach x i y to współrzędne ekranowe. (Powinno to być jasne, po- nieważ funkcja nie wymaga podania parametru hwnd). Jak już wspomniałem, możesz konwertować współrzędne ekranowe na współrzędne obszaru robocze- go i vice versa, wywołując funkcję ScreenToClient oraz ClientToScreen. Jeżeli zdecydujesz się wywołać funkcję GetCursorPos w trakcie obsługiwania komu- nikatu myszy, może się okazać, że otrzymane w ten sposób współrzędne różnią się nieco od tych, które przekazane zostały za pomocą parametru lParam. Dzieje się tak, ponieważ funkcja GetCursorPos zwraca aktualne współrzędne, natomiast IParam prze- chowuje współrzędne kursora z momentu wygenerowania komunikatu. Prawdopodobnie chcesz, aby napisany przez ciebie interfejs klawiatury pozwo- lił na przemieszczanie kursora myszy za pomocą klawiszy strzałek, natomiast naciśnięcie klawiszy [Spacja] lub [Enter] symulowało przyciśnięcie przycisku my- szy. Na pewno nie chcesz, aby kursor myszy przemieszczał się o jeden punkt za każdym naciśnięciem klawisza. Zmuszałoby to użytkownika do długiego przy- trzymania klawisza. Jeżeli mimo wszystko zdecydowałeś, że w zaimplementowanym przez ciebie interfejsie klawiatury kursor będzie się przemieszczał co jeden punkt, zrób to przynajmniej tak, aby na początku jego ruch był wolny, a później ulegał przy- śpieszeniu. Z pewnością pamiętasz, że parametr IParam towarzyszący komuni- katowi WMKEYDOWN zawiera informację o tym, czy komunikat ten został wy- generowany na skutek automatycznego powtarzania klawisza. Jest to idealne za- stosowanie tej informacji. Dodanie interfejsu klawiatury do programu CHECKER Przedstawiony na rysunku 7-6 program CHECKER2 jest kopią wcześniejszego CHECKERI, z tą jednak różnicą, że został w nim zaimplementowany interfejs klawiatury. Do przemieszczania kursora pomiędzy prostokątami możesz posłu- żyć się klawiszami kursora. Naciśnięcie klawisza [Home] spowoduje przeniesie- 272 CzęśĆ I: Podstawy nie kursora do górnego prawego prostokąta, natomiast klawisza [End] - do dol- nego lewego. Naciśnięcie klawisza [Spacja] lub [Enter] powoduje zaznaczenie wskazywanego aktualnie prostokąta. CHECKER2.C /* CHECKER2.C - Test trafień myszy, wersja 2 (c) Charles Petzold, 1998 */ include define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine. int iCmdShow) static TCHAR szAppNameC] = TEXT ("Checker2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndcla.ss.hbrBackground = (HBRUSH) GetStockObjecv (WHITE BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBICONERROR) ; return 0 ; j ) hwnd = CreateWindow (szAppName, TEXT ("Checker2 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWUSEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; ; Rozdział 7: Mvsz 273 ; return msg.wParam ; i LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParcm, LPARAM lParam) I ? static BOOL fStateCDIVISIONS]CDIVISIONS] ; static int cxBlock, cyBlock ; HDC hdc ; int x, y ; PAINTSTRUCT ps ; POINT point ; RECT rect ; switch (message) i f case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; return 0 ; case WM_SETFOCUS : ShowCursor (TRUE) ; return 0 ; I i case WM_KILLFOCUS : ShowCursor (FALSE) ; . return 0 ; case WM_KEYDOWN : GetCursorPos (&point) , ScreenToClient (hwnd, &point) ; x = max (0, min (DIVISIONS - 1, point.x / cxBlock)) ; y = max (0, min (DIVISIONS - 1, point.y / cyBlock)) ; switch (wParam) ( case VK UP : break ; case VK_DOWN : y++ ; break ; case VK_LEFT : x-- break ; case VK_RIGHT : x++ ; break ; case VK_HOME : x = y = 0 ; break ; case UK_END : x = y = DIVISIONS - 1 ; 274 Część I: Podstawy (ciąg dalszy ze strony 273) break ; case UK_RETURN : case VK_SPACE : SendMessage (hwnd, WM_LBUTTONDOWN, MK LBUTTON, MAKELONG (x * cxBlock, y * cylock)) ; break ; ) x = (x + DIVISIONS) % DIVISIONS ; y = (y + DIVISIONS) % DIVISIONS ; point.x = x * cxlock + cxlock / 2 ; point.y = y * cylock + cyBlock / 2 ; ClientToScreen (hwnd, &point) ; SetCursorPos (point.x, point.y) ; return 0 ; case WM_LBUTTONDOWN : x = LOWORD (lParam) / cxlock ; y = HIWORD (lParam) / cylock ; if (x < DIVISIONS && y < DIVISIONS) ( fState[xCy] ^= 1 ; rect.left = x * cxlock ; rect.top = y * cylock ; rect.right = (x + 1) * cxlock ; rect.bottom = (y + 1) * cyBlock ; InvalidateRect (hwnd, &rect, FALSE) ; ) else Messageeep (0) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (x = 0 ; x < DIUISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) ( Rectangle (hdc, x * cxBlock, y * cyBlock, (x + 1) * cxlock, (y + 1) * cyBlock) ; if (fState Cx]Cy]) ( MoveToEx (hdc, x *cxBlock, y *cyBlock, NULL) ; LineTo (hdc, (x+1)*cxlock, (y+1)*cyBlock) ; MoveToEx (hdc, x *cxBlock, (y+1)*cyBlock, NULL) ; LineTo (hdc, (x+1)*cxlock, y *cyBlock) ; ) ) EndPaint (hwnd, &ps) ; return 0 ; Rozdział 7: MVsz 275 case WM_DESTROY : PostOuitMessage (0) ; return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-6. Program CHECKER2 Obsługa komunikatu WM-KEYDOWN w programie CHECKER2 obejmuje okre- ślenie pozycji kursora za pomocą funkcji GetCursorPos, konwersję współrzędnych ekranowych na współrzędne obszaru roboczego za pomocą ScreenToClient oraz podzielenie każdej z nich przez szerokość i wysokość prostokąta. W ten sposób otrzymuje się pozycje w tablicy o rozmiarach 5 na 5. Ponieważ w momencie klik- nięcia kursor nie musi znajdować się nad obszarem roboczym, konieczne jest prze- filtrowanie otrzymanych współrzędnych przez makra min i max, co spowoduje, że rezultat będzie zawierał się w przedziale od 0 do 4. Jeżeli naciśnięty został któryś z klawiszy sterujących ruchem kursora, współrzędne x oraz y zostają odpowiednio zwiększone lub zmniejszone. Jeżeli natomiast naci- śnięta została [Spacja] lub [Enter], CHECKER2 wysyła sam do siebie komunikat WMLBUTTONDOWN. Jest to metoda bardzo podobna do tej, która została za- stosowana w programie SYSMETS z rozdziału 6. Obsługa komunikatu WM KEY DOWN kończy się wyznaczeniem współrzędnych obszaru roboczego wskazują- cych na środek prostokąta, przekonwertowaniu ich na współrzędne ekranowe za pomocą funkcji ClientToScreen oraz na ustawieniu kursora myszy za pomocą Set- CursorPos. Wykorzystanie okien potomnych do testowania trafienia Część programów (na przykład Windows Paint) dzieli obszar roboczy na kilka mniejszych obszarów. Po lewej stronie okna Painta umieszczone zostały ikony oznaczające dostępne narzędzie, natomiast u dołu - paleta kolorów. Gdy program ten sprawdza, czy któryś z tych obszarów nie został trafiony, musi wziąć pod uwagę fragment obszaru roboczego, zanim spróbuje określić, który właściwie ele- ment użytkowruk miał na myśli. A może wcale tak nie jest? W rzeczywistości Paint upraszcza sobie zarówno rysowa- nie, jak i sprawdzanie trafionego obszaru, przez wykorzystanie okien potomnych. Pozwalają one na podział całego obszaru roboczego na kilka mniejszych prostokąt- nych obszarów. Każde z okien potomnych ma swój własny uchwyt, procedurę okna oraz obszar roboczy. Każda z procedur odbiera tylko te komunikaty, które dotyczą jej okna. W komunikatach myszy parametr IParam zawiera współrzędne kursora względem lewego górnego rogu obszaru roboczego okna potomnego, a nie wzglę- dem okna nadrzędnego (które w przypadku Painta jest głównym oknem programu). Takie wykorzystanie okien potomnych ułatwia modularyzację programu. Jeżeli okna te oparte zostały o inne klasy, każde z nich może posługiwać się swoją wła- sną procedurą okna. Okna różnych klas mogą również mieć inne tła oraz różne kursory. W rozdziale 9 zapoznamy się z kontrolkami okien potomnych, które są predefiniowanymi oknami przyjmującymi postać pasków przewijania, przycisków 276 Czść I: Podstawy lub pól edycji. Jednak teraz zajmijmy się tym, w jaki sposób możemy wykorzy- stać okna potomne w programie CHECKER. Okna potomne w programie CHECKER Na rysunku 7-7 przedstawiony został program CHECKER3. Ta wersja tworzy 25 okien potomnych, których zadaniem jest reagowanie na komunikaty myszy. Nie został w nim zaimplementowany interfejs klawiatury, jednak opierając się na tym, co przedstawiłem w jego poprzedniej wersji, z łatwością możesz zrobić to sam. CHECKER3.C /* CHECKER3.C - Test trafień myszy, wersja 3 (c) Charles Petzold, 1998 */ include define DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szChildClassC] = TEXT ("Checker3 Child") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[7 = TEXT ("Checker3") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CSVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB-ICONERROR) ; return 0 wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; Rozial 7: Mysz 277 ł RegisterClass (&wndclass) ; hwnd = CreateWindow (szAppName, TEXT ("Checker3 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) f TranslateMessage (&msg) ; DispatchMessage (&msg) ; i return msg.wParam ; I LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i i static HWND hwndChildCDIVISIONS]CDIVISIONS] ; int cxBtock, cyBlock, x, y ; I: switch (message) case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) i for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x][y] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW WS VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) (y 8 x), (HINSTANCE) GetWindowLong (hwnd, GWL HINSTAtdCE), NULL) ; ! return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow (hwndChildCx][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM_LBUTTONDOWN : ( MessageBeep (0) ; I return 0 ; case WM_DESTROY : PostOuitMessage (0) ; ' return 0 ; , return DefWindowProc (hwnd, message, wParam, lParam) ; ) 278 Część I: Podstawy i (ciąg dalszy ze strony 277) i LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) ( case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // flaga włdczenia/wyldczenia return 0 ; case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, 1 ^ GetWindowLong (hwnd, 0)) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; if (GetWindowLong (hwnd, 0)) MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) LineTo (hdc, rect.right, 0) ; EndPaint (hwnd, &ps) ; return 0 ; ) return OefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-7. Program CHECKER3 Program CHECKER3 posiada dwie funkcje okna o nazwach WndProc oraz Chil- dWndProc. Pierwsza z nich w dalszym ciągu wykorzystywana jest przez główne okno programu (okno nadrzędne). Z kolei ChildWndProc jest funkcją okna dla 25 okien potomnych. Obie funkcj,e muszą zostać zdefiniowane jako CALLBACK. Ponieważ procedura okna związana jest z jedną, szczególną strukturą klasy okna, którą rejestrujesz za pomocą funkcji RegisterClass, program CHECKER3 wymaga zarejestrowania dwóch klas. Klasa pierwszego okna przeznaczona jest dla okna głównego i nosi nazwę "Checker3". Natomiast nazwa drugiej klasy to "Chec- ker3 Child". Wybrane nazwy nie muszą spełniać jakichś istotnych kryteriów. CHECKER3 rejestruje obie klasy okna w funkcji WinMain. Po zarejestrowaniu pierwszej, wykorzystuje tę samą strukturę wndclass w trakcie rejestracji drugiej. Wszystkie jej pola, za wyjątkiem czterech przedstawionych niżej, nie są modyfi- kowane: 6 Rozdział 7: Mysz ; ł Polu lpfnWndProc przypisywana jest procedura okna potomnego ChiIdWnd- ' Proc. ł Polu cbWndExtra zostaje nadana wartość 4, a dokładniej siezeof(long). Zawar- tość tego pola nakazuje systemowi Windows zarezerwowanie dodatkowych 4 bajtów na wewnętrzne dane, dostępne dla każdego okna, które zostanie stwo- rzone na podstawie tej klasy. Nie będą to jednak wspólne dane okien: każde z nich może przechowywać w tym obszarze swoje własne informacje. ł Pole hlcon zostaje ustawione na NULL, ponieważ takie okna potomne, jakie wykorzystane zostały w programie CHECKER3, nie potrzebują ikon. ł Polu pszClassName nadany zostaje łańcuch "Checker3 Child" będący nazwą klasy. Funkcja CreateWindow na podstawie klasy Checker3 tworzy okno główne progra- mu wywołane w WinMain. Ten fragment programu niczym nie różni się od po- przednich. Jednak gdy funkcja WinProc otrzyma komunikat WM CREATE, wy- wołuje CreateWindow 25 razy, aby w oparciu o klasę Checker3 Child, utworzyć 25 okien potomnych. W poniższej tabeli porównane zostały parametry przeka- zywane do funkcji CreateWindow wywoływanej w WinMain oraz w WndProc. Parametr Okno główne Okno potomne klasa okna "Checker3" "Checker3 Child" nagłówek okna "Checker3 .... NULL styl okna WM OVERLAPPEDWINDOW WS CHILDWINDOW I WS VISIBLE położenie w pionie CW USEDEFAULT 0 położenie w poziomie CW USEDEFAULT 0 ' szerokość CW USEDEFAULT 0 wysokość CW USEDEFAULT 0 ! uchwyt okna nadrzędnego NULL hwnd uchwyt menu/ NULL (HMENU)(y 8 I x) identyfikator potomka uchwyt realizacji hInstance (HINSTANCE) GetWindowLong (hwnd, GWL HINSTANCE) dodatkowe parametry NULL NULL Zwykle okna potomne wymagają podania parametrów określających ich wiel- kość oraz położenie. Jednak w programie CHECKER są one odpowiednio roz- mieszczane w oknie nadrzędnym w dalszej części funkcji WndProc. W przypad- ku okna głównego uchwyt okna nadrzędnego ma wartość NULL, ponieważ wła- śnie to okno jest oknem nadrzędnym. Z kolei parametr ten jest obowiązkowy w przypadku wywołania funkcji CreateWindow tworzącej okno potomne. Okno główne nie ma menu, dlatego odpowiadający mu parametr ma wartość NULL. W przypadku okien potomnych ten sam parametr nosi nazwę "identyfi- kator potomka" (ang. child ID) lub "identyfikator okna potomnego" (ang. child window ID). Jest to liczba, dzięki której możliwa jest jednoznaczna identyfikacja okna potomnego. Identyfikator ten ma większe znaczenie, gdy pracujesz z kon- . 280 Część I. Podstawy trolkami umieszczonymi w oknie dialogowym, o czym przekonasz się w rozdziale 11. W programie CHECKER3 zdecydowałem, że identyfikatorem tym będzie liczba wynikająca ze współrzędnych x i y, które określają położenie każdego okna w ta- blicy o 5 wierszach i 5 kolumnach w oknie głównym. Funkcja CreateWindow wymaga podamia uchwytu instancji (realizacji). W funkcji Win- Main uchwyt ten jest bardzo łatwo dostępny, ponieważ jest jej parametrem. Gdy z kolei tworzone jest okno potomne, CHECKEIZ3 musi posłużyć się funkcją GetWindowLong, która umożliwia pobranie wartości hlnstance ze struktury, którą Windows przecho- wuje dla każdego okna. (Zamiast wy'woływania GetWindowLong mógłbym zdefinio- wać po prostu zmienną globalną i korzystać bezpośrednio z jej zawartości). Każde z okien potomnych ma swój własny uchwyt, który przechowywany jest w tablicy hwndChild. Gdy funkcja WndProc odbierze komurukat WMSIZE, dla każdego z 25 okien potomnych wywołana zostanie funkcja MoveWindow. Para- metry tej funkcji pozwalają na określenie lewego górnego rogu okna potomnego względem obszaru roboczego okna głównego, a także jego wysokości i szeroko- ści. Możesz również określić, czy zawartość okna potomnego powinna zostać od- świeżona. Przyjrzyjmy się teraz ChildWndProc - funkcji okna potomnego. Jest ona odpowie- dzialna za przetwarzanie komunikatów docierających do każdego z 25 okien potomnych. Przekazywany do niej parametr hwnd jest uchwytem okna potom- nego, które odebrało komunikat. Gdy ChildWndProc przetwarza komunikat WMCREATE (co odbywa się 25 razy, ponieważ tyle jest okien potomnych), wy- wołuje funkcję SetWindowWord, umieszczając w obszarze danych okna wartość 0. (Jak z pewnością pamiętasz, zarezerwowaliśmy ten obszar, korzystając z pola cbWndExtra struktury definiującej klasę okna). Funkcja ChildWndProc poshxguje się tą wartością, aby przechować aktualny stan prostokąta (zaznaczony lub nie). Po kliknięciu okna potomnego, w ramach obshzgi komunikatu WMLBUTTON- DOWN zmieniana jest po prostu wartość tej flagi (z 0 na 1 lub z 1 na 0), a następ- nie unieważniony zostaje cały obszar okna. Obshzga komunikatu WM PAINT jest tym razem trywialna, ponieważ wielkość prostokąta, który należy narysować, jest dokładnie taka sama jak wielkość obszaru roboczego. Ponieważ zarówno plik z kodem źródłowym, jak i z programem są większe niż w przypadku programu CHECKERl, nie będę nawet próbował przekonać cię, że CHECKER3 jest "prostszy". Zwróć jednak uwagę, że nie musimy już wykony- wać żadnych testów na trafienie! Jeżeli okno potomne w programie CHECKER3 otrzyma jakikolwiek komunikat WM LBUTTONDOWN, oznacza to, że zostało wybrane i nic więcej nie musi ono wiedzieć. Okna potomne a klawiatura Wydaje się, że dodanie do CHECKER3 interfejsu klawiatury będzie ostatnią lo- giczną rzeczą, którą będziemy mogli zrobić. Jednak tym razem powinniśmy po- dejść do problemu trochę inaczej. W programie CHECKER2 położenie kursora myszy mówiło, które okno zostanie zaznaczone, gdy naciśnięty zostanie klawisz [Spacja]. Ponieważ korzystamy z okien potomnych, możemy w pewnym stopniu wzorować się na funkcjonowaniu okien dialogowych. Jeżeli okno potomne w oknie Rozdzial 7: Mysz 281 dialogowym ma fokus, sygnalizuje to za pomocą migającej karetki lub naryso- wanego wokół siebie przerywanego prostokąta. Nie zamierzam oczywiście powielać tu wszystkich funkcji okien dialogowych, które oczywiście są już "zaszyte" gdzieś w systemie. Zamierzam po prostu z grub- sza symulować w aplikacji zachowanie się okna dialogowego. Gdy będę wyja- śniał, w jaki sposób należy to zrobić, odkryjesz, że okno główne oraz okna po- tomne powinny wspólnie obsługiwać klawiaturę. Okno potomne powinno nary- sować lub usunąć zaznaczenie, jeżeli naciśnięty został klawisz [Spacja] lub [En- ter]. Z kolei okno główne powinno przenosić fokus z jednego okna potomnego do następnego, gdy naciśnięty zostanie któryś z klawiszy kursora. Czynnoci te ulegną jednak pewnej komplikacji, ponieważ kliknięcie okna potomnego spowo- duje, że fokus otrzyma nie ono, ale okno główne. Program CHECKER4 przedstawiony został na rysunku 7-8. CHECKER4.C /* CHECKER4.C - Test trafień myszy, wersja 4 (c) Charles Petzold, 1998 */ include 4idefine DIVISIONS 5 LRESULT CALLBACK WndProc (HWND, UINT. WPARAM, LPARAM) ; LRESULT CALLBACK ChildWndProc (HWND, UINT, WPARAM, LPARAM) ; int idFocus = 0 ; TCHAR szChildClassCJ = TEXT ("Checker4 Child") ; int WINAPI WinMain (HINSTANCE hInstance, KINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameCJ = TEXT ("Checker4") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW ( CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDCARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB-ICONERROR) ; 282 Część I: Podstawy (ciąg dalszy ze strony 281) return 0 i wndclass.lpfnWndProc = ChildWndProc ; wndclass.cbWndExtra = sizeof (long) ; wndclass.hIcon = NULL ; wndclass.lpszClassName = szChildClass ; RegisterClass (&wndclass) ; i hwnd = CreateWindow (szAppName, TEXT ("Checker4 Mouse Hit-Test Demo"), WS_OVERLAPPEDWINDOW, CW USEDEFAULT, CW USEDEFAULT, I - - CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; i return msg.wParam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) static HWND hwndChildCDIVISIONS][DIVISIONS] ; int cxBlock, cyBlock, x, y ; , switch (message) case WM_CREATE : for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) hwndChild[x]Cy] = CreateWindow (szChildClass, NULL, WS_CHILDWINDOW WS-VISIBLE, 0, 0, 0, 0, ' hwnd, (HMENU) (y 8 x), (HINSTANCE) GetWindowLong (hwnd, GWL-HINSTANCE), NULL) ; return 0 ; case WM_SIZE : cxBlock = LOWORD (lParam) / DIVISIONS ; - cyBlock = HIWORD (lParam) / DIVISIONS ; for (x = 0 ; x < DIVISIONS ; x++) for (y = 0 ; y < DIVISIONS ; y++) MoveWindow (hwndChild[x][y], x * cxBlock, y * cyBlock, cxBlock, cyBlock, TRUE) ; return 0 ; case WM-LBUTTONDOWN : Rozdział 7: Mysz 283 MessageBeep (0) ; , return 0 ; // Po odebraniu komunikatu SETFOCUS przenieś fokus // do okna potomnego case WM_SETFOCUS: SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; // Po odebraniu komunikatu WM_KEYDOWN, możliwe że należy // zmienić okno, które ma fokus case WM_KEYDOWN: x = idFocus & OxFF ; y = idFocus Ż 8 ; i switch (wParam) case UK_UP: y-- , break ; ' case UK_DOWN: y++ ; break ; case VK LEFT: x-- , break ; case VK RIGHT: x++ ; break ; case VK HOME: x = y = 0 ; break ; case VKEND: x = y = DIVISIONS - 1 ; break ; default: return 0 ; ) x = (x, + DIVISIONS) 6 DIVISIONS ; y = (y + DIVISIONS) DIVISIONS ; idFocus = y 8 x ; 1 SetFocus (GetDlgItem (hwnd, idFocus)) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; return 0 ; t, return DefWindowProc (hwnd, message, wParam, lParam) ; LRESULT CALLBACK ChildWndProc (HWND hwnd, UINT message ś WPARAM wParam, LPARAM lParam) HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) case WM_CREATE : SetWindowLong (hwnd, 0, 0) ; // flaga wlączona/wyldczona return 0 ; "''jr:, , case WM_KEYDOWN: i,: // Wyślij większość komunikatów klawiaturowych // do okna 9lównego 284 Część 1: Podstawy (ciąg dalszy ze strony 283) if (wParam != VK RETURN && wParam != VKSPACE) SendMessage (GetParent (hwnd), message, wParam, lParam) ; return 0 ; ) I // Jeżeli naciśnięto [Enter] lub spację - kontynuacja // i zaznaczenie lub odznaczenie prostokąta case WM_LBUTTONDOWN : SetWindowLong (hwnd, 0, i ^ GetWindowLong (hwnd, 0)) ; SetFocus (hwnd) ; InvalidateRect (hwnd, NULL, FALSE) ; return 0 ; // Komunikaty zwidzane z fokusem - unieważnienie obszaru // roboczego wymuszajdce ponowne wyświetlenie case WM_SETFOCUS: idFocus = GetWindowLong (hwnd, GWLID) ; // kontynuacja case WM KILLFOCUS: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd. &ps) ; GetClientRect (hwnd, &rect) ; Rectangle (hdc, 0, 0, rect.right, rect.bottom) ; // Narysuj znak "x" if (GetWindowLong (hwnd, 0)) ( MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, rect.right, rect.bottom) ; MoveToEx (hdc, 0, rect.bottom, NULL) ; LineTo (hdc, rect.right, 0) ; // Narysuj prostokdt oznaczajdcy fokus if (hwnd GetFocus ()) i rect.left += rect.right / 10 ; rect.right -= rect.left ; rect.top += rect.bottom / 10 ; rect.bottom -= rect.top ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; SelectObject (hd.c, CreatePen (PS DASH, 0, 0)) ; Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ; Oelete0bject (Sel.ectObject (hdc, GetStockObject (BLACKPEN))) ; Rozdziai 7: Mysz 285 EndPaint (hwnd, &ps) ; return 0 ; ? return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-8. Program CHECKER4 Jak pamiętasz, każde z okien potomnych ma swój własny unikatowy identyfika- tor, który został zdefiniowany podczas tworzenia okna za pomocą funkcji Create- Window. W naszym programie jest on związany ze współrzędnymi prostokąta x i y. Dysponując uchwytem okna, program może pobrać jego identyfikator za po- mocą: idChild = GetWindowLong (hwndChild, GWLID) ; Poruższa funkcja wykonuje dokładnie tę samą operację: idChild = GetDlgCtrlID (hwndChild) ; Jak sugeruje nazwa funkcji, jej podstawowym zadaniem jest praca z oknami dia- logowymi i znajdującymi się w nich kontrolkami. Jeżeli znasz uchwyt okna głów- nego oraz identyfikator okna potomnego, z łatwością możesz wyznaczyć uchwyt okna potomnego: hwndChild = GetDlgItem (hwndParent, idChild) ; Zmienna globalna idFocus wykorzystywana jest przez program CHECKER4 do przechowywania identyfikatora tego okna potomnego, które aktualnie ma fokus. Jak wspomniałem wcześniej, kliknięcie myszą okna potomnego nie powoduje automatycznie, że otrzyma fokus. Dlatego też okno główne powinno w takiej sy- tuacji obshzżyć komunikat WMSETFOCUS, wywohxjąc SetFocus (GetDlgItem (hwnd, idFocus)) ; co spowoduje ustawienie fokusu wejściowego na wybranym oknie potomnym. Funkcja ChildWndProc przetwarza zarówno komunikat WM SETFOCUS, jak i WMKILLFOCUS. W przypadku pojawienia się tego pierwszego, w zmiennej globalnej idFocus zapamiętany zostaje identyfikator okna potomnego, które otrzy- mało ten komunikat. Następnie niezależnie od tego, który komunikat został ode- brany, obszar roboczy okna zostaje unieważniony, co z kolei powoduje wygene- rowanie komunikatu WMPAINT. Jeżeli w trakcie obsługi tego komunikatu oka- że się, że odrysowywane okno ma fokus, do narysowania prostokąta wykorzy- stywane jest pióro PS DASH. Funkcja ChiIdWndProc przetwarza również komunikat WMKEYDOWN. Jeżeli nie został on spowodowany naciśnięciem [Spacji] ani [Enter], zostaje odesłany do okna głównego. W przeciwnym wypadku wykonywane są takie same czyn- ności jak po odebraniu komunikatu WMLBUTTONDOWN. Przetwarzanie naciskanych klawiszy kursora przekazane zostało do okna głów- nego. Podobnie jak miało to miejsce w CHECKER2, program pobiera współrzęd- ne x i y tego okna potomnego, które ma fokus, a następnie przenosi fokus w spo- sób zależny od naciśniętego klawisza, korzystając przy tym z funkcji SetFocus. 286 Część I: Podstawy Przechwytywanie myszy Zwykle procedura okna otrzymuje komurukaty myszy tylko wtedy, gdy jej kur- sor znajduje się nad obszarem okna. Jednak w pewnych sytuacjach może się oka- zać, że program musi również otrzymywać te komunikaty, nawet jeżeli mysz zna- lazła się poza oknem. W takiej sytuacji możliwe jest przechwycenie (ang. capture) myszy. Nie bój się, to nie będzie bolało. Blokowanie prostokąta Poniższy przykład pomoże ci zrozumieć, dlaczego w pewnych sytuacjach prze- chwytywanie myszy może okazać się niezbędne. Popatrz na przedstawiony na rysunku 7-9 program BLOKOUTI. Na pierwszy rzut oka może się wydawać, że wszystko jest w jak najlepszym porządku. Ma on jednak pewną "wadę". BLOKOUTl.C /* BLOKOUTl.C - Program demonstrujdcy użycie przycisków myszy (c) Charles Petzold, 1998 */ include LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("BlokOutl") HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CSVREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEBRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) t ! MessageBox (NULL, TEXT ("Program requires Windows NT!"), ! szAppName, MBICONERROR) ; return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Mouse Button Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, CW USEDEFAULT, Rozdział 7: Mysz NULL, NULL, hInstance, NULL) ; i ShowWindow (hwnd, kCmdShow) ; ;, UpdateWindow (hwnd) ; ; while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; ? return msg.wParam ; void DrawBoxOutline (HWND hwnd, POINT ptBeg, POINT ptEnd) Ii HDC hdc ; hdc = GetDC (hwnd) ; Ij SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULLBRUSH)) ; I.: Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; ReleaseDC (hwnd, hdc) ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) i static BOOL fBlocking, fValidBox ; static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ; HDC hdc ; PAINTSTRUCT ps ; switch (message) ( case WM_LBUTTONDOWN : ptBeg.x = ptEnd.x = LOWORD (lParam) ; ptBeg.y = ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) : i i SetCursor (LoadCursor (NULL, IDC CROSS)) ; fBlocking = TRUE ; return 0 : case WM_MOUSEMOVE : if (fBlocking) ^ SetCursor (LoadCursor (NULL, IDC CROSS)) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg, ptEnd) ; return 0 ; , 288 Część I: Podstawy (ciąg dalszy ze strony 287) case WM_LBUTTONUP : if (fBlocking) i DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptBoxBeg = ptBeg ; ptBoxEnd.x = LOWORD (lParam) ; ptBoxEnd.y = HIWORD (lParam) ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; fBlocking = FALSE ; fValidBox = TRUE ; InvalidateRect (hwnd, NULL, TRUE) ; J return 0 ; f case WM_CHAR : if (fBlocking & (wParam '\x1B')) // tzn. [Esc7 ( DrawBoxOutline (hwnd, ptBeg, ptEnd) ; r SetCursor (LoadCursor (NULL, IDC ARROW)) ; fBlocking = FALSE ; ) return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; ; if (fValidBox) i SelectObject (hdc, GetStockObject (BLACK BRUSH)) ; Rectangle (hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y) ; ) if (fBlocking) SetROP2 (hdc, R2_NOT) ; ! SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostOuitMessage (0) ; , return 0 ; return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-9. Program BLOKOLTTI Rozdział 7: Mysz 289 Program ten pokazuje coś, co może zostać zaimplementowane w dowolnym pro- gramie graficznym. Zabawę możesz zacząć od naciśnięcia lewego przycisku myszy, aby zaznaczyć wierzchołek prostokąta. Przesuń następnie mysz. Pomię- dzy zaznaczonym punktem a aktualnym położeniem kursora naszkicowany zo- stał prostokąt. Po zwolnieniu przycisku myszy prostokąt zostanie wypełniony. , Na rysunku 7-10 przedstawiony został jeden gotowy prostokąt oraz jeden w trakcie 1 tworzenia. Rysunek 7-10. Okno programu BLOKOUT1 W czym problem? Spróbuj zrobić coś takiego: naciśnij lewy przycisk myszy nad obszarem roboczym okna programu BLOKOUTI, a następnie przesuń jej kursor poza krawędź okna. Program nie otrzymuje już komunikatów WM MOUSEMOVE. A teraz zwolnij przycisk. BLOKOUT1 nie odebrał również komunikatu WMLBUTTONUP, po- nieważ kursor znalazł się poza obszarem roboczym. A teraz przenieś kursor po- nownie do wnętrza okna. Procedura okna w dalszym ciągu myśli, że przycisk jest naciśnięty. To niedobrze. Program nie wie, o co chodzi. Przechwycenie Program BLOKOUTI pokazał jedną z typowych funkcji, jednak implementujący ją kod ma wadę. Jest to jednak ten rodzaj problemu, do którego rozwiązania wymy- ślono przechwytywanie myszy. Jeżeli użytkownik przesuwa kursor myszy, to nie powinno mieć znaczenia, że znalazł on się chwilowo poza obszarem okna. Program powinien stale kontrolować ruch myszy. Przechwycenie myszy nie jest czynnością zbyt skomplikowaną. Wystarczy wy- wołać jedynie SetCapture (hwnd) ; Od tego momentu Windows będzie przesyłał wszystkie komunikaty związane z myszą do procedury tego okna, którego uchwyt przekazany został jako para- metr funkcji, niezależnie od tego, czy kursor myszy będzie znajdował się nad ob- 290 Część I: Podstawy szarem roboczym, czy nie. Parametr lParam w dalszym ciągu będzie przechowy- wał pozycję kursora we współrzędnych obszaru roboczego. Jednakże, jeżeli kur- sor myszy znajdzie się nad obszarem roboczym lub po lewej jego stronie, jedna ze współrzędnych (albo obie) będzie miała wartość ujemną. Gdy chcesz uwolnić kursor myszy, wystarczy wywołać funkcję ReleaseCapture () ; Od tego momentu przywrócone zostanie normalne przekazywanie komunikatów myszy. W 32-bitowej wersji Windows przechwytywanie myszy jest nieco bardziej restryk- cyjne niż we wcześniejszych wersjach tego systemu. Szczególnie jeżeli mysz zo- stała przechwycona, lecz jej przycisk nie jest właśnie wciśnięty, a kursor znajdzie się nad obszarem innego okna, wtedy właśnie to okno będzie otrzymywało ko- munikat. Jest to zabieg konieczny, aby nie blokować całego systemu, kiedy je- den program przechwyci mysz i jej nie zwolni. Aby uniknąć problemu, twoje programy powinny przechwytywać mysz tylko wte- dy, gdy nad ich obszarem roboczym został wciśnięty jeden z przycisków. Mysz powinna zostać zwolniona wraz ze zwolnieniem przycisku. Program BLOKOUT2 Na rysunku 7-11 przedstawiony został program BLOKOUT2 ilustrujący przechwy- tywanie myszy. BLOKOUT2.C /* BLOKOUT2.C - Program demonstrujący przyciski i przechwytywanie myszy (c) Charles Petzold, 1998 */ include LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("Blok0ut2") HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CS HREDRAW CS-VREDRAW ; wndclass.lpfnWndProc" = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITEBRUSH) wndclass.lpszMenuName = NULL ; Rozdział 7: Mysz 291 wndclass.lpszClassName = szAppName : if (!Re9isterClass (&wndclass)) ( MessageBox (NULL. TEXT ("Program requires Windows NT!"), szAppName, MBICONERROR) : return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Mouse Button & Capture Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&ms9, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wParam ; void DrawBoxOutling (HWND hwnd, POINT ptBeg, POINT ptEnd) HDC hdc : hdc = GetDC (hwnd) ; SetROP2 (hdc, R2 NOT) : SelectObject (hdc, GetStockObject (NULLBRUSH)> : Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) : ReleaseDC (hwnd, hdc) ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static BOOL fBlocking, fUalidBox : static POINT ptBeg, ptEnd, ptBoxBeg, ptBoxEnd ; HDC hdc ; PAINTSTRUCT ps ; switch (message) case WM_LBUTTONDOWN : ptBeg.x = ptEnd.x = LOWORD (lParam) ; ptBeg.y = ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg. ptEnd) : SetCapture (hwnd) : SetCursor (LoadCursor (NULL, IDC CROSS)) : fBlocking = TRUE ; 292 Część I Podstawy (ciąg dalszy ze strony 291) return 0 ; case WM_MOUSEMOVE : if (fBlocking) SetCursor (LoadCursor (NULL, IDC CROSS)) ; : DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptEnd.x = LOWORD (lParam) ; ptEnd.y = HIWORD (lParam) ; DrawBoxOutline (hwnd, ptBeg. ptEnd) ; return 0 ; case WM_LBUTTONUP : : if (fBlocking) DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ptBoxBeg = ptBeg ; ptBoxEnd.x = LOWORD (lParam) ; ptBoxEnd.y = HIWORD (lParam) ; ReleaseCapture () ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; fBlocking = FALSE ; fUalidBox = TRUE ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_CHAR : if (fBlocking & (wParam = '\x1B')) // tzn. [Esc] DrawBoxOutline (hwnd, ptBeg, ptEnd) ; ReleaseCapture () ; SetCursor (LoadCursor (NULL, IDC ARROW)) ; i fBlocking = FALSE ; ) i return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; ; f if (fValidBox) ( SelectObject (hdc, GetStockObject (BLACK BRUSH)) ; Rectangle (hdc, ptBoxBeg.x, ptBoxBeg.y, ptBoxEnd.x, ptBoxEnd.y) ; Rozdzial 7: Mysz 293 if (fBlocking) ( SetROP2 (hdc, R2_NOT) ; SelectObject (hdc, GetStockObject (NULL_BRUSH)) ; Rectangle (hdc, ptBeg.x, ptBeg.y, ptEnd.x, ptEnd.y) EndPaint (hwnd, &ps) ; return 0 : case WM DESTROY : PostOuitMessage (0) ; return 0 ; 1 return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-11. Program BLOKOUT2 Program BLOKOUT2 różni się od BLOKOUT1 zaledwie kilkoma liniami kodu: wywołaniem SetCapture w ramach obsługi komunikatu WM LBUTTONDOWN oraz wywołaniami ReleaseCapture w ramach obsługi WM LBUTTONUP. Możesz teraz wypróbować wprowadzone zmiany. Zmniejsz okno tak, aby nie zajmowa- ło całego ekranu i rozpocznij kreślenie prostokąta w obszarze roboczym, a na- stępnie przemieść kursor poza ten obszar, obojętne do góry czy do dołu, i zwol- nij przycisk myszy. Program będzie teraz miał wszelkie niezbędne informacje do narysowania poprawnego prostokąta. Po prostu powiększ okno i sprawdź. Przechwytywanie myszy nie jest zarezerwowane wyłącznie dla jakichś dziwnych aplikacji. Powinieneś to robić zawsze, gdy musisz śledzić komunikaty WM MO- USEMOVE po naciśnięciu przycisku myszy w obszarze roboczym. Przechwyty- wanie możesz zakończyć dopiero wtedy, gdy przycisk zostanie zwolniony. Nie dość, że dzięki temu pisane przez ciebie programy będą prostsze, to jeszcze speł- nią wymagania użytkownika. Kółko myszy Pewnego dnia moja matka, nieświadomie parafrazując Emersona, powiedziała: "Wymyśl lepszą pułapkę na myszy, a cały świat wydepcze ścieżkę do twoich drzwi". Oczywiście, w naszych czasach większe znaczenie miałoby wynalezie- nie lepszej myszy. Funkcja Microsoft IntelliMouse, będąca rozszerzeniem możliwości zwykłej my- szy, zmaterializowała się w postaci niewielkiego kółka umieszczonego pomiędzy dwoma przyciskami. Jeżeli poruszysz je, zadziała on w taki sam sposób, jak środ- kowy przycisk myszy. Jeżeli natomiast obrócisz je palcem wskazującym, wyge- nerowany zostanie specjalny komunikat WMMOUSEWHEEL. Programy, które potrafią go wykorzystać, w odpowiedzi przewijają lub powiększają wyświetla- ny przez siebie dokument. Na pierwszy rzut oka ta innowacja może się wyda- wać rukomu niepotrzebną sztuczką, muszę jednak przyznać, że bardzo szybko przyzwyczaiłem się do nowego sposobu przewijania zawartości dokumentu w programie Microsoft Word lub w Internet Explorer. 294 Część I: Podstawy Nie zamierzam tu nawet przymierzać się do dyskusji o wszelkich możliwych zale- tach kółka. Zamiast tego po prostu pokażę ci, w jaki sposób możesz je wykorzystać do przewijania danych w obszarze roboczym w takim programie jak SYSMETS4. Na rysunku 7-12 przedstawiona została ostateczna wersja tego programu. SYSMETS.C /* SYSMETS.C - Ostateczna wersja programu wyświetlającego wymiary elementów graficznych (c) Charles Petzold, 1998 */ include include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[] = TEXT ("SysMets") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass : wndclass.style = CS_HREDRAW CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE-BRUSH) wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) i MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics"), WS_OVERLAPPEDWINDOW WS_VSCROLL WS HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) i TranslateMessage (&msg) ; DispatchMessage (&msg) ; Rozdzial 7: Mysz 295 1 return msg.wParam ; 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxChar, cxCaps, cyChar. cxClient, cyClient, iMaxWidth ; static int iDeltaPerLine, iAccumDelta ; // obslugd kólka myszy HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; ULONG ulScrollLines ; // obsluga kólka myszy switch (message) ( case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeadin9 ; ReleaseDC (hwnd, hdc) ; // Zapamiętaj szerokość trzech kolumn iMaxWidth = 40 * cxChar + 22 * cxCaps ; // Kontynuacja - pobranie informacji o kółku CdSe WM_SETTINGCHANGE: SystemParametersInfo (SPI GETWHEELSCROLLLINES, 0, &ulScrollLines, 0) ; i // ulScrollLines zwykle ma wartość 3 lub 0 (gdy bez przewijania) // WHEEL DELTA jest równe 120, dlatego iDeltaPerLine bedzie wynosilo 40 if (ulScrollLines) iDeltaPerLine = WHEEL DELTA / ulScrollLines ; else iDeltaPerLine = 0 ; return 0 ; case WM_SIZE: j cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; ' ,. // Ustaw zakres pionowego paska przewijania oraz // wielkość strony si.cbSize = sizeof (si> ; si.fMask = SIF RANGE SIFPAGE ; si.nMin = 0 : 4, si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; 296 Część I: Podstawy (ciąg dalszy ze strony 295) SetScrollInfo (hwnd, SB VERT, &si, TRUE) ; // Ustaw zakres poziomego paska przewijania oraz wielkość strony si.cbSize = sizeof (si) ; si.fMask = SIF RANGE SIFPAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Pobierz wszystkie dane o przesunięciu w pionie si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB VERT, &si) ; // Zapamiętaj polożenia dla późniejszego porównania iVertPos = si.nPos ; t switch (LOWORD (wParam)) ( case SB_TOP: ' si.nPos = si.nMin ; break ; ,. case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: ` f si.nPos -= 1 ; break ; case SB_LINEDOWN: ' si nPos += 1 ; i break ; i i case SB_PAGEUP: " si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si nPos = si.nTrackPos ; break ; default: break ; J Rozdział7: Mysz // Ustaw położenie, a następnie przywróć je. Na skutek ustawień // Windows może to nie być taka sama wartość, jaka została ustawiona. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB VERT. &si) ; // Jeżeli położenie uległo zmianie, przesuń zawartość i odśwież if (si.nPos != iVertPos) i ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; ) return 0 ; case WM HSCROLL: // Pobierz wszystkie dane o przesunięciu w poziomie ; si.cbSize = sizeof (si) ; si.fMask = SIF ALL ; i, // Zapamiętaj położenia, aby później porównać GetScrollInfo (hwnd, SB HORZ, &si) ; "' iHorzPos = si.nPos ; switch (LOWORD (wParam)) case SB_LINELEFT: si nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += i : break ; i case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SBPAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si nPos = si.nTrackPos ; break ; default: break ; l // Ustaw położenie. a następnie przywróć je. Na skutek ustawień // Windows może to nie być taka sama wartość, jaka została ustawiona. si.fMask = SIF_POS : SetScrollInfo (hwnd, SB HORZ. &si, TRUE) ; GetScrollInfo (hwnd, SB HORZ, &si) ; I 298 Część I: Podstawy (ciąg dalszy ze strony 297) i // Jeżeli polożenie uleglo zmianie, przesuń zawartość okna I if (si.nPos != iHorzPos) ( ScrollWindow (hwnd, cxChar * (iNorzPos - si.nPos), 0, NULL, NULL) ; ) return 0 ; case WM KEYDOWN : switch (wParam) t case VK_HOME : SendMessage (hwnd, WM VSCROLL, SB TOP, 0) ; break ; case VK_END : SendMessage (hwnd, WM VSCROLL, SB BOTTOM, 0) ; break ; case VK_PRIOR : SendMessage (hwnd, WM VSCROLL, SBPAGEUP, 0) ; break ; case VK_NEXT : SendMessage (hwnd, WM VSCROLL, SBPAGEDOWN, 0) ; break ; case VK_UP : SendMessage (hwnd, WM VSCROLL, SBLINEUP, 0) ; break ; case VK_DOWN : SendMessage (hwnd, WM VSCROLL, SBLINEDOWN, 0) ; break ; case VK_LEFT : SendMessage (hwnd, WM HSCROLL, SBPAGEUP, 0) ; break ; case VK_RIGHT : SendMessage (hwnd, WM HSCROLL, SBPAGEDOWN, 0) : break ; J return 0 ; case WM_MOUSEWHEEL: if (iDeltaPerLine = 0) break ; iAccumDelta += (short) HIWORD (wParam) ; // 120 lub -120 while (iAccumDelta >= iDeltaPerLine) f SendMessage (hwnd, WM_VSCROLL, SBLINEUP, 0) ; iAccumDelta -= iDeltaPerLine ; Rozdział l: Mysz 299 ) while (iAccumDelta <= -iDeltaPerLine) ( SendMessage (hwnd, WM_VSCROLL, SBLINEDOWN, 0) ; iAccumDelta += iDeltaPerLine ; ) return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Pobierz położenie pionowego paska przewijania si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SBVERT, &si) ; iVertPos = si.nPos ; // Pobierz położenie poziomego paska przewijania GetScrollInfo (hwnd, SB HORZ. &si) ; iHorzPos = si.nPos ; // Określ ograniczenia rysowania iPaintBe9 = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min (NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) ( x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut (hdc, x, y, I sysmetricsCi].szLabel, lstrlen (sysmetricsCiJ.szLabel)) ; ' TextOut (hdc, x + 22 * cxCaps, y, sysmetricsCiJ.szDesc, lstrlen (sysmetricsCi].szDesc)) ; SetTextAlign (hdc, TARIGHT T TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TALEFT T TOP) ; EndPaint (hwnd, &ps) : return 0 ; case WM DESTROY : PostOuitMessage (0) ; 300 Część I: Podstawy return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 7-12. Program SYSMETS Obracanie kółka wymusza na systemie wysłanie komunikatu WM-MOUSEWHEEL do tego okna, które aktualnie ma fokus (a nie do okna, nad którym znajduje się kursor myszy). Jak zwykle, parametr IParam przechowuje informację o położe- niu kursora myszy. Jednak współrzędne te podane są względem lewego górnego rogu ekranu, a nie obszaru roboczego. Również jak zwykle młodsze słowo wPa- ram przechowuje informację o stanie przycisków myszy oraz klawiszy [Shift] i [Ctrl] . Jednak w starszym słowie parametru wParam pojawiła się nowa informacja. Jest to wartość "delta", która obecnie przyjmuje jedną z dwóch wartości: 120 lub -120 za- leżnie od tego, czy kółko obracane jest do przodu (to znaczy w kierunku kabla myszy), czy do tyłu. Wartości 120 i -120 oznaczają, że dokument jest przewijany odpowiednio o trzy linie do góry lub do dołu. Być może kolejne wersje będą za- pewniały dokładniejsze stopniowanie, a co za tym idzie wartość delta wynoszą- ca (na przykład) 40 i -40 będzie oznaczała przewijanie o jeden wiersz. Aby program był wystarczająco ogólny, w ramach obsługi komunikatów 4VM-CREATE oraz WM SETTINGCHANGE wywoływana jest funkcja System- Parameterslnfo z parametrem SI GETWHEELSCROLLLINES. Otrzymana w ten sposób wartość wskazuje, o ile linii należy przewinąć dokument dla delty wyno- szącej WHEEL DELTA (wartość ta została zdefiniowana w pliku nagłówkowym WINUSER.H). WHEEL DELTA ma wartość 120, a funkcja SystemParameterslnfo domyślnie zwróci 3. Tak więc delta odpowiadająca przewinięciu dokumentu o jed- ną linię wynosi 40. Wartość ta przechowywana jest w iDeltaPerLine. W ramach obsługi komunikatu WM MOUSEWHEEL, program SYSMETS doda- je wartość delty do zmiennej statycznej iAccumDelta. Następnie, jeżeli wartość iAccumDelta jest większa lub równa iDeltaPerLine (albo mniejsza lub równa -iDel- taPerLine), wygenerowany zostaje komunikat WM VSCROLL z parametrem SB LINEUP lub SB LINEDOWN. Po każdym wysłaniu tego komunikatu, iAc- cumDelta jest zmniejszana (albo zwiększana) o iDeltaPerLine. Co dalej? W zasadzie pozostało nam jeszcze omówienie tworzenia i dostosowywania do swoich potrzeb kursorów myszy. Zajmę się tym, a także wstępem do zasobów Windows w rozdziale 10.