Jacek Matulewski http://www.phys.uni.torun.pl/~jacek/ Tworzenie aplikacji Windows Podstawy obsługi komunikatów Windows (C++ Builder) Ćwiczenia Toruń, 2 grudnia 2002 Najnowsza wersja tego dokumentu znajduje się pod adresem http://www.phys.uni.torun.pl/~jacek/dydaktyka/rad/rad4_wm_cbuilder.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 C++ Buildera 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(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: void fastcall TForm1::ApplicationEvents1Message(tagMSG &Msg, bool &Handled) 3
switch (Msg.message) //Odmalowywanie okna case WM_PAINT: ListBox1->Items->Add("WM_PAINT"); break; //Myszka case WM_LBUTTONDOWN: ListBox1->Items->Add("WM_LBUTTONDOWN"); break; case WM_LBUTTONUP: ListBox1->Items->Add("WM_LBUTTONUP"); break; case WM_LBUTTONDBLCLK: ListBox1->Items->Add("WM_LBUTTONDBLCLK"); break; case WM_RBUTTONDOWN: ListBox1->Items->Add("WM_RBUTTONDOWN"); break; case WM_RBUTTONUP: ListBox1->Items->Add("WM_RBUTTONUP"); break; case WM_RBUTTONDBLCLK: ListBox1->Items->Add("WM_RBUTTONDBLCLK"); break; //Klawiatura case WM_KEYDOWN: ListBox1->Items->Add("WM_KEYDOWN"); break; case WM_KEYUP: ListBox1->Items->Add("WM_KEYUP"); break; case WM_CHAR: ListBox1->Items->Add("WM_CHAR"); break; 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: void fastcall TForm1::ApplicationEvents1Message(tagMSG &Msg, bool &Handled) switch (Msg.message) case WM_MOUSEMOVE: Label1->Caption="W obrębie client area formy (wsp. okna)"; Label2->Caption=LOWORD(Msg.lParam); Label4->Caption=HIWORD(Msg.lParam); if (Msg.hwnd==Form1->Handle) Label5->Caption="Form1"; else Label5->Caption="Form2"; break; case WM_NCMOUSEMOVE: Label1->Caption="Poza client area formy (wsp. ekranu)"; Label2->Caption=LOWORD(Msg.lParam); Label4->Caption=HIWORD(Msg.lParam); if (Msg.hwnd==Form1->Handle) 4
Label5->Caption="Form1"; else Label5->Caption="Form2"; break; 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 C++ Builderze występuje też jako 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. protected: void fastcall WMMove(TMessage& Msg); W następnych liniach deklaracji klasy musimy za pomocą odpowiednich makr przechwycić komunikat i skierować go do tej metody: MESSAGE_HANDLER(nazwa_komunikatu, typ_struktury_komunikatu, nazwa_metody) /* tu dodajemy kolejne przechwycenia */ END_MESSAGE_MAP(klasa_bazowa) Makra te dbają o wywołanie odpowiedniej metody obsługi klasy bazowej. W naszym przypadku: MESSAGE_HANDLER(WM_MOVE, TMessage, WMMove) END_MESSAGE_MAP(TForm) 5
1 Pozostaje tylko napisać odpowiednią metodę może to być na przykład: void fastcall TForm1::WMMove(TMessage& Msg) if (CheckBox1->Checked) MessageBeep(0); FlashWindow(Application->Handle,true); 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: void fastcall WMDisplayChange(TMessage& Msg); MESSAGE_HANDLER(WM_DISPLAYCHANGE, TMessage, WMDisplayChange) END_MESSAGE_MAP(TForm) oraz definiując metodę, która zapisuje nowe parametry ekranu do Memo1: #define pisz(a) Memo1->Lines->Add(A) void fastcall TForm1::WMDisplayChange(TMessage& Msg) pisz("zmiana rozdzielczości:"); pisz("komunikat Windows: "+(AnsiString)(double)Msg.Msg); pisz("kolory: "+(AnsiString)(double)Msg.WParam+" bitów"); pisz("rozdzielczość: "+(AnsiString)LOWORD(Msg.LParam)+" x "+(AnsiString)HIWORD(Msg.LParam)); pisz(""); #undef pisz 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 1 Częstym błędem jest umieszczanie w END_MESSAGE_MAP() klasy bieżącej zamiast bazowej. 2 Paragraf ten można potraktować jako kontynuację rozdziału VI w części dotyczącej WinAPI. 6
zdarzenia Application->OnMessage. Parametry obu funkcji są identyczne: uchwyt do okna, numer komunikatu, lparam, wparam. Dopiszmy przed usunięciem makra: void fastcall TForm1::FormCreate(TObject *Sender) pisz("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<<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. 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: void fastcall WMQueryEndSession (TWMQueryEndSession& Message); MESSAGE_HANDLER(WM_QUERYENDSESSION, TWMQueryEndSession, WMQueryEndSession); END_MESSAGE_MAP(TForm) natomiast w pliku głównym definiujemy metodę obsługi komunikatu jak poniżej: void fastcall TForm1::WMQueryEndSession(TWMQueryEndSession& Message) 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_SCREENSAVE,0); 3 Możliwości funkcji WinAPI GetDeviceCaps() są znacznie szersze. Zob. opis w Win32 SDK. 7