Rozdział 8 Ze a r W systemie Microsoft Windows zegar (ang. timer) jest urządzeniem wejściowym, okresowo informującym aplikację o upłynięciu zadanego przedziahz czasu. Za- danie programu jest proste: musi poinformować zegar, jaki okres ma odmierzać. W odpowiedzi program będzie otrzymywał komunikaty WM TIMER sygnalizu- jące zakończenie każdego okresu. Na pierwszy rzut oka może wydawać się, że zegar jest urządzeniem wejściowym o mniejszym znaczeniu, niż klawiatura czy mysz. W większości aplikacji tak jest w istocie. Istnieją jednak i takie aplikacje, które bardzo intensywnie korzystają z zegara. I nie są to tylko aplikacje związane z wyświetlaniem aktualnego czasu, jak na przykład zegar wyświetlany na pasku zadań czy też dwa programy, które napiszemy w tym rozdziale. Poniżej przedstawione zostały inne zegary wyko- rzystywane przez Windows, przy czym czgść z nich nie jest zbyt typowa. ł Wielozadaniowość. Chociaż Windows 98 jest systemem wielozadaniowym z wy- właszczaniem (ang. preemptive multitasking environment), w pewnych sytuacjach program powinien zwrócić sterowanie do systemu po obsłużeniu komunika- tu tak szybko, jak to tylko możliwe. Jeżeli program musi wykonać bardzo zło- żone i długotrwałe obliczenia, może podzielić zadanie na kilka mniejszych eta- pów i wykonywać je kolejno po odebraniu komunikatu WM TIMER. (Na ten temat będę miał znacznie więcej do powiedzenia w rozdziale 20). ł Sterowanie odświeżaniem raportów. Program może poshxżyć się zegarem do od- świeżania wyświetlanych, stale zmieniających się danych, na przykład infor- macji o aktualnym stanie zasobów systemu lub postępach jakiegoś zadania. ł Implementacja funkcji "automatycznego zapisu". Zegar może być wykorzystany do okresowego wymuszenia zapisu w pliku dyskowym rezultatów pracy użyt- kownika. ł Zakończenie pracy demonstracyjnej wersji programu. Pewne demonstracyjne wersje programów zostały zaprojektowane w ten sposób, aby zakończyć swoją pra- cę, powiedzmy, po 30 minutach od jej rozpoczęcia. W tym wypadku zegar systemowy wykorzystywany jest do wygenerowania impulsu kończącego działanie. ł Równomierne przemieszczanie się. W wielu grach konieczne jest, aby obiekty gra- ficzne poruszały się z pewną określoną prędkością. Tymczasem może okazać się, że jest ona zależna od szybkości maszyny, na której program został uru- chomiony. Aby zabezpieczyć się przed podobnymi problemami, wykorzysty- wany jest zegar. 302 Część I: Podstawy ł Multimedia. Programy odtwarzające płyty CD lub pliki dźwiękowe często mogą działać w tle. Program może poshxżyć się zegarem do okresowego sprawdza- nia, jaka część utworu została już odegrana, i wyświetlania tych informacji na ekranie. Inną metodą myślenia o zegarze jest traktowanie go jako gwarancji, że kiedyś w przyszłości program ponownie otrzyma sterowanie. Program zwykle nie wie, kiedy nadejdzie komunikat. Podstawowe informacje o zegarze Przydzielenie zegara aplikacji następuje po wywołaniu funkcji SetTimer. Jako jej parametr podaje się liczbę całkowitą bez znaku, której zadaniem jest określenie, jak dhzgi odcinek czasu ma być odmierzany. Może ona przyjmować wartości (teo- retycznie) od 1 milisekundy do 4 294 967 295 milisekund (czyli blisko 50 dni). Wartość ta określa więc częstotliwość, z jaką do twego programu nadchodzić będą komunikaty WMTIMER. Podanie na przykład odcinka czasu równego 1000 mi- lisekund spowoduje, że WM T'IMER będzie wysyłany co 1 sekundę. Gdy program nie potrzebuje już zegara, powinien wywołać funkcję KillTimer. Mo- żesz oczywiście zaprogramować zegar, który wyśle tylko jeden impuls: wystar- czy po prostu wywołać KillTimer w ramach obsługi komurukatu WM TIMER. Wywołanie KillTimer powoduje również, że z kolejki usunięte zostaną wszystkie nieobsłużone komunikaty WM TIMER. Dlatego też masz pewność, że po jej wywołaniu aplikacja nie otrzyma już żadnego komunikatu WM TIMER. System i zegar Zegar dostępny w Windows jest stosunkowo prostym rozszerzeniem możliwo- ści, które ma każdy komputer PC dzięki obecności na jego płycie głównej pew- nych układów elektronicznych. W dawnych czasach, zanim na świecie pojawił się system Windows, programiści pisząc swoje programy przeznaczone dla MS- DOS-a mogli zaimplementować zegar, przechwytując "przerwanie zegarowe" (ang. timer tick). Generowane było ono co 54,925 milisekundy, czyli około 18,2 razy na sekundę. Działo się tak, ponieważ zegar oryginalnego IBM PC o często- tliwości 4,772720 MHz dzielony był przez 2'8. Aplikacje Windows nie przechwytują przerwań BIOS-u. Za ich przechwytywa- nie odpowiedzialny jest system Windows. Dla każdego z utworzonych zegarów Windows przechowuje pewien licznik, którego wartość jest zmniejszana za każ- dym razem, gdy przechwycone zostanie prżerwanie zegarowe. Gdy zawartość licznika osiągnie wartość 0, generowany jest komunikat WM TIMER, wstawia- ny do kolejki odpowiedniej aplikacji, a następnie przywracana jest początkowa wartość licznika. Ponieważ aplikacja Windows otrzymuje komunikaty WMTIMER za pośrednic- twem zwykłej kolejki, nie musisz się martwić, że program zostanie niespodzie- wanie przerwany przez jego pojawienie się. Pod tym względem zegar podobny jest do klawiatury i myszy: sterownik obsługuje asynchroniczne przerwania sprzę- towe, a następnie przekształca je w odpowiednio zbudowane komunikaty. Rozdział 8: Zegar 303 W Windows 98 zegar ma taką samą rozdzielczość, wynoszącą 55 milisekund, co wykorzystywane przez niego przerwanie sprzętowe zegara PC. Natomiast w Win- dows NT rozdzielczość zegara wynosi około 10 milisekund. Aplikacje Windows nie mogą otrzymywać komunikatu WMTIMER częściej, niż wynika to z rozdzielczości zegara. W Windows 98 jest to około 18,2 razy na sekun- dę, natomiast w Windows NT - 100 razy na sekundę. Dlatego też podana przez ciebie wartość parametru funkcji SetTimer zaokrąglana jest przez system do naj- bliższej całkowitej liczby impulsów zegara. Na przykład, podanie czasu 1000 mili- sekund spowoduje jego podzielenie przez 54,925, w wyniku czego otrzyma się 18,207 impulsów zegara. Wartość ta zostanie zaokrąglona w dół do 18, co spowoduje, że odmierzany będzie czas 989 milisekund. Jeżeli natomiast podany zostanie okres krótszy niż 55 milisekund, komunikat generowany jest dla każdego impulsu. Komunikaty zegarowe nie są asynchroniczne Ponieważ zegar działa w oparciu o przerwanie sprzętowe, część programistów sądzi, że ich aplikacje mogą zostać przerwane w dowolnym momencie w celu obsługi komunikatu WM TTMER. Jednakże komunikat WMTIMER nie jest asynchroniczny. Za każdym razem , gdy się pojawi, zostaje umieszczony w zwykłej kolejce komunikatów i dostarczony do aplikacji razem z innymi. Dlatego też, jeżeli w wywołaniu SetTimer podasz okres wynoszący 1000 milisekund, program nie ma gwarancji, że komunikaty pojawiać się będą co 1 sekundę lub nawet (jak wspomniałem wcześniej) co 989 milisekund. Jeżeli aplikacja będzie zajęta dhxżej niż jedną sekundę, w tym czasie nie otrzyma żadnego komunikatu WM TIMER. Będziesz mógł się o tym przeko- nać, korzystając z programów przedstawionych w tym rozdziale. W rzeczy sa- mej, system Windows traktuje komunikat WM-TIMER podobnie jak WM-PAINT. Oba mają niski priorytet i program odbierze je dopiero wtedy, gdy jego kolejka komunikatów nie będzie zawierała żadnych innych. Komunikat WMTIMER jest podobny do WM-PAINT pod jeszcze innym wzglę- dem. System Windows nie zawraca sobie głowy umieszczaniem w kolejce wielu komunikatów WMTIMER. Zamiast tego są one łączone w jeden. Dlatego też aplikacja nigdy nie odbierze dwóch kolejnych komunikatów WMTIMER nawet wtedy, jeżeli pojawią się one w bardzo krótkim okresie czasu. Co gorsza, aplika- cja nie jest w stanie określić, ile komunikatów zostało zgubionych na skutek ta- kiego traktowania. Z tego powodu programy zegarowe nie mogą wyznaczać czasu na podstawie odebranych przez siebie komunikatów WM-TIMER. Komunikaty te mogą jedy- nie informować, że wyświetlany czas powinien zostać zmieniony. W tym rozdziale napiszemy dwie aplikacje zegarowe, które będą odświeżane co sekundę i dopie- ro wtedy dowiesz się, w jaki sposób zostało to zrealizowane. Dla jasności, o zegarze będę mówił w taki sposób, jakby "komunikat WM-TIMER był co sekundę". Powinieneś jednak pamiętać, że komunikat to nie przerwanie, może pojawić się trochę później. 304 CzęśĆ l. Podstawy Zegar: trzy metody wykorzystania Jeżeli pisany przez ciebie program będzie korzystał z zegara przez cały czas swojej pracy, funkcję SetTimer prawdopodobnie wywołasz w ramach obshzgi komuni- katu WM CREATE, a KillTimer - w ramach WM DESTROY. Zegarem możesz posługiwać się na jeden z trzech sposobów, w zależności od argumentów SetTi- mer. Metoda pierwsza Metoda ta, najłatwiejsza, powoduje, że Windows wysyła komunikat WM TIMER do zwykłej procedury okna aplikacji. W tym przypadku wywołanie SetTimer wy- gląda następująco: SetTimer (hwnd, 1, uiMsecInterval, NULL) ; Pierwszy parametr to uchwyt okna, którego procedura będzie otrzymywała ko- munikaty WM TIMER. Drugi parametr to identyfikator zegara. Musi on być liczbą różną od zera. W tym prżykładzie arbitralnie zdecydowałem, że będzie on rów- ny jeden. Trzecim parametrem funkcji jest 32 bitowa liczba całkowita bez znaku, która określa w milisekundach długość odmierzanego odcinka czasu. Jeżeli po- dasz 60 000, komunikat WMTIMER będzie wysyłany do aplikacji co minutę. W dowolnej chwili, nawet w trakcie przetwarzania komunikatu WM T'IMER, możesz zakończyć wysyłanie komunikatów. Wystarczy wywołać funkcję KillTimer (hwnd, 1) ; Drugi argument to ten sam identyfikator zegara, który podany został w wywo- łaniu funkcji SetTimer. Powszechnie uważa się, że dobrym zwyczajem jest kaso- wanie wszystkich uruchomionych zegarów w ramach obsługi komunikatu WM DESTROY. Gdy procedura okna odbierze komunikat WM TIMER, towarzyszący mu para- metr wParam przechowuje identyfikator zegara (w naszym przypadku będzie to 1), a lParam - 0. Jeżeli musisz wykorzystać więcej niż jeden zegar, nadaj każdemu z nich odrębny identyfikator. Dzięki temu wartość wParam pozwoli ci rozróżnić komumikaty WM-TiMER nadchodzące do procedury okna. Jeżeli chcesz, aby twój program był łatwiejszy w "czytaniu", definiując poszczególne identyfikatory, mo- żesz poshzżyć się wyrażeniem #define: lldefine TIMER SEC 1 lldefine TIMER MIN 2 Oba zegary możesz teraz ustawić w następujący sposób: SetTimer (hwnd, TIMER SEC, 1000, NULL) ; SetTimer (hwnd, TIMER MIN, 60000, NULL) ; Natomiast obsługa WM TIMER w funkcji okna będzie wyglądała jakoś tak: case WM_TIMER: switch (wParam) f case TIMERSEC: Cprzetwarzanie raz na sekundę) break ; Rflzdzisł 8: Zer 305 case TIMER_MIN: [przetwarzanie raz na minutęJ break ; ) return 0 ; Jeżeli chcesz, aby istniejący zegar zaczął odmierzać inne odcinki czasu, możesz po prostu jeszcze raz wywołać SetTimer i podać inną wartość czasu. Możesz to na przykład wykorzystać w programie zegara, który dysponuje opcją, pozwalająca na ukrywanie bądź wyświetlanie sekund. Wystarczy przełączać okres pomiędzy 1000 a 60 000 milisekund. Na rysunku 8-1 przedstawiony został program posługujący się zegarem. Program ten, noszący nazwę BEEPERl, tak ustawia zegar, aby odmierzał jednosekundowe odcinki czasu. Po odebraniu komunikatu WMTIllfER zmienia on kolor obszaru ro- boczego na niebieski lub czerwony, a następnie, za pomocą funkcji MessageBeep, ge- neruje sygnał dźwiękowy. (Chociaż MessageBeep często jest wykorzystywana łącznie z MessageBox, tak naprawdę jest funkcją, która może być stosowana w dowolnej sy- tuacji. Jeżeli komputer wyposażony jest w kartę dźwiękową, do funkcji możesz prze- kazać jedną z wartości MBICON, które zwykle przekazywane są do MessageBox. Dzięki temu mogą być generowane różne sygnały dźwiękowe ustawiane przez użyt- kownika za pomocą apletu Dźwięki znajdującego się w Panelu sterowania). BEEPERI ustawia zegar w procedurze okna w trakcie obsługi komunikatu WM CREATE. W ramach obsługi WMTIMER, program wywołuje MessageBeep, zmienia na przeciwną wartość Bagi FlipFlop, a następnie unieważnia cały ob- szar roboczy, dzięki czemu wygenerowany zostaje komunikat WMPAINT. Z kolei w trakcie jego obsługi BEEPER1 za pomocą funkcji GetClientRect pobiera struktu- rę RECT przechowującą rozmiar okna. Na koniec dzięki wywołaniu FillRect, ob- szar roboczy zostaje wypełniony odpowiednim kolorem. 8E^P^RI.C /* BEEPERl.C - Program demonstrujący zegar, wersja 1 (c) Charles Petzold, 1998 */ ipinclude idefine ID TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppNameC] = TEXT ("Beeperl") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CS VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; 306 Część I: Podstawy (ciąg dalszy ze strony 305) wndclass.cbWndExtra = 0 : wndclass.hInstance = hInstance : wndclass.hIcon = LoadIcon (NULL, IDI PPLICATION) : wndclass:hCursor = LoadCrsor (NULL, IDC ARROW) : wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE BRUSH) : wndclass.lpszMenuName = NULL : wndclass.lpszClassName = szAppName : if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBICONERROR) : return 0 : ) hwnd = CreateWindow (szAppName, TEXT ("Beeperl Timer 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) : ) return msg.wParam : 1 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ! static BOOL fFlipFlop = FALSE ; HBRUSH hBrush : HDC hdc : PAINTSTRUCT ps : RECT rc : , switch (message) ( case WM CREATE: SetTimer (hwnd, ID TIMER, 1000, NULL) : return 0 : case WM TIMER : MessageBeep (-1) : fFlipFlop = !fFlipFlop InvalidateRect (hwnd, NULL, FALSE) : return 0 : [ case WMPAINT : hdc = BeginPaint (hwnd, &ps) : GetClientRect (hwnd, &rc) : Rozdział 8: Zegar 307 hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ; FillRect (hdc, &rc, hBrush) ; EndPaint (hwnd, &ps) Delete0bject (hBrush) return 0 ; case WM_DESTROY : KillTimer (hwnd, ID_TIMER) ; PostOuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wParam. lParam) ; Rysunek 8-1. Program BEEPER1 Ponieważ BEEPERI głośno obwieszcza odebranie każdego komunikatu WMTI- MER, ciekawym doświadczeniem może okazać się wykonanie w trakcie jego pracy jakiejś innej czynności. Z pewnością zorientujesz się, że komunikaty nie są odbie- rane zbyt regularnie. Oto pewien odkrywczy ekspeyment, który możesz przeprowadzić: najpierw uruchom aplet Ekran znajdujący się w Panelu sterowania i kliknij kartę Efekty. Upewnij się, że pole wyboru Pokazuj zawartość okna podczas przeciągania nie jest zaznaczone. Spróbuj teraz zmieruć wielkość okna programu BEEPERl. Spo- woduje to, że program wejdzie w modalną pętlę komurukatów (ang. modal mes- sage loop). Polega ona na tym, że system Windows "przechwytuje" wszystkie komunikaty przez swoją wewnętrzną pętlę i uniemożliwia przerwanie zmiany wielkości okna lub jego przemieszczania przez jakikolwiek komunikat. Większość z nich jest po prostu usuwana z kolejki. Dlatego też BEEPER1 w trakcie tej opera- cji przestaje generować sygnały dźwiękowe. Po zakończeniu przesuwania zauwa- żysz, że BEEPER1 nie otrzymał tych wszystkich komunikatów, które nadeszły w trakcie operacji. Jeżeli z kolei pole Pokazuj zawartość okna podczas przeciągania jest zaznaczone, modalna pętla komunikatów Windows usiłuje przekazać do procedury okna przy- najmniej część komunikatów, które w przeciwnym razie zostałyby utracone. Cza- sem działa to poprawnie, a czasem nie. Metoda druga Pierwsza metoda ustawiania zegara powoduje, że komunikat WM TIMER wy- syłany jest do procedury okna. Korzystając natomiast z drugiej metody, możesz sprawić, że system Windows wyśle go do wybranej innej funkcji twego progra- mu. Funkcja, która odbiera komunikaty zegara, określana jest jako call-back. Jest ona po prostu w twoim programie wywoływana przez Windows. Wystarczy, abyś podał systemowi jej adres. Brzmi to znajomo: przecież funkcja okna jest rodza- jem funkcji call-back! Jej adres podajesz systemowi w trakcie rejestracji klasy okna, dlatego jest później wywoływana za każdym razem, gdy powinna obshzżyć jakiś komunikat. 308 Część 1: Po"stawy SetTimer nie jest jedyną funkcją Windows, która korzysta z tego sposobu wywo- ływania. Z podobnej metody korzystają również funkcje CreateDialog oraz Dia- logBox (przedstawię je w rozdziale 11) do przetwarzania komunikatów dociera- jących do okna dialogowego. Wiele funkcji Windows (EnumChildWindow, Enum- Fonts, EnumObjects, EnumProps oraz EnumWindow) przekazuje pobierane przez siebie informacje do funkcji call-back. W ten sam sposób działa również kilka in- nych mniej znanych funkcji: GrayString, LineDDA oraz WindowHookEx. Podobnie jak funkcja okna, również funkcja call-back musi zostać zadeklarowa- ! na jako CALLBACK, ponieważ jest wywoływana przez Windows spoza przestrze- ru kodu programu. Parametry funkcji oraz zwracana przez nią wartość zależą od jej przeznaczenia. W przypadku funkcji wywoływanej przez zegar, parametry są ! takie same jak funkcji okna, chociaż zdefiniowane w nieco inny sposób. Nie po- winna ona również zwracać żadnej wartości. Nazwijmy naszą funkcję TimerProc (możesz wybrać dowolną inną nazwę, która nie koliduje z już istniejącą).Będzie ona obsługiwała wyłącznie komunikaty WM_TIMER: VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime) Cobsiuga komunikatu WM TIMER7 Parametr hwnd ma taką samą wartość jak hwnd przekazywane do SetTimer. Po- nieważ system Windows będzie przekazywał do TimerProc jedynie komunikaty WM TIMER, parametr message zawsze będzie miał wartość WM TIMER. iTimer- ID jest identyfikatorem zegara, natomiast dwTime jest wartością kompatybilną z wartością zwracaną przez funkcję GetTickCount. Jest to liczba milisekund, które upłynęły od czasu uruchomienia systemu. Jak widzieliśmy, pierwsza metoda uruchamiania zegara wygląda następująco: SetTimer (hwnd, iTimerID, iMsecInterval, NULL) ; Jeżeli do obsługi komunikatów WM TTMER posługujesz się funkcją call-back, czwartym argumentem SetTimer jest jej adres: SetTimer (hwnd, iTimerID, iMsecInterval, TimerProc) ; Przyjrzyjmy się teraz przykładowi, abyś mógł się przekonać, jak to wszystko wygląda razem. Przedstawiony na rysunku 8-2 program BEEPER2 pod wzglę- dem funkcjonalnym niczym nie różni się od programu BEEPERl. Jedynie kornu- nikat WMTIMER trafia do TimerProc, a nie do WndProc. Zwróć uwagę, że Timer- Proc jest zadeklarowana na samym początku programu, razem z WndProc. BEEPERI.C /* BEEPERl.C - Pro9ram demonstrujdcy zegar, wersja 2 (c) Charles Petzold, 1998 */ ilinclude ildefine ID TIMER Rozdziai 8: Zegar 309 i LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; VOID CALLBACK TimerProc (HWND, UINT, UINT, DWORD ) ; I int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppNameC] = ("Beeper2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CS UREDRAW ; 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 ; i. if (!RegisterClass (&wndclass)) ( MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MBICONERROR) ; return 0 ; hwnd = CreateWindow (szAppName, TEXT ("Beeper2 Timer 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 (&ms9) ; DispatchMessage (&msg) ; ;.; ) return msg.wParam ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( , switch (message) f case WM_CREATE: SetTimer (hwnd, ID TIMER, 1000, TimerProc) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; PostOuitMessage (0) ; return 0 ; ) 310 Część I: Podstawy (ciąg dalszy ze strony 309) return DefWindowProc (hwnd, message, wParam, lParam) ; VOID CALLBACK TimerProc (HWND hwnd, UINT message, UINT iTimerID, DWORD dwTime) ( static BOOL fFlipFlop = FALSE ; HBRUSH hBrush ; HDC hdc : RECT rc ; MessageBeep (-1) : fFlipFlop = !fFlipFlop ; GetClientRect (hwnd, &rc) ; hdc = GetDC (hwnd) : hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) : FillRect (h-dc, &rc, hBrush) : ReleaseDC (hwnd, hdc) ; Delete0bject (hBrush) : Rysunek -2. Program BEEPER2 Metoda trzecia Trzecia metoda ustawiania zegara podobna jest do drugiej, jednak w tym przy- padku parametr hwnd przekazywany do funkcji SetTimer ma wartość NULL, a dru- gi parametr (identyfikator zegara) jest ignorowany. Powoduje to, że funkcja zwraca identyfikator zegara. iTimerId = SetTimer (NULL, 0, wMsecInterval, TimerProc) : Zwracana przez funkcję SetTimer wartość iTimerId będzie równa 0 tylko w tych nielicznych przypadkach, gdy zegar nie jest dostępny. Pierwszym parametrem funkcji KilITimer (uchwyt okna) również powinien być NULL. Musisz wykorzystać zwrócony przez SetTimer identyfikator zegara: KillTimer (NULL, iTimerID) ; Przekazywany do funkcji TimerProc parametr hwnd również będzie miał wartość NULL. Ten sposób ustawiania zegara nie jest zbyt często stosowany. Możesz po- służyć się nim, gdy w swoim programie musisz uruchomić wiele zegarów odmie- rzających różne czasy, a nie chcesz śledzić przydzielanych im identyfikatorów. Teraz, skoro już wiesz, w jaki sposób możesz ustawiać zegar, nadeszła pora, aby napisać kilka użytecznych aplikacji. Wykorzystanie zegara Najczęściej zegar wykorzystywany jest w aplikacjach wyświetlających aktualny czas. Napiszemy dwa takie programy; w oknie pierwszego programu czas bę- dzie przedstawiany cyfrowo, w oknie drugiego natomiast - analogowo. Rozdział 8: Zegar 311 i Zegar cyfrowy Program DIGCLOCK przedstawiony na rysunku 8-3 wyświetla aktualną godzi- ; nę, symulując wyświetlacze siedmiosegmentowe. DIGCLOCK. c i /* i DIGCLOCK.c - Zegar cyfrowy (c) Charles Petzold, 1998 */ 1 ; include j 4define ID TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; 1 i int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( I, i:. . a static TCHAR szAppNameC] = TEXT ("DigClock") ; HWND hwnd ; MSG msg ; ,.,, i:. WNDCLASS wndclass ; ,,., wndclass.style = CS-HREDRAW CS VREDRAW ; ' i wndclass.lpfnWndProc = WndProc ; ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; i 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) ; ) hwnd = CreateWindow (szAppName, TEXT ("Digital Clock"), 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) ; 312 Czś t: P"dawy (ciąg datszy ze strony 322) 1 return msg.wParam ; 1 void DisplayDigit (HDC hdc, int iNumber) ( static 800L fSevenSegment [107[77 = ( 1, I, 1, 0, 1. 1, 1, // 0 0, 0, 1, 0, 0, 1, 0, // 1 1, 0, 1, 1. 1, 0. 1. /l 2 1, 0, I, 1, 0, 1, 1, // 3 0, 1, 1. 1, 0, l. 0, // 4 1, 1. 0, 1, 0. 1, 1, l/ 5 1, 1, 0, 1, 1, 1, 1, // 6 1, 0, 1, 0, 0, 1, 0. /! 7 1, l, 1, 1, 1. 1, 1, // 8 1, 1, 1, i, 0, 1, i ) ; /l 9 static POINT ptSegment C77C67 = ( 7, 6, 11, 2, 31, 2, 35, 6, 31. 10, il. 10, 6, 7, 10, 11, 10. 31, 6, 35. 2. 31, 2, 11, 36, 7, 40, il, 40, 31, 36, 35, 32. 31, 32, il, 7. 36, 11, 32, 31. 32,. 35. 36, 31, 40, 11, 40, 6. 37, 10, 41, 10, 61, 6, 65, 2, 61. 2, 4I, 36, 37, 40, 41, 40, 61, 36, 65, 32, 61, 32, 41, 7, 66. 11, 62, 31, 62, 35, 66, 31. 70. 11, 70 ) ; int iSeg ; for (iSeg = 0 ; iSeg < 7 ;. iSeg++) if (fSevenSegment CiNumber7CiSeg]) Polygon (hdc, ptSegment [iSeg7, 6) ; ) void DisplayTwoDigits (HDC hdc, int iNumber, BOOL fSuppress) ( if (!fSuppress (iNumber / 10 != 0)) Displa.y0igit (hdc, iNumber / 10) ; OffsetWindowOrgEx (hdc, -42, 0. NULL) ; DisplayDigit (hdc, iNumber % 10) ; OffsetWindowOrgEx (hdc, -42, 0, NULL) ; ) void DisplayColon (HDC hdc) ( POINT ptColon [2.][47 = ( 2. 21, 6, 17, 10, 21, 6,. 25, 2, 51, 6, 47, 10, 51, 6, 55 1 ; Polygon (hdc. ptColon [07, 4) ; Polygon (hdc, ptColon [17, 4) ; ) OffsetWindowOrgEx (hdc, -12, 0, NULL) ; void DisplayTime (HDC hdc, BOOL f24Hour, BOOL fSuppress) ( SYSTEMTIME st ; Rozdziar 8: Zegar 3t3 GetLocalTime (&st) ; if (f24Hour) DisplayTwoDigits (hdc, st.wHour, fSuppress) ; else DisplayTwoDigits (hdc, (st.wHour %= 12) ? st.wHour : 12, fSuppress) ; DisplayColon (hdc) ; DisplayTwoOigits (hdc, st.wMinute, FALSE) ; DisplayColon (hdc) DisplayTwoDigits (hdc, st.wSecond, FALS.E) ; l LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) I':'. static BOOL f24Hour, fSuppress : i static HBRUSH hBrushRed ; static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; TCHAR szBuffer [2] ; switch (message) case WM_CREATE: hBrushRed = CreateSolidBrush (RGB (255, 0, 0)) ; SetTimer fhwnd, ID TIMER, 1000, NULL) ; i // kontynuacja case WM_SETTINGCHANGE: GetLocaleInfo (LOCALE_USER_DEFAULT, LOCALEITIME, szBuffer, 2) ; f24Hour = (szBuffer[0] = '1') ; GetLocaleInfo (LOCALE_USER DEFAULT, LOCALEITLZERO, szBuffer, 2) ; fSu.ppress = (szBuffer[0] = '0') : InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; I,'. cyClient = HIWORD (lParam) ; retu:rn 0 : case WM TIMER: InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) : SetMapMode (hdc. MMISOTROPIC) ; SetWindowExtEx (hdc, 216, 72, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; SetWindowOrgEx (hdc, 138, 36, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; 314 Częć I: (ciąg dalszy ze strony 313) SelectObject (hdc, GetStockObject (NULLPEN)) ; SelectObject (hdc, hBrushRed) ; DisplayTime (hdc, f24Hour, fSuppress) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: KillTimer (hwnd, ID_TIMER) ; Delete0bject (hBrushRed) ; PostOuitMessage (0) ; ' return 0 ; l return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek 8-3. Program DIGCLOCK Okno programu DIGCLOCK przedstawione zostało na rysunku 8-4. e nr^ aAs a Rysunek 8-4. Okno programu DIGCLOCK Chociaż na rysunku tego nie widać, poszczególne cyfry są czerwone. W ramach obsługi komunikatu WM-CREATE procedura okna programu tworzy czerwony pędzel, natomiast usuwa go - w ramach obsługi WM DESTROY. Również w WM CREATE ustawiany jest zegar generujący komunikaty WM TIIVIER co jed- ną sekundę. Jak łatwo się domyślić, jest on usuwany w WM DESTROY. (Wywo- łaniem funkcji GetLocallnfo zajmę się wkrótce). Po odebraniu komunikatu WM TIMER, procedura okna programu DIGCLOCK unieważnia cały obszar okna za pomocą funkcji InvalidateRectangle. Nie jest to naj- lepsze z możliwych rozwiązań, ponieważ oznacza ono, że co sekundę całe okno będzie odświeżane, co w pewnych okolicznościach może doprowadzić do migo- tania obrazu. Zdecydowanie najlepszym rozwiązaniem byłoby unieważnienie tylko tych fragmentów obszaru roboczego okna, które wymagają zmiany ze wzglę- du na upływający czas. Jednakże algorytm, który należałoby zaimplementować, jest dość złożony. Unieważnianie okna w ramach obsługi komurukatu 4VMTIMER powoduje, że cała jego logika umieszezona jest w obshxdze WMPAINT. Obsługę tego komu- Rozdział 8: Zegar 315 mikatu program DIGCLOCK rozpoczyna od ustawienia trybu odwzorowania (ang. mapping mode) na MM ISOTROPIC. Dlatego też może on używać arbitralnie wy- branej skali osi, takiej samej zarówno w pionie, jak i w poziomie. Osie te (usta- wione za pomocą funkcji SetWindowExtEx) podzielone są w poziomie na 276 jed- nostek, a w pionie - na 72. Oczywiście, osie te wyglądają całkiem dowolnie, jed- nak ich punktem odniesienia są odstępy, jakie powinny być zachowane pomię- dzy poszczególnymi cyframi zegara. Jako punkt początkowy okna (138, 36) wybrany został jego środek. Z kolei punkt początkowy widoku (ang. viewport) to (cxClient/2, cyClient/2). Oznacza to, że ze- gar wyświetlany będzie w centrum obszaru roboczego programu DIGCLOCK, jednak sam program może posługiwać się osiami, których punkt początkowy (0, 0) znajduje się w lewym górnym rogu ekranu. Kolejną czynnością wykonywaną w ramach obshzgi komunikatu WM PAINT jest przypisanie aktualnemu pędzlowi tego, który został stworzony wcześniej (czer- wony). Natomiast jako pióro wybierane jest NULL PEN. Kolejnym krokiem jest wywołanie funkcji DispIayTime. Pobieranie aktualnego czasu Funkcja DispIayTime rozpoczyna się od wywołania funkcji Windows GetLocaITi- me. Jedynym jej parametrem jest struktura SYSTEMTIME zdefiniowana w pliku nagłówkowym WINBASE.H w następujący sposób: typedef struct SYSTEMTIME f WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds; ) SYSTEMTIME, *PSYSTEMTIME ; Jak można było się spodziewać, struktura SYSTEMTIME zawiera zarówno pola służące do przechowywania czasu, jak i pola przechowujące datę. Miesiące nu- merowane są od 1 (styczeń ma numer 1), natomiast dni tygodnia - od 0 (niedzie- la to 0). Pole wDay przechowuje informację o aktualnym dniu miesiąca, który rów- nież numerowany jest od l. Struktura SYSTEMTIME wykorzystywana jest głównie przez funkcje GetLocalTi- me oraz GetSystemTime. Funkcja GetSystemTime zwraca uniwersalny czas skoor- dynowany (ang. Universal Coordinated Time, UTC), który z grubsza odpowiada średniemu czasowi Greenwich. Z kolei funkcja GetLocalTime zwraca czas lokalny wyznaczony na podstawie strefy czasowej komputera. Dokładność obu funkcji zależy wyłącznie od staranności użytkownika, który powinien zadbać o poprawne ustawienie czasu oraz zdefiniowanie strefy czasowej. Aktualną strefę czasową możesz sprawdzić, dwukrotnie klikając zegar znajdujący się na pasku zadań. Pro- 316 Częśe J: Podstawy gram umożliwiający ustawienie dokładnego czasu na podstawie informacji ścią- gniętych przez Internet przedstawiony został w rozdziale 23. System Windows udostępnia również funkcje SetLocalTime oraz SetSystemTime. Te i inne podobne funkcje opisane zostały w /Piatform SDK/Windows Base Servi- ces/General Library/Time. Wyświetlanie cyfr i dwukropków Program DIGCLOCK mógłby być znacznie prostszy, gdybyśmy wykorzystali czcionkę symulującą wyświetlacz siedmiosegmentowy. Niestety, program wszyst- ko musi zrobić sam, posługując się funkcją Polygon. Funkcja DisplayDigit programu DIGCLOCK definiuje dwie tablice. Pierwsza z nich, fSevenSegment, zawiera siedem wartości typu BOOL dla każdej z 10 cyfr od 0 do 9. Wartości te określają, które segmenty wyświetlacza są zapalone (wartość 1), a które zgaszone (wartość 0). Każdy z siedmiu segmentów wyświetlany jest jako sześcio- kąt. Druga tablica programu, ptSegment, przechowuje struktury POINT określa- jące współrzędne graficzne każdego z siedmiu segmentów. Dlatego też każda cyfra rysowana jest w następujący sposób: for (i5eg = 0 ; iSeg < 7 ; iSeg++) if (fSevenSegment [iNumber][iSeg]) Polygon (hdc, ptSegment [iSeg], 6) ; Podobnie (chociaż znacznie prościej) funkcja DisplayColon wyświetla znak dwu- kropka oddzielający minuty i sekundy. Poszczególne cyfry mają szerokość 42 jed- nostek, natomiast dwukropek - 6. Dlatego też 6 cyfr i 2 dwukropki zajmują ra- zem 276 jednostek. Jest to wartość, która została podana w wywołaniu funkcji SetWindowExtEx. W momencie wejścia do funkcji DisplayTime punkt początkowy znajduje się w le- wym górnym narożniku miejsca przeznaczonego na lewą skrajną cyfrę. Display- Time wywohje funkcję DispIayTwoDigits, która z kolei dwa razy wywohzje Di- splayDigit, przy czym za każdym razem wywołana zostaje również funkcja Offset- WindowOrgEx przesuwająca początek układu współrzędnych o 42 jednostki w pra- wo. Podobnie, funkcja DisplayColon przesuwa początek układu o 12 jednostek w prawo. Dzięki temu DisplayTime, wyświetlając każdą cyfrę, może zawsze posłu- giwać się tymi samymi współrzędnymi, niezależnie od tego, gdzie obiekt ma się pojawić w oknie. , Inne sztuczki w kodzie polegają na uwzględnieniu wyświetlania czasu w forma- cie 12- i 24-godzinnym oraz na pomijaniu pierwszej cyfry, jeżeli jest ona zerem. Byó światowcem Chociaż wyświetlanie czasu w sposób proponowany w DIGCLOCK wydaje się odporne na błędy, w bardziej zaawansowanych programach prezentujących datę lub czas powixieneś polegać na ustawieniach międzynarodowych systemu Win- dows. Najprostszym sposobem sformatowania daty i czasu jest skorzystanie z funk- cji GetDateFormat oraz GetTimeFormat. Zostały one opisane w rnlatform SDL/Win- dows Base Services/General Library/String Manipulation Reference/String Manipulation Functions. Jako parametr wejściowy pobierają one strukturę SYSTEMTlME, a na- Rozdział : Zegar 317 stępnie formatują odpowiednio datę i czas na podstawie ustawień dokonanych za Y,' pomocą apletu Ustawienia regionalne znajdującego się w Panelu sterowania. Program DIGCLOCK nie może posłużyć się funkcją GetDateFormat, ponieważ wie jedynie, w jaki sposób należy wyświetlać cyfry oraz dwukropki. Mimo wszystko jednak powinien respektować ustawienia użytkownika dotyczące wyświe ania czasu w formacie 12- lub 24-godzinnym, jak również ukrywanie (lub nie) pierw- szej cyfry, jeżeli jest ona zerem. Wszelkie niezbędne po temu informacje możesz otrzymać, wywołując funkcję GetLocalelnfo. Mimo że została ona udokumentowa- na w /Platform SDK/Windows Base Seroices/General Library/String Manipulation/String Manipulation Reference/String Manipulation Functions, identyfikatory, z których bę- dziesz korzystał, przedstawione zostały w /Platform SDK/Windows Base Services/In- ternational Features/National Language Support/Nationale Language Support Constants. Początkowo program DIGCLOCK wywołuje dwukrotnie funkcję GetLocalelnfo w ramach obsługi komunikatu WMCREATE. Za pierwszym razem z identyfikato- rem LOCALE TIME (aby określić, czy ma być wykorzystany format 12- czy też 24-godzinny). Po raz drugi natomiast z identyfikatorem LOCALE ITLZERO (co pozwala na sprawdzenie, czy mają być wyświetlane wiodące zera). Funkcja Ge- tLocaleInfo zwraca wszelkie informacje w postaci napisów, jednak jeżeli to koniecz- ne z łatwością można je przekonwertować na liczby całkowite. Otrzymane za po- mocą tej funkcji wartości program DIGCLOCK przechowuje w zmiennych sta- tycznych, które są następnie przekazywane do funkcji DispIayTime. Jeżeli użytkownik zmodyfikuje jakikolwiek parametr systemu, rozesłany zostaje do wszystkich aplikacji komunikat WMSETTINGCHANGE. W ramach jego ob- sługi program DIGCLOCK ponownie wywołuje GetLocalelnfo. Dzięki temu mo- żesz eksperymentować z różnymi ustawieniami za pomocą apletu Ustawienia regionalne znajdującego się w Panelu sterowania. Teoretycznie prograrn DIGCLOCK powinien również wywoływać funkcję GetLo- calelnfo z parametrem LOCALE STIME. Spowoduje to, że funkcja zwróci zdefi- niowane przez użytkownika separatory godzin, minut, sekund oraz dziesiątych części sekundy. Ponieważ jednak DIGCLOCK został napisany w ten sposób, aby wyświetlać jedynie dwukropki, zostaną one wykorzystane w charakterze sepa- ratorów, nawet jeżeli użytkownik zdecyduje się na coś innego. Chcąc określić, czy czas dotyczy przedpołudnia czy też wieczora, aplikacja może wywołać GetLoca- leInfo z identyfikatorem LOCALE 51159 lub LOCALE 52359. Zwrócone zostaną napisy odpowiednie dla kraju i języka użytkownika. Moglibyśmy również wyposażyć program DIGCLOCK w obsługę komunikatu WMTIMECHANGE. Jest on wysyłany do aplikacji, aby ją poinformować o zmia- nie daty systemowej lub czasu. Nie jest to jednak konieczne, ponieważ program jest uaktualniany co sekundę na skutek odebrania komunikatu WM TIMER. Prze- twarzanie WM TTMECHANGE miałoby większy sens w przypadku zegara, któ- ry byłby aktualizowany co minutę. Zegar analogowy Zegar analogowy nie musi troszczyć się o jakiekolwiek ustawienia międzynaro- dowe, jednak złożoność jego gratiki z powodzenem rekompensuje te niewielkie 318 Część I: Podstawy ; oszczędności. Jeżeli chcesz sobie z nim poradzić, musisz orientować się w trygo- nometr. Na rysunku 8-5 przedstawiony został program CLOCK. I CLOCK.C /* CLOCK.C - Zegar analogowy (c) Charles Petzold, 1998 */ include include define ID_TIMER 1 define TWOPI (2 * 3.14159) LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) static TCHAR szAppName[ = TEXT ("Clock") ; HWND hwnd; MSG msg; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (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 ("Analog Clock"), 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) ; } Rozdział 8: Zegar 319 return msg.wParam ; void SetIsotropic (HDC hdc, int cxClient, int cyClient) ( SetMapMode (hdc, MMISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient / 2, -cyClient / 2, NULL) ; SetViewportOrgEx (hdc, cxClient / 2, cyClient / 2, NULL) ; 1 void RotatePoint (POINT ptC7, int iNum, int iAngle) ( int i ; POINT ptTemp ; for (i = 0 ; i < iNum ; i++) l ptTemp.x = (int) (ptCi].x * cos (TWOPI * iAngle / 360) + ptCi7.y * sin (TWOPI * iAngle / 360)) ; ptTemp.y = (int) (pt[i7.y * cos (TWOPI * iAngle / 360) - ptCi].x * sin (TWOPI * iAngle / 360)) ; ptCi7 = ptTemp ; void DrawClock (HDC hdc) int iAngle ; POINT ptC37 ; for (iAngle = 0 ; iAngle < 360 ; iAngle += 6) i ;, ptC07.x = 0 ; ptC07.y = 900 : RotatePoint (pt, l, iAngle) ; ptC27.x = ptC27.y = iAngle % 5 ? 33 : 100 ; ptC07.x -= ptC27.x / 2 ; ptC07.y -= ptC2].y / 2 ; ptCll.x = ptCOl.x + ptC27.x : ptCl7.y = ptC07.y + ptC27.y : SelectObject (hdc, 6etStockObject (BLACK BRUSH)) ; Ellipse (hdc, ptC07.x, ptC07.y, ptCl7.x, ptCl7.y) : void DrawHands (HDC hdc, SYSTEMTIME * pst, BOOL fChange) ( static POINT ptC37C5] = i 0, -150, 100, 0, 0, 600, -100, 0, 0, -150, 0, -200, 50, 0, 0, 800. -50, 0, 0, -200, 0, 0, 0, 0. 0, 0, 0, 0, 0, 800 ) ; 320 Czść t: Podstawy (ciąg dalszy ze strony 3l9) int i, iAngle[3J ; POINT ptTemp[3J[5J ; iAngl.e[OJ = (pst->wHour.* 30) % 360 + pst->wMinute / 2 ; iAngle[1] = pst->wMinute * 6 ; iAngle[2] = pst->wSecond. * 6 ; memcpy (ptTemp, pt, sizeof (pt)) : for (i = fChange ? 0 : 2 ; i < 3 ; i++) f RotatePoint (ptTemp[iJ, 5, iAngle[iJ) ; Polyline (hdc, ptTemp[iJ, 5) ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static int cxClient, cyClient ; static SYSTEMTIME stPrevious ; BOOL fChange ; HDC hdc ; PAINTSTRUCT ps ; SYSTEMTIME st ; switch (message) f case WM_CREAFE : SetTimer (hwnd, ID_TIMER, 1000, NULL) ; GetLocalTime (st) ; stPrevious = st ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_TIMER : GetLocalTime (&st) ; fChange = st.wHour != stPrevious.wHour st.wMinute != stPrevious.wMinute ; hdc = GetDC (hwnd) ; SetIsotropic (hdc, cxClient, cyClient) ; SelectObject (hdc, GetStockObject (WHITEPEN)) ; DrawHands (hdc, &stPrevious, fCha.nge) ; ' SelectObject (hdc, GetStockObject (BLACKPEN)) ; DrawHands (hdc, &st, TRUE) ; ReleaseDC (hwnd, hdc) ; 321 stPrevious = st ; return 0 ; case WMPAINT : hdc = BeginPaint (hwnd, &ps) ; SetIsotropic (hdc, cxClient, cyClient) ; DrawClock (hdc) ; DrawHands (hdc, &stPrevious. TRUE) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : KillTimer (hwnd, ID_TIMER) ; PostOuitMessage (0) ; return 0 ; ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunetc &5. Program CLOCK Okno programu CLOCK przedstawione zostało na rysunku 8-6. , ' 'ł ,ł . . . . . . ł ł . , . . . . ł . . . . . . . ł . . . . . . .ł. ł. . . .ł. . Rysuek 8-. Okno programu CLOCK Kolejny raz okazuje sig, że dla takiej aplikaeji idealny jest tryb odwzorowania izo- tropicznego. Za jego ustawienie odpowiedzialna jest funkcja Setlsotroic znajdu- jąca się w CLOCK.C. Po wywołaniu SetMapMode funkcja ta ustawia wielkość okna na 100, natomiast wielkość widoku na połowę wysokości obszaru roboczego oraz ujemną (pomnożoną razy -I) połowę wysokości obszaru roboczego. Punkt po- czątkowy medium ustawiony zostaje w środku obszaru roboezego. Jak napisa- łem w rozdziale 5, spowoduje to utworzenie kartezjańskiego układu współrzęd- nych, którego punkt (0,0) znajduje się w środku obszaru roboczego, a każda oś ma długość 1000 w obu kierunkach. Trygonometria dochodzi do głosu w funkeji RotatePoint. Jej trzy parametry po- zwalają na przekazanie do tablicy punktów liczby znajdujących się w niej ele- mentów oraz kąta, o który mają one zostać obrócone. Obrót następuje zgodnie 322 Czść i: Podstawv z ruchem wskazówek zegara (jak to zwykle bywa we wszystkich porządnych ze- garach) wokół środka. Na przykład, jeżeli przekazywany do funkcji punkt ma współrzędne (0,100) - czyli, znajduje się na pozycji godziny 12:00 - a kąt obrotu wynosi 90 stopni, jego współrzędne zostaną przekształcone na (100,0) - czyli na odpowiednik godziny 3:00. Funkcja posługuje się następującymi formułami: x'=x*cos(a)+y*sin(a) y'=y*cos(a)+x*sin(a) Funkcja RotatePoint jest użyteczna nie tylko w trakcie rysowania tarczy zegara, ale, jak za chwilę się przekonamy, również jego wskazówek. Funkcja DrawClock odpowiedzialna jest za wyświetlenie 60 kropek, rozpoczyna- jąc od tej, która znajduje się najwyżej (odpowiada godzinie 12:00). Każda z nich znajduje się w odległości 900 jednostek od środka układu współrzędnych - pierw- sza znajduje się w punkcie (0, 900). Każda kolejna jest przesuwana o 6 stopni zgodnie z ruchem wskazówek zegara. Dwanaście kropek odpowiadających go- dzinom ma średnicę 100 jednostek, natomiast pozostałe - 33 jednostki. Poszcze- gólne kropki rysowane są za pomocą funkcji Ellipse. Furikcja DrawHands rysuje trzy wskazówki zegara: wskazującą godziny, minuty oraz sekundy. Współrzędne każdej z nich, odpowiadające pozycji pionowej, prze- chowywane są w tablicy struktur POINT. W zależności od aktualnego czasu współrzędne te obracane są o odpowiedni kąt za pomocą funkcji RotatePoint, a na- stępnie wyświetlane przez funkcję Windows Polyline. Zwróć uwagę, że wskazówka minutowa oraz godzinna wyświetlane są tylko wtedy, gdy parametr bChange funk- cji DrawHands ma wartość TRUE. W trakcie odświeżania położenia te dwie wska- zówki w większości przypadków nie zostaną przesunięte. Zwróćmy teraz uwagę na funkcję okna. W ramach obsługi komunikatu WM-CRE- ATE funkcja ta pobiera aktualny czas i umieszcza go w zmiennej o nazwie dtPre- vious. Zostanie ona później wykorzystana do określenia, czy poprzednio zmieni- ła się godzina lub minuta. Po raz pierwszy zegar jest rysowany za pośrednictwem komunikatu WM-PAINT. Wystarczy po prostu wywołać funkcje Setlsotropic, DrawClock oraz DrawHands. W ramach obsługi komunikatu WM-TiMER najpierw pobierany jest aktualny czas , a następnie program sprawdza, czy wskazówki godzin i minut wymagają od- świeżenia. Jeżeli tak, za pomocą białego pióra rysowane są wszystkie wskazów- ki na podstawie poprzedniej wartości czasu. Dzięki temu prostemu zabiegowi zostają one usunięte. W przeciwnym wypadku w ten sam sposób rysowany jest tylko sekundnik. Następnie rysowane są wszystkie wskazówki, tym razem jed- nak z wykorzystaniem czarnego pióra. Zegar a raporty Ostatni program w tym rozdziale poświęcony będzie czemuś, o czym wspomnia- łem w rozdziale 5. Jest to jedyne godne uwagi zastosowanie funkcji GetPixel, któ- re znalazłem. Przedstawiony na rysunku 8-7 program WHATCLR wyświetla informację o skła- dowych koloru punktu, który znajduje się akurat pod kursorem myszy. Rozdział 8: Zegar 323 r WHATCLR.C /* WHATCLR.C - Kod koloru pod kursorem myszy (c) Charles Petzold, 1998 */ include define ID TIMER 1 ' (... i void FindWindowSize (int * int *) ; LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) ( static TCHAR szAppName[] = TEXT ("WhatClr") ; HWND hwnd : int cxWindow, cyWindow ; MSG ms9 ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW CS UREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULC, 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 ("This program requires Windows NT!"), szAppName, MBICONERROR) ; FindWindowSize (&cxWindow, &cyWindow) ; hwnd = CreateWindow (szAppName, TEXT ("What Color"), WS_OVERLAPPED WS_CAPTION WS-SYSMENU WS BORDER, CW_USEDEFAULT, CW_USEDEFAULT, cxWindow, cyWindow, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) : UpdateWindow (hwnd) : while (GetMessage (&msg, NULL, 0, 0)) ( TranslateMessage (&msg) ; DispatchMessage (&msg) ; . return msg.wParam ; 324 Część I: Podstawy (ciąg dalszy ze strony 323) ) void FindWindowSize (int * pcxWindow, int * pcyWindow) ( HDC hdcScreen ; TEXTMETRIC tm ; hdcScreen = CreateIC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; GetTextMetrics (hdcScreen, &tm) ; DeleteDC (hdcScreen) ; * pcxWindow = 2 * GetSystemMetrics (SM_CXBORDER) + 12 * tm.tmAveCharWidth ; * pcyWindow = 2 * GetSystemMetrics (SM CYBORDER) + GetSystemMetrics (SM CYCAPTION) + 2 * tm.tmHeight ; ) LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) ( static COLORREF cr, crLast ; static HDC hdcScreen ; HDC hdc ; PAINTSTRUCT ps ; POINT pt ; RECT rc ; TCHAR szBuffer [16] ; switch (message) i case WM_CREATE: hdcScreen = CreateDC (TEXT ("DISPLAY"), NULL, NULL, NULL) ; SetTimer (hwnd, ID TIMER, 100, NULL) ; return 0 ; case WM_TIMER: GetCursorPos (&pt) ; cr = GetPixel (hdcScreen, pt.x, pt.y) ; SetPixel (hdcScreen, pt.x, pt.y, 0); if (cr != crLast) ( crLast = cr ; InvalidateRect (hwnd, NULL, FALSE) ; ) return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rc) ; wsprintf (szBuffer, TEXT (" %02X %02X %02X "), GetRValue (cr), GetGValue (cr), GetBValue (cr)) ; DrawText (hdc, szBuffer, -i, &rc, DTSINGLELINE DT CENTER DTVCENTER) ; Rozdział 8: Zegar 325 EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteDC (hdcScreen) ; KillTimer (hwnd, ID TIMER) ; PostOuitMessage (0) ; return 0 : ) return DefWindowProc (hwnd, message, wParam, lParam) ; Rysunek &7. Program WHATCLR Wykonując funkcję WinMain, program WHATCLR zachowuje się w sposób nieco odmienny niż inne programy. Ponieważ jego okno musi być tylko tak duże, aby pomieścić szesnastkową wartość RGB, to posługując się stylem WS BORDER w funkcji CreateWindow, tworzy okno, którego wielkości nie da się zmienić. Chcąc wyznaczyć rozmiar okna, WHATCLR pobiera informacyjny kontekst urządzenia (ang. information device context) wywohxjąc CreatelC, a następnie GetSystemMetrics. Obliczona w ten sposób wysokość i szerokość przekazywana jest do funkcji Cre- ateWindow. Procedura okna programu WHATCLR w ramach obshzgi komunikatu WMCRE- ATE tworzy kontekst urządzenia dla całego ekranu, wywołując CreateDC. Kon- tekst ten przechowywany jest przez cały czas pracy programu. Z kolei w ramach obsługi komunikatu WM TIMER, pobierany jest kolor punktu, nad którym znaj- duje się kursor myszy. Informacja o nim wyświetlana jest w ramach WMPAINT. Prawdopodobnie zastanawiasz się, czy uchwyt kontekstu urządzenia pobrany za pomocą funkcji CreateDC może zostać wykorzystany nie tylko do pobierania ko- loru punktu, lecz na przykład do wyświetlenia czegoś. Odpowiedź brzmi: tak. , Ogólnie rzecz biorąc uważa się, że "grzeczne" aplikacje nie powinny tego robić niemniej jednak, w niektórych przypadkach, jest to konieczne.