Jacek Matulewski http://www.phys.uni.torun.pl/~jacek/ Tworzenie aplikacji Windows Podstawy obsługi komunikatów Windows (Delphi) Ćwiczenia Toruń, 2 grudnia 2002 Najnowsza wersja tego dokumentu znajduje się pod adresem http://www.phys.uni.torun.pl/~jacek/dydaktyka/rad/rad4_wm_delphi.pdf Źródła opisanych w tym dokumencie programów znajdują się pod adresem http://www.phys.uni.torun.pl/~jacek/dydaktyka/rad/rad4_wm.zip 1
I. Spis treści I. Spis treści... 2 II. Odbieranie i wysyłanie komunikatów...3 1. Lista komunikatów odbieranych przez aplikację... 3 2. Śledzenie komunikatów dotyczących ruchu myszy w obrębie aplikacji... 4 3. Metody obsługujące komunikaty: zmiana położenia formy... 5 4. Wysyłanie komunikatów: wykrycie zmiany trybu pracy karty graficznej... 6 5. Blokowanie zamknięcia sesji Windows... 7 6. Jeszcze raz wysyłanie komunikatów... 7 2
II. Odbieranie i wysyłanie komunikatów Komunikat Windows jest informacją przekazywaną przez system do aplikacji dotyczącą szczególnej sytuacji w systemie związanej bądź z działaniem użytkownika (np. przesunięcie myszy, naciśnięcie klawisza, zamknięcie okna itp.) lub wynikającej z funkcjonowania systemu (np. odmalowanie formy, zamknięcie sesji Windows itp.). Przekazywana przez komunikat informacja dotyczy przede wszystkim rodzaju zdarzenia, której jest skutkiem oraz związanych z tą sytuacją parametrów (zawsze czas wystąpienia, położenie myszy oraz np. naciśnięty klawisz). Mechanizm komunikatów Windows (ang. Windows messages) stracił na znaczeniu w środowisku Delphi/C++ Builderze po wprowadzeniu mechanizmu zdarzeń w obiektach VCL. Większość komunikatów, a wszystkie istotne w codziennej praktyce programowania aplikacji Windows, jest reprezentowana przez odpowiednie zdarzenia. Co więcej operowanie zdarzeniami jest bezpieczniejsze. Jest jednak kilka szczególnych zdarzeń nie mających swojej reprezentacji wśród zdarzeń obiektów VCL wymagających ręcznej obsługi. Przykładem może być komunikat związany z przesunięciem okna (TForm nie ma zdarzenia OnMove). Etapy systemowej obsługi komunikatów są następujące: 1. W reakcji na sytuację w systemie lub działanie użytkownika system tworzy strukturę typu tagmsg (opis w Win32 SDK) zawierającą uchwyt okna którego dotyczy komunikat, numer zdarzenia systemowego (w Win32 SDK opisane są stałe identyfikujące komunikaty, nazwy stałych zaczynają się zawsze od WM_ (ang. Windows Message), definicje stałych można też znaleźć w C++ Builderze w pliku winuser.h), dwa elementy przekazujące informację o zdarzeniu wparam i lparam, czas wystąpienia zdarzenia i położenie myszki. Znaczenie parametrów wparam i lparam opisane jest oddzielnie dla każdego komunikatu w Win32 SDK. 2. Struktura zostaje umieszczona w kolejce komunikatów aplikacji, której okno jest adresatem przesyłanego komunikatu. 3. Aplikacja odbiera komunikat i przekazuje go do właściwego okna (zgodnie z uchwytem w komunikacie) 4. Wywoływana jest metoda obsługująca komunikat (np. WM_PAINT spowoduje odmalowanie formy). Od tej reguły są wyjątki niektóre komunikaty przekazywane są bezpośrednio do właściwego okna z pominięciem kolejki aplikacji. 1. Lista komunikatów odbieranych przez aplikację Obiekt Application (tworzony automatycznie w momencie uruchomienia aplikacji) posiada zdarzenie OnMessage. W nowszych wersjach Delphi dostęp do tego i innych zdarzeń Application został ułatwiony dzięki komponentowi TApplicationEvents. Umieśćmy ten komponent na formie i stwórzmy szkielet metody zdarzeniowej ApplicationEvents1Message(). Uruchamiana będzie zawsze, gdy aplikacja odbierze komunikat. Pierwszym argumentem tej metody jest referencja do struktury komunikatu. Najprostszym zastosowaniem tego zdarzenia jest wyświetlanie listy odebranych komunikatów (nie dotyczy to komunikatów wysyłanych bezpośrednio do okna z pominięciem obiektu aplikacji): ListBox1.Items.Add(IntToStr(Msg.message)); Wcześniej należy umieścić na formie ListBox1 i ustalić liczbę kolumn na np. 15 (ListBox musi być duży, żeby pomieścić efekty ruchu myszki). Można też reagować na wybrane przez nas zdarzenia: procedure TForm1.ApplicationEvents1Message(var Msg: tagmsg; var Handled: Boolean); case (Msg.message) of 3
//Odmalowywanie okna WM_PAINT: ListBox1.Items.Add('WM_PAINT'); //Myszka WM_LBUTTONDOWN: ListBox1.Items.Add('WM_LBUTTONDOWN'); WM_LBUTTONUP: ListBox1.Items.Add('WM_LBUTTONUP'); WM_LBUTTONDBLCLK: ListBox1.Items.Add('WM_LBUTTONDBLCLK'); WM_RBUTTONDOWN: ListBox1.Items.Add('WM_RBUTTONDOWN'); WM_RBUTTONUP: ListBox1.Items.Add('WM_RBUTTONUP'); WM_RBUTTONDBLCLK: ListBox1.Items.Add('WM_RBUTTONDBLCLK'); //Klawiatura WM_KEYDOWN: ListBox1.Items.Add('WM_KEYDOWN'); WM_KEYUP: ListBox1.Items.Add('WM_KEYUP'); WM_CHAR: ListBox1.Items.Add('WM_CHAR'); Pominięto dwa komunikaty związane z ruchem myszy: WM_MOUSEMOVE (w obrębie client area) i WM_NCMOUSEMOVE (poza nim, ale w obrębie okna). Poza rozpoznaniem nazwy można by oczywiście wyświetlić dodatkowe informacje są one dostępne w strukturze Msg. Śledzenie komunikatów w ten sposób nie zmienia ich obsługi przez odpowiednie metody formy (TForm). 2. Śledzenie komunikatów dotyczących ruchu myszy w obrębie aplikacji Stosując zdarzenie OnMessage udostępnione w TApplicationEvents będziemy śledzić położenie myszy tj. zareagujemy na komunikaty WM_MOUSEMOVE i WM_NCMOUSEMOVE. Tworzymy nowy projekt. Na formie umieszczamy cztery komponenty TLabel (w tym przykładzie pokazanym na rysunku dodatkowy obiekt Label3 wykorzystany jest przez znak x pomiędzy Label2 i Label4). Dodajemy również drugą formę i ustalamy jej własność Visible=True. Dalej postępujemy jak w poprzednim paragrafie, z tym, że w metodzie zdarzeniowej umieszczamy następujący kod: procedure TForm1.ApplicationEvents1Message(var Msg: tagmsg; var Handled: Boolean); case (Msg.message) of WM_MOUSEMOVE: Label1.Caption:='W obrębie client area formy (wsp. okna)'; Label2.Caption:=IntToStr(LOWORD(Msg.lParam)); Label4.Caption:=IntToStr(HIWORD(Msg.lParam)); if (Msg.hwnd=Form1.Handle) then Label5.Caption:='Form1' else Label5.Caption:='Form2'; WM_NCMOUSEMOVE: Label1.Caption:='Poza client area formy (wsp. ekranu)'; Label2.Caption:=IntToStr(LOWORD(Msg.lParam)); Label4.Caption:=IntToStr(HIWORD(Msg.lParam)); if (Msg.hwnd=Form1.Handle) then 4
Label5.Caption:='Form1' else Label5.Caption:='Form2'; Widać, że aplikacja odbiera komunikaty skierowane do obu form. Jeżeli myszka znajduje się wewnątrz obszaru dostępnego dla użytkownika (ang. client area) przekazywane przez komunikat współrzędne położenia myszy to współrzędne względem formy, a dokładniej względem lewego górnego rogu obszaru użytkownika. Poza nim (a więc na brzegu okna, na pasku tytułu) są to współrzędne ekranu. Zadanie Uzgodnić współrzędne wyświetlane w przypadku obu komunikatów na współrzędne ekranu lub okna korzystając z funkcji WinAPI ClientToScreen lub ScreenToClient (ewentualnie z metod TForm o tych samych nazwach). Punkt zapisać korzystając ze struktury WinAPI tagpoint (w Delphi jej implementacja nazywa się TPoint) 3. Metody obsługujące komunikaty: zmiana położenia formy Niestety kilka ciekawych komunikatów nie przechodzi przez aplikację. Wśród nich komunikaty związane ze zmianą rozmiaru i położenia okna. O ile zmiana rozmiaru jest obsługiwana przez zdarzenie OnResize formy, to poruszenie okna nie ma odzwierciedlenia w VCL. Możemy jednak przechwycić komunikat i napisać do niego metodę obsługi. Musi być to dosłownie metoda, a nie funkcja, gdyż komunikat dotyczyć będzie obiektu i tylko metoda tego obiektu może go obsłużyć. W ten sposób na poziomie obiektu możemy przechwycić wszystkie komunikaty, nawet te, które omijają kolejkę aplikacji. Zadeklarujmy metodę, najlepiej w sekcji private lub protected, o nazwie związanej z nazwą obsługiwanego komunikatu w ten sposób, że pomijamy znak podkreślenia i część znaczącą nazwy piszemy tzw. stylem wielbłądzim (oddzielne słowa piszemy razem, ale zaznaczamy wielkimi literami). Z WM_MOVE powstaje WMMove, z WM_MOUSEMOVE WMMouseMove. Aby związać metodę obiektu ze zdarzeniem należy do jej deklaracji dodać słowo kluczowe message z odpowiednią stałą identyfikującą komunikat: protected procedure WMMove(var Msg :TMessage); message WM_MOVE; 1 Pozostaje tylko napisać odpowiednią metodę może to być na przykład: procedure TForm1.WMMove(var Msg :TMessage); if (CheckBox1.Checked) then MessageBeep(0); FlashWindow(Application.Handle,True); inherited; //Trzeba samemu zadbac o wywolanie metody WMMove klasy TForm 1 Częstym błędem jest umieszczanie w END_MESSAGE_MAP() klasy bieżącej zamiast bazowej. 5
Zadanie Stworzyć komponent dziedziczący z TForm zawierający zdarzenie OnMove. 4. Wysyłanie komunikatów: wykrycie zmiany trybu pracy karty graficznej 2 Jest pewna grupa komunikatów, które rozsyłane są do wszystkich aplikacji. Związane są one najczęściej ze zmianą parametrów systemu np. wylogowanie użytkownika lub zmiana rozdzielczości karty graficznej. Użytkownik może zareagować na wysłanie także tych komunikatów. Zrobimy to pisząc odpowiednie metody obsługi. Zmiana trybu pracy karty graficznej (a co za tym idzie także monitora) powoduje wysłanie komunikatu WM_DISPLAYCHANGE. Przechwyćmy komunikat deklarując metodę i jej przechwycenie podobnie jak w poprzednim paragrafie: protected procedure WMDisplayChange(var Msg :TMessage); message WM_DISPLAYCHANGE; oraz definiując metodę, która zapisuje nowe parametry ekranu do Memo1: procedure TForm1.WMDisplayChange(var Msg :TMessage); with Memo1.Lines do Add('Zmiana rozdzielczości:'); Add('Komunikat Windows: '+IntToStr(Msg.Msg)); Add('Kolory: '+IntToStr(Msg.WParam)+' bitów'); Add('Rozdzielczość: '+IntToStr(LOWORD(Msg.LParam))+' x '+ IntToStr(HIWORD(Msg.LParam))); Add(''); Przed metodą zostało zdefiniowane makro, które zastępuje długi łańcuch dodawania łańcucha do Memo1. Jeżeli po uruchomieniu aplikacji chcemy uzyskać aktualne parametry możemy to zrobić bardzo prosto wysyłając komunikat do okna za pomocą funkcji SendMessage() lub PostMessage(). Pierwsza wysyła komunikat bezpośrednio do okna, druga do kolejki komunikatów jego aplikacji. Tylko ta druga spowoduje wywołanie zdarzenia Application.OnMessage. Parametry obu funkcji są identyczne: uchwyt do okna, numer komunikatu, lparam, wparam. Dopiszmy przed usunięciem makra: procedure TForm1.FormCreate(Sender: TObject); Memo1.Lines.Add('Rozpoznawamie parametrów wyświetlania (wysyłanie komunikatu):'); SendMessage(Form1.Handle,WM_DISPLAYCHANGE, GetDeviceCaps(Form1.Canvas.Handle,PLANES)* GetDeviceCaps(Form1.Canvas.Handle,BITSPIXEL), DWORD(Screen.Height shl 16)+Screen.Width); W trzecim argumencie badamy ilość kolorów (dokładniej ilość bitów, ilość kolorów możemy uzyskać podnosząc 2 do tej liczby) 3. W ostatnim argumencie, 32-bitowym DWORD, musimy umieścić informacje o rozdzielczości ekranu, tak, aby wysokość zajmowała pierwsze 16-bitów, a szerokość drugie. 2 Paragraf ten można potraktować jako kontynuację rozdziału VI w części dotyczącej WinAPI. 3 Możliwości funkcji WinAPI GetDeviceCaps() są znacznie szersze. Zob. opis w Win32 SDK. 6
5. Blokowanie zamknięcia sesji Windows Kolejnym przykładem przechwycenia obsługi komunikatów systemowych jest zablokowanie zamknięcia systemu przez aplikację. Można oczywiście zrobić podobnie działającą aplikację korzystając ze zdarzenia TForm.OnCloseQuery, ale wynik nie jest do końca satysfakcjonujący ponieważ nie w każdej sytuacji system pyta aplikację o pozwolenie zamknięcia. Natomiast obsługa komunikatu WM_QUERYENDSESSION daje bardzo dobre rezultaty. Postępujemy podobnie jak poprzednio. Do deklaracji klasy dodajemy protected procedure WMQueryEndSession(var Message :TWMQueryEndSession); message WM_QUERYENDSESSION; natomiast w pliku głównym definiujemy metodę obsługi komunikatu jak poniżej: procedure TForm1.WMQueryEndSession(var Message :TWMQueryEndSession); ShowMessage('Zamknięcie sesji Windows zablokowane'); FlashWindow(Application.Handle,true); Message.Result:=0; Po uruchomieniu aplikacji próba zamknięcia systemu zakończy się jedynie wyświetleniem odpowiedniego komunikatu. 6. Jeszcze raz wysyłanie komunikatów Przykład wysyłania komunikatu mieliśmy w paragrafie II.4. Teraz jeszcze tylko jeden zabawny przykład. Wysyłając komunikat do systemu można spowodować włączenie wygaszacza ekranu. Wystarczy wykonać polecenie: SendMessage(Application.Handle,WM_SYSCOMMAND,SC_MONITORPOWER,0); 7