Rozdział 3.
Obsługa komunikatów Windows
Pomimo iż Delphi w dużym stopniu uwalnia programistę od operowania komunikatami Windows — na rzecz bardziej wygodnego mechanizmu zdarzeń — to jednak warto poświęcić komunikatom nieco uwagi, zwłaszcza w kontekście ich związku ze zdarzeniami Delphi. Dla projektanta nowych komponentów gruntowna znajomość natury komunikatów i zasad zarządzania nimi jest wręcz niezbędna; dla projektantów posługujących się gotowymi komponentami może być przydatna, z tego względu, iż bezpośrednie operowanie komunikatami bywa niekiedy koniecznością, bo nie wszystko da się wykonać za pomocą standardowych mechanizmów Delphi. W niniejszym rozdziale opiszemy pokrótce system komunikatów Win32 i zasady ich obsługi w Object Pascalu, zajmiemy się także ich związkiem ze zdarzeniami Delphi.
Wskazówka
Komunikaty są mechanizmem specyficznym dla Windows i nie znajdują zastosowania w aplikacjach międzyplatformowych (CLX). Szczegółowe informacje na temat aplikacji międzyplatformowych zawarte są w rozdziale 13.
Natura komunikatów
Komunikaty Windows stanowią odzwierciedlenie — w postaci odpowiednich struktur danych i procedur — pewnych szczególnych sytuacji: naciśnięcia klawisza, kliknięcia, przesunięcia myszy, upłynięcia odcinka czasu itp.
Strukturą danych ucieleśniającą komunikat jest rekord zawierający informację o rodzaju zdarzenia oraz dane niosące informację dodatkową; rzeczownik „zdarzenie” należy tu rozumieć w znaczeniu potocznym, nie w sensie zdarzeń Delphi czy zdarzeń Win32. W kategoriach Object Pascala rekord ten, w najbardziej podstawowej formie, ma następującą strukturę:
Type
TMsg = packed record
// uchwyt okna, do którego komunikat jest adresowany
hwnd: HWND;
// identyfikator komunikatu
message: UINT;
// dwa 32-bitowe pola zawierające dodatkową informację
wParam: WPARAM;
lParam: LPARAM;
// czas utworzenia komunikatu
time: DWORD;
// pozycja kursora myszy w momencie utworzenia komunikatu
pt: TPoint;
end;
Powyższa definicja znajduje się w module Windows.pas, a znaczenie poszczególnych jej pól jest następujące:
hwnd — 32-bitowy uchwyt okna, do którego komunikat jest adresowany; z każdą okienkową kontrolką Delphi związane jest okno, którego uchwyt przechowywany jest pod właściwością Handle.
message — stała symboliczna klasyfikująca komunikat; oprócz stałych predefiniowanych w module Windows.pas możliwe jest definiowanie własnych komunikatów.
wParam — zawartością tego pola jest najczęściej stała stowarzyszona z komunikatem; może ono również zawierać uchwyt okna źródłowego lub numer identyfikacyjny kontrolki związanej z treścią komunikatu.
lParam — pole to zawiera najczęściej dodatkowe dane o zdarzeniu, bądź indeks, czy wskaźnik do określonej struktury danych w pamięci. Jako że pola wParam i lParam są 32-bitowe, możliwe jest ich rzutowanie na dowolny typ wskaźnikowy.
time — zawiera czas utworzenia rekordu związanego z komunikatem.
pt — zawiera położenie kursora myszy (we współrzędnych ekranowych) w momencie, gdy utworzono rekord związany z komunikatem.
Po zapoznaniu się z ogólną strukturą komunikatu, zobaczmy teraz, jakie typy komunikatów można napotkać w Win32.
Typy komunikatów
Dla każdego standardowego komunikatu Windows, Delphi (w module Messages.pas) definiuje charakterystyczną stałą symboliczną, określającą jedną z wartości pola message rekordu TMsg. Każda z tych stałychsymbolicznych rozpoczyna się od liter WM_ (Windows Message). Zestawienie najczęściej spotykanych komunikatów zawiera tabela 3.1.
Tabela 3.1. Najczęściej występujące komunikaty Windows
Identyfikator komunikatu |
Wartość |
Znaczenie |
WM_ACTIVATE |
$0006 |
Okno docelowe staje się aktywne lub przestaje być aktywne. |
WM_CHAR |
$0102 |
Naciśnięto i zwolniono klawisz; komunikat ten występuje łącznie z parą komunikatów WM_KEYDOWN—WM_KEYUP . |
WM_CLOSE |
$0010 |
Okno docelowe powinno zostać zamknięte. |
WM_KEYDOWN |
$0100 |
Naciśnięto klawisz. |
WM_KEYUP |
$0101 |
Zwolniono klawisz. |
WM_LBUTTONDOWN |
$0201 |
Naciśnięto lewy przycisk myszy. |
WM_MOUSEMOVE |
$0200 |
Przesunięto kursor myszy. |
WM_PAINT |
$000F |
Okno docelowe powinno odświeżyć |
WM_TIMER |
$0113 |
Wystąpiło zdarzenie zegarowe. |
WM_QUIT |
$0012 |
Należy zakończyć aplikację. |
Jak funkcjonuje system komunikatów Windows?
Z obsługą komunikatów Windows związane są trzy kluczowe elementy:
Kolejka komunikatów (message queue) — system Windows utrzymuje oddzielną kolejkę komunikatów dla każdej aplikacji; za pobieranie komunikatów z tej kolejki i ich obsługę odpowiedzialna jest aplikacja.
Pętla obsługi komunikatów (message loop) — centralną częścią każdej aplikacji Windows jest pętla pobierająca cyklicznie komunikaty z kolejki aplikacji i kierująca je do właściwych okien.
Procedura okienkowa (window procedure) — jest to procedura związana z konkretnym oknem, zajmująca się obsługą komunikatów kierowanych do tego okna; jest wywoływana w trybie odwołania zwrotnego (callback), a rezultatem jej działania jest najczęściej zapisanie informacji zwrotnej w otrzymanym rekordzie komunikatu.
Notatka
Odwołanie zwrotne polega na asynchronicznym wywołaniu (przez system operacyjny) procedury lub funkcji stanowiącej część aplikacji; dokładniej zajmiemy się tym zagadnieniem w rozdziale 6.
„Koleje życia” typowego komunikatu przedstawiają się więc następująco:
W systemie występuje określone zdarzenie.
System dokonuje klasyfikacji zdarzenia, tworzy reprezentującą je strukturę danych i umieszcza ją w kolejce związanej z aplikacją, której zdarzenie dotyczy.
Aplikacja odczytuje wspomnianą strukturę z kolejki i formuje na jej podstawie rekord reprezentujący komunikat.
Aplikacja przekazuje rekord komunikatu do właściwej procedury okienkowej
Procedura okienkowa wykonuje działania specyficzne dla otrzymanego komunikatu.
Powyższy scenariusz jest przedstawiony schematycznie na rysunku 3.1. Kroki 3. i 4. realizują to, co przed chwilą nazwaliśmy pętlą obsługi komunikatu. Pętla taka jest charakterystyczna dla każdego programu Windows, gdyż cała jego praca sprowadza się do właściwego reagowania na zdarzenia zewnętrzne, czyli w konsekwencji — na komunikaty Windows. Może się oczywiście zdarzyć tak, że kolejka komunikatów jest pusta i działanie programu zostaje zawieszone w punkcie 3., aż do otrzymania jakiegoś komunikatu przez aplikację.
Rysunek 3.1. Schemat funkcjonowania komunikatów Windows
Obsługa komunikatów w kategoriach Delphi
Biblioteka VCL w znacznym stopniu odciąża programistę od obsługi komunikatów, realizując chociażby wspomnianą pętlę pobierającą komunikaty (jest ona zaimplementowana w module Forms.pas). Delphi definiuje ponadto (w module Messages.pas) własną strukturę reprezentującą informację zawartą w komunikacie:
TMessage = packed record
Msg: Cardinal;
case Integer of
0: (
WParam: Longint;
LParam: Longint;
Result: Longint);
1: (
WParamLo: Word;
WParamHi: Word;
LParamLo: Word;
LParamHi: Word;
ResultLo: Word;
ResultHi: Word);
end;
Struktura ta zawiera nieco mniej informacji niż jej pierwowzór TMsg, z którego przejmuje jedynie identyfikator komunikatu oraz parametry lParam i wParam; pozostałe pola są wykorzystywane wewnętrznie przez Delphi.
Pole Result, nie mające odpowiednika w strukturze TMsg, przeznaczone jest do przekazania informacji zwrotnej — jak napisaliśmy przed chwilą, procedura okienkowa musi informować system (w ściśle określonych kategoriach) o wyniku obsługi każdego komunikatu. Po zakończeniu obsługi komunikatu przez aplikację Delphi przetworzy go do postaci zgodnej ze strukturą TMsg, pobierając informację zwrotną z tego właśnie pola. Powrócimy do tej kwestii w dalszym ciągu niniejszego rozdziału.
Struktury specyficzne dla różnych typów komunikatów
Udogodnienia oferowane przez Delphi nie kończą się na poziomie struktury TMessage. By uwolnić programistę od mozolnego „odcyfrowywania” informacji zawartej w polach lParam i wParam, Delphi oferuje rekordy o strukturze specyficznej dla określonych typów komunikatów. Oto rekord charakterystyczny dla większości komunikatów związanych z myszą:
TWMMouse = record
Msg: Cardinal;
Keys: Longint;
case Integer of
0: (
XPos: Smallint;
YPos: Smallint);
1: (
Pos: TSmallPoint;
Result: Longint);
end;
Ten rekord jest dostępny także pod innymi nazwami synonimicznymi, co podkreśla jego związek z poszczególnymi komunikatami:
TWMLButtonDblClk = TWMMouse;
TWMLButtonDown = TWMMouse;
TWMLButtonUp = TWMMouse;
TWMMButtonDblClk = TWMMouse;
TWMMButtonDown = TWMMouse;
TWMMButtonUp = TWMMouse;
Podobne struktury zdefiniowane są dla niemal każdego standardowego komunikatu, zgodnie z jednolitą konwencją nazewniczą: po przedrostku „T” następuje nazwa komunikatu pozbawiona podkreślenia i przekształcona na charakterystyczną dla Pascala notację „wielbłądzią” — na przykład komunikatowi WM_SETFONT odpowiada struktura o nazwie TWMSetFont.
Nic oczywiście nie stoi na przeszkodzie, by zrezygnować z tych udogodnień i posługiwać się uniwersalnym rekordem TMessage, przydatnym dla każdego komunikatu.
Przetwarzanie komunikatów
Jak przed chwilą stwierdziliśmy, przetworzenie komunikatu przez aplikację polega na skierowaniu go do właściwej procedury okienkowej; ta dokonuje jego mozolnej klasyfikacji, interpretacji zawartej w nim informacji, po czym następuje jego właściwa obsługa i zwrotne przekazanie wyniku tej obsługi. Również w tym przypadku Delphi stwarza niebagatelne udogodnienie, gdyż, zamiast jednej uniwersalnej procedury okienkowej, możliwe jest definiowanie odrębnych procedur przeznaczonych dla wybranych typów komunikatów. Każda z tych procedur musi być metodą obiektu, posiadającą jeden parametr przekazywany przez referencję i opatrzoną klauzulą message specyfikującą identyfikator obsługiwanego komunikatu. Oto przykład deklaracji metody obsługującej komunikat WM_PAINT:
procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;
Z punktu widzenia Delphi nazwa metody obsługującej komunikat może być dowolna, dla przejrzystości zaleca się jednak stosowanie konwencji nazewniczej takiej samej, jak w przypadku struktur dla poszczególnych komunikatów.
W charakterze przykładu stwórzmy metodę obsługującą komunikat WM_PAINT; „obsługa” będzie się tu sprowadzać do wyemitowania krótkiego sygnału dźwiękowego. Deklaracja metody w sekcji private formularza będzie mieć następującą postać:
Procedure WMPaint ( var Msg : TWMPaint ); message WM_PAINT;
Jej implementacja jest prosta:
Procedure WMPaint ( var Msg : TWMPaint );
begin
MessageBeep(0);
inherited;
end;
Instrukcja inherited powoduje przekazanie komunikatu do procedury obsługi w klasie macierzystej — w tym przypadku TForm.
Notatka
Zauważ, że po słowie inherited nie występuje nazwa metody, ponieważ nie chodzi tu o konkretnie nazwaną metodę klasy macierzystej, lecz metodę obsługującą konkretny komunikat.
Na wydruku 3.1 znajduje się kompletny kod projektu ilustrującego opisywaną obsługę komunikatu; na załączonym krążku CD-ROM projekt ten ma nazwę GetMess.dpr.
Wydruk 3.1. Przykład obsługi komunikatu
unit GMMain;
interface
uses
SysUtils, Windows, Messages, Classes, Graphics, Controls,
Forms, Dialogs;
type
TForm1 = class(TForm)
private
procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.WMPaint(var Msg: TWMPaint);
begin
MessageBeep(0);
inherited;
end;
end.
Gdy aplikacja otrzymuje od systemu komunikat WM_PAINT (stanowiący polecenie odświeżenia zawartości formularza) wywoływana jest metoda WMPaint(), generująca krótki sygnał dźwiękowy i przekazująca komunikat do standardowej obsługi.
<ramka>
Generowanie sygnałów dźwiękowych od zawsze stanowiło swoistą namiastkę debuggera, gdyż wyemitowanie określonego wzorca dźwiękowego było świadectwem tego, iż wykonany został określony blok instrukcji. Niezależnie od niebywałego rozwoju technik programistycznych, w ostatnich dwóch dziesięcioleciach ów prymitywny „debugger babuni” nadal jest bardzo atrakcyjny — wywoływanie metody MessageBeep() z różnymi predefiniowanymi parametrami powoduje generowanie rozmaitych dźwięków, poprzez wbudowany głośniczek lub głośniki przyłączone do karty dźwiękowej. Mimo całego swego prymitywizmu ma to wiele niezaprzeczalnych zalet: nie wymaga ustawiania punktów przerwań, nie konsumuje zasobów Windows itp.
Jeżeli (jak autorzy) nie lubisz wpisywać długich nazw funkcji, możesz posłużyć się funkcją Beep() zdefiniowaną w module SysUtils:
{$IFDEF MSWINDOWS}
…
procedure Beep;
begin
MessageBeep(0);
end;
{$ENDIF}
…
{$IFDEF LINUX}
procedure Beep;
var
ch: Char;
begin
ch := #7;
__write(STDOUT_FILENO, ch, 1);
end;
{$ENDIF}
<ramka>
Kontraktowy charakter obsługi komunikatów
W pierwszym rozdziale książki stwierdziliśmy, że operowanie zdarzeniami Delphi ma charakter niekontraktowy (contract-free), co oznacza, iż programista nie ma obowiązku zajmować się zdarzeniami, w związku z którymi aplikacja nie wykonuje żadnych specjalnych czynności.
Z komunikatami rzecz się ma zupełnie inaczej: Windows, wysyłając do aplikacji określony komunikat, oczekuje od niej wykonania związanych z tym komunikatem konkretnych czynności. Na szczęście komponenty biblioteki VCL wykonują to zadanie, a jeżeli użytkownik chce (w ramach definiowanej przez siebie klasy) obsłużyć któryś komunikat w sposób niestandardowy, jedyną jego powinnością wobec kontraktowego charakteru komunikatów jest przekazanie komunikatu do klasy macierzystej (za pomocą instrukcji inherited).
Notatka
Kontraktowość komunikatów to jednak coś więcej niż tylko konieczność zapewnienia im obsługi odziedziczonej z klasy macierzystej; obsługa niektórych komunikatów wiąże się mianowicie z dodatkowymi ograniczeniami — na przykład w ramach obsługi komunikatu WM_KILLFOCUS (w kontekście danej kontrolki) niedozwolone jest przeniesienie --> skupienia [Author:AG] na inną kontrolkę, pod groźbą zawieszenia systemu operacyjnego.
Konsekwencje kontraktowego charakteru obsługi komunikatów możemy poznać bez trudu, usuwając instrukcję inherited z metody WMPaint() formularza projektu GetMess.dpr:
procedure TForm1.WMPaint(var Msg: TWMPaint);
begin
MessageBeep(0);
// inherited;
end;
Takie posunięcie nie daje systemowi operacyjnemu żadnej szansy na przetworzenie komunikatu WM_PAINT, wskutek czego formularz w ogóle nie zostanie wyświetlony. Mało tego — nieustanne wysyłanie do formularza komunikatu WM_PAINT spowoduje serię pisków w głośniku komputera, trwającą aż do zamknięcia lub zminimalizowania formularza.
Zdarza się jednak, iż brak odwołania do odziedziczonej obsługi komunikatu jest efektem zamierzonym — w ten właśnie sposób można np. powstrzymać minimalizację lub maksymalizację okna w odpowiedzi na komunikat WM_SYSCOMMAND.
Zwrotne przekazywanie wyniku obsługi komunikatu
System Windows, kierując do aplikacji różnorodne komunikaty, oczekuje zazwyczaj informacji zwrotnej na temat przebiegu ich obsługi. W kategoriach Delphi informacja ta wpisywana jest w pole Result struktury TMessage i innych struktur związanych z komunikatami. Klasycznym przykładem komunikatu tego typu jest komunikat WM_CTLCOLOR: system Windows oczekuje jako informacji zwrotnej uchwytu „pędzla” (brush) określającego kolor wyświetlanego obiektu. Przy założeniu, iż uchwyt ten przechowywany jest w zmiennej BrushHand, stosowna procedura obsługi komunikatu mogłaby wyglądać następująco:
procedure TForm1.WMCtlColor(var Msg: TWMCtlColor);
begin
inherited;
Msg.Result := BrushHand;
end;
Powyższy przykład ma jednak znaczenie raczej teoretyczne, ponieważ każdy komponent posiada właściwość Color, co uwalnia programistę od bezpośredniej obsługi komunikatu WM_CTLCOLOR.
Zdarzenie OnMessage klasy TApplication
Zdarzenie Application.OnMessage stanowi wygodny środek przechwytywania komunikatów skierowanych do aplikacji. Przypisanie mu procedury obsługi sprawi, że każdy komunikat pobrany z kolejki zostanie przez tę procedurę przechwycony przed skierowaniem go do standardowej obsługi przez Windows. Umożliwia to niestandardową obsługę komunikatów — a wręcz całkowitą kontrolę nad nimi — lecz programista powinien być świadom tego, iż może się to odbić niekorzystnie na ogólnej efektywności aplikacji, bo wspomniane zdarzenie wywoływane jest w odpowiedzi na każdy komunikat umieszczony w kolejce aplikacji. Wymaga to szczególnej efektywności od procedury zdarzeniowej — więc np. ustawienie punktu przerwania w jej wnętrzu byłoby raczej nierozsądne.
Nagłówek procedury obsługującej zdarzenie OnMessage powinien mieć następującą strukturę (TMessageEvent w kategoriach Delphi):
procedure JakiśObiekt.JakaśNazwa ( var Msg : TMsg; var Handled : Boolean);
Wyjściowa wartość drugiego parametru zawiera informację o tym, czy komunikat został obsłużony w całości (True), czy też wymagana jest jeszcze jego standardowa obsługa ze strony Windows (False).
Zdarzeniu Application.OnMessage można przypisać procedurę obsługi w kodzie programu:
Application.OnMessage := Form1.AppMessageHandler;
możemy to również uczynić na etapie projektowania, umieszczając na formularzu komponent TApplicationEvents ze strony Additional palety komponentów i oprogramowując jego zdarzenie OnMessage, na przykład:
var
NumMessages: integer;
…
procedure TForm1.ApplicationEvents1Message(var Nsg: tagMSG; var Handled:Boolean);
begin
Inc(NumMessages);
Handled := False;
end;
W powyższym przykładzie zmienna NumMessages stanowi licznik komunikatów pobieranych z kolejki aplikacji.
Zdarzenie Application.OnMessage nie jest generowane dla komunikatów kierowanych do aplikacji w sposób bezpośredni, z pominięciem kolejki. Więcej informacji na ten temat znajdziesz na początku rozdziału 13. książki „Delphi 4. Vademecum profesjonalisty”.
Wysyłanie własnych komunikatów
Jak Windows przekazuje komunikaty swoim aplikacjom, tak jest możliwe opracowanie własnego systemu komunikatów przesyłanych między komponentami tej samej aplikacji. Delphi oferuje kilka procedur ułatwiających tę czynność: SendMessage(), PostMessage()i Perform()— pierwsze dwie działają na podstawie Win32 API, trzecia niezależnie od niego.
Metoda Perform()
Jest to metoda klasy TControl; służy do bezpośredniego przekazania określonego komunikatu do wskazanej kontrolki. Metoda ta posiada trzy parametry: identyfikator komunikatu i dwie wartości stanowiące jego treść:
Function TControl.Perform(Msg: Cardinal; WParam, LParam: Longint): Longint;
Metoda Perform() tworzy dla komunikatu odpowiedni blok TMessage oraz wywołuje metodę Dispatch(), omijając w ten sposób interfejs Win32 (metoda Dispatch() jest opisana w dalszej części niniejszego rozdziału). Metoda Perform() funkcjonuje w sposób synchroniczny — kończy swe działanie dopiero po całkowitym obsłużeniu komunikatu.
Funkcje SendMessage() i PostMessage()
Metoda Perform() okazuje się jednak całkowicie bezużyteczna, jeżeli chcemy przekazać komunikat do nieznanego obiektu, bądź do okna reprezentowanego jedynie przez uchwyt, należącego do aplikacji nie stworzonej w Delphi. Z pomocą przychodzą wówczas dwie procedury Win32 API: SendMessage() i PostMessage(), zadowalające się jedynie uchwytem (handle) okna docelowego. Obydwie te funkcje są niemal identyczne, jedyna różnica pomiędzy nimi sprowadza się do sposobu przekazania komunikatu: SendMessage(), podobnie jak Perform(), wysyła komunikat bezpośrednio do okna docelowego i oczekuje na zakończenie jego obsługi. PostMessage() — wprost przeciwnie — przekazuje natomiast komunikat do kolejki Windows i natychmiast zwraca sterowanie. Formalna deklaracja opisywanych funkcji wygląda tak:
Function SendMessage(hWnd: HWND; Msg: UINT;
wParam : WPARAM; lParam : LPARAM): LRESULT;stdcall;
Function PostMessage(hWnd: HWND; Msg: UINT;
wParam : WPARAM; lParam : LPARAM): BOOL;stdcall;
Znaczenie ich parametrów jest następujące:
hWnd jest uchwytem okna-adresata,
Msg jest identyfikatorem komunikatu,
wParam i lParam to dwa parametry zawierające treść komunikatu.
Wspomniane funkcje różnią się także pod względem typu i znaczenia zwracanego wyniku: SendMessage() zwraca wartość równą zwrotnej wartości pola Result z rekordu TMsg, natomiast PostMessage() informuje jedynie o umieszczeniu (True), bądź nieumieszczeniu (False) komunikatu w kolejce okna docelowego.
Komunikaty niestandardowe
Oprócz „regularnych” komunikatów Windows, o identyfikatorach rozpoczynających się od WM_, istnieją dwie ważne ich grupy, wymagające odrębnego omówienia: są to komunikaty powiadamiające (notification messages) i komunikaty użytkownika (user-defined messages).
Komunikaty powiadamiające
Komunikaty powiadamiające przesyłane są od okien potomnych (child windows) do okna macierzystego (parent window) w celu poinformowania go o pewnych zdarzeniach. Mają miejsce niemal wyłącznie podczas współpracy ze standardowymi kontrolkami Windows — przyciskami, listami wyboru, listami rozwijanymi, okienkami edycyjnymi itp. „Naciśnięcie” przycisku, przesunięcie suwaka na pasku przewijania, wybranie określonego tekstu — wszystko to staje się źródłem komunikatów powiadamiających.
W środowisku Delphi tego rodzaju komunikaty najczęściej kierowane są ze strony komponentów do formularza, a więc to on powinien zawierać procedury ich obsługi. Tabela 3.2 zawiera zestawienie komunikatów powiadamiających związanych ze standardowymi kontrolkami Win32.
Tabela 5.2. Komunikaty powiadamiające związane ze standardowymi kontrolkami Win32
Symbol |
Znaczenie |
Przycisk |
|
BN_CLICKED |
Kliknięto przycisk |
BN_DISABLE |
Przycisk został zablokowany |
BN_DOUBLECLICKED |
Kliknięto dwukrotnie przycisk |
BN_HILITE |
Przycisk stał się przyciskiem domyślnym |
BN_PAINT |
Należy odświeżyć obraz przycisku |
BN_UNHILITE |
Przycisk przestał być przyciskiem domyślnym |
Lista rozwijana (combo) |
|
CBN_CLOSEUP |
Lista została zamknięta |
CBN_DBLCLK |
Kliknięto dwukrotnie pozycję listy |
CBN_DROPDOWN |
Lista została rozwinięta |
CBN_EDITCHANGE |
Została zmieniona zawartość w oknie edycyjnym |
CBN_EDITUPDATE |
Należy odświeżyć zmieniony tekst w oknie edycyjnym |
CBN_ERRSPACE |
Brak pamięci do wykonania operacji |
CBN_KILLFOCUS |
Lista przestaje być aktywna |
CBN_SELCHANGE |
Wybrano nową listę |
CBN_SELENDCANCEL |
Należy anulować wybór |
CBN_SELENDOK |
Należy zatwierdzić wybór |
CBN_SETFOCUS |
Lista stała się aktywna |
Kontrolka edycyjna |
|
EN_CHANGE |
Zmieniono edytowany tekst |
EN_ERRSPACE |
Brak pamięci do wykonania operacji |
EN_HSCROLL |
Kliknięto poziomy pasek przewijania |
EN_KILLFOCUS |
Kontrolka edycyjna przestała być aktywna |
EN_MAXTEXT |
Po wstawieniu znaku tekst wykracza poza kontrolkę edycyjną |
EN_SETFOCUS |
Kontrolka edycyjna staje się aktywna |
EN_UPDATE |
Należy odświeżyć wyświetlany tekst w kontrolce edycyjnej |
EN_VSCROLL |
Kliknięto pionowy pasek przewijania |
Lista wyboru |
|
LBN_DBLCLK |
Dwukrotne kliknięcie pozycji listy |
LBN_ERRSPACE |
Brak pamięci do wykonania operacji |
LBN_KILLFOCUS |
Lista przestaje być aktywna |
LBN_SELCANCEL |
Anulowano wybór |
LBN_SELCHANGE |
Przesunięto wybór na inną pozycję |
LBN_SETFOCUS |
Lista wyboru staje się aktywna |
Wewnętrzne komunikaty VCL
Komponenty biblioteki VCL posiadają rozbudowaną kolekcję komunikatów wewnętrznych; komunikaty te nie są raczej przydatne w procesie budowania aplikacji, jednak ich znajomość jest nieodzowna dla projektanta nowych komponentów. Identyfikatory tych komunikatów rozpoczynają się od przedrostka CM_ (Component Message) lub CN_ (Component Notification), a komunikaty te wykorzystywane są do wewnętrznego ustawiania aktywności, kolorów, widzialności, odtwarzania okien, przeciągania komponentów itp. Ich kompletna lista znajduje się w systemie pomocy, w części poświęconej tworzeniu komponentów.
Jednym z przykładów zastosowania komunikatów na wewnętrzne potrzeby komponentów VCL jest wykrywanie sytuacji, gdy kursor myszy wchodzi w obszar danej kontrolki lub obszar ten opuszcza — generowane są wówczas komunikaty (odpowiednio) CM_MOUSEENTER i CM_MOUSELEAVE. Rozpatrzmy w charakterze przykładu następujący fragment definicji komponentu:
TSpecialPanel = class(TPanel)
protected
procedure CMMouseEnter(var Msg: TMessage); message CM_MOUSEENTER;
procedure CMMouseLeave(var Msg: TMessage); message CM_MOUSELEAVE;
end;
…
procedure TSpecialPanel.CMMouseEnter(var Msg: TMessage);
begin
inherited;
Color := clWhite;
end;
procedure TSpecialPanel.CMMouseLeave(var Msg: TMessage);
begin
inherited;
Color := clBtnFace;
end;
Komponent TSpecialPanel reaguje na wniknięcie kursora w jego obszar przyjmując kolor biały; po przesunięciu kursora poza jego obszar wraca do standardowego koloru clBtnFace. Możesz to zaobserwować, uruchamiając przykładowy projekt CustMessage.dpr znajdujący się na załączonym krążku CD-ROM.
Komunikaty definiowane przez użytkownika
Często zdarza się, iż aplikacja chce poinformować inne aplikacje o pewnym zdarzeniu, bądź też chcą się ze sobą skomunikować obiekty tej samej aplikacji — chodzi tu o sytuacje charakterystyczne z punktu widzenia danej aplikacji. Wielce pomocne mogą się wówczas okazać komunikaty.
W przypadku komunikujących się obiektów tej samej aplikacji można by postawić pytanie „po co w ogóle posługiwać się komunikatami, czyż nie wystarczy wprost wywołać odpowiednią metodę obiektu docelowego?”
Dobre pytanie — i kilka równie dobrych odpowiedzi. Otóż, po pierwsze, przekazanie komunikatu do obiektu jest znacznie mniej krępującym sposobem komunikacji niż wywołanie jego metody — po wysłaniu komunikatu można kontynuować pracę i nie przejmować się tym, co stanie się z wysłanym komunikatem. Drugim ważnym powodem jest swoisty „polimorficzny” charakter komunikatów — wysyłając określony komunikat pod adresem danego obiektu, nie musimy znać szczegółów jego definicji w kategoriach syntaktycznych; przy bezpośrednim wywoływaniu metody znajomość taka byłaby nieodzowna. Trzecia przyczyna związana jest natomiast z komunikatami rozgłaszającymi (o nich za chwilę) — pojedynczy komunikat tego typu zauważalny jest dla wielu adresatów, nie znanych nawet dokładnie obiektowi-nadawcy; bezpośrednie wywoływanie metod obiektów-adresatów, jeżeli nawet dałoby się zrealizować, wymagałoby skomplikowanych iteracji. Wreszcie — obsługa komunikatu nie musi być obowiązkowa: jeżeli obiektu-adresata nie wiążą z danym komunikatem żadne szczególne zadania, nie musi on nawet definiować obsługującej go metody.
Przekonawszy się więc o celowości operowania komunikatami, przyjrzyjmy się dokładniej ich poszczególnym kategoriom.
Wewnętrzne komunikaty aplikacji
Jest to dość nieskomplikowana kategoria komunikatów. Do ich przesyłania służą poznane już funkcje SendMessage(), PostMessage() i Perform(), a jako identyfikatory można wykorzystywać wartości od WM_USER+100 do $7FFF (ten właśnie zakres system Windows rezerwuje dla komunikatów użytkownika). Oto prosty przykład:
const
sx_MyMessage = wm_User+100;
begin
SomeForm.Perform (sx_MyMessage, 0, 0);
{ lub }
SendMessage (SomeForm.Handle, sx_MyMessage, 0, 0);
{ lub }
PostMessage (SomeForm.Handle, sx_MyMessage, 0, 0);
....
end;
Formularz SomeForm należy oczywiście wyposażyć w stosowną procedurę obsługi komunikatu sx_Message:
TForm1 = class(TForm)
...
private
procedure SXMyMessage(var Msg: TMessage);
message sx_MyMessage;
end;
....
....
Procedure TForm1.SXMyMessage(var Msg: TMessage);
begin
MessageDlg('Ona zamieniła mnie w żabę!', mtInformation, [mbOK], 0);
end;
Obsługa komunikatów użytkownika nie różni się więc zbytnio od obsługi standardowych komunikatów Windows. Jest rzeczą zrozumiałą, iż komunikujące się aplikacje (lub poszczególne komponenty danej aplikacji) muszą stosować jednolitą konwencję numeracji komunikatów, zaś dla przejrzystości i czytelności kodu należy na oznaczenie poszczególnych komunikatów wybierać nazwy powiązane ze spełnianymi przez nie funkcjami.
Ostrzeżenie
Nie wysyłaj nigdy komunikatu z zakresu WM_USER ÷ WM_USER+$7F, chyba że masz gwarancję, iż obiekt docelowy obsłuży go zgodnie z Twoimi oczekiwaniami. Ponieważ każde okno może posługiwać się komunikatami z tego zakresu niezależnie od pozostałych okien, nietrudno o kolizję identyfikatorów.
Komunikaty międzyaplikacyjne
Narzucenie jednolitej konwencji numerowania komunikatów w przypadku współpracy różnych aplikacji, stworzonych często przez różnych autorów, jest zadaniem trudnym, a czasem wręcz niewykonalnym. Ponadto, na etapie projektowania aplikacji niemożliwe jest wybranie numerów komunikatów nie kolidujących z innymi aplikacjami, leżącymi poza sferą naszego zainteresowania. Stąd pomysł, aby porozumiewające się aplikacje używały do identyfikacji komunikatów nie numerów, lecz napisów (łańcuchów znaków); potrzebny byłby jeszcze tylko globalny mechanizm odwzorowujący jednakowe napisy w jednakowe identyfikatory numeryczne, nie kolidujące z innymi, już wykorzystywanymi w innych aplikacjach. Mechanizmu takiego dostarcza funkcja RegisterWindowMessage(). Posiada jeden parametr, będący łańcuchem z zerowym ogranicznikiem zawierającym nazwę komunikatu, a jej wynikiem jest (nadany przez Windows) identyfikator tegoż komunikatu, z zakresu $C000 ÷ $FFFF. Funkcja ta gwarantuje, że w ramach tej samej sesji Windows identyczne napisy będą odwzorowywane w identyczne identyfikatory.
Z komunikatami, których identyfikatory uzyskiwane są za pomocą funkcji RegisterWindowMessage() wiąże się jednak pewna subtelna trudność. Otóż, jak sobie zapewne przypominasz, w definicji metody obiektu obsługującej komunikat, po słowie message występuje konkretny identyfikator komunikatu, ergo — ten sposób obsługi daje się zastosować wyłącznie do komunikatów, których identyfikatory znane są już w momencie kompilacji. Komunikaty o dynamicznie uzyskiwanych identyfikatorach muszą być obsługiwane w inny sposób — mianowicie poprzez przedefiniowanie procedury okienkowej (subclassing) lub w ramach domyślnej procedury obsługi, jaką jest metoda DefaultHandler()obiektu. Zagadnienie to zostało szczegółowo opisane w 13. rozdziale „Delphi 4. Vademecum profesjonalisty”.
Notatka
Identyfikator zwracany przez funkcję RegisterWindowMessage() ma znaczenie lokalne dla bieżącej sesji Windows — w następnej sesji funkcja ta może zwrócić dla tego samego parametru inny identyfikator. Nie ma więc żadnego sensu używanie tego identyfikatora na etapie projektowania.
Komunikaty rozgłaszające
Komunikaty rozgłaszające (broadcasts) skierowane są do wszystkich kontrolek potomnych (child) danej kontrolki. Do ich rozsyłania służy metoda Broadcast() klasy TWinControl. Oto przykład wysłania komunikatu UM_FOO do wszystkich kontrolek zawartych w panelu Panel1:
var
M : TMessage;
begin
with M do
begin
Message := um_Foo;
wParam := 0;
lParam := 0;
Result := 0;
end;
Panel1.Broadcast (M);
end;
Anatomia systemu komunikatów VCL
Dla programisty korzystającego z komponentów VCL metoda obiektu, zadeklarowana z użyciem klauzuli message jest jedynym widocznym miejscem, w którym odbywa się obsługa komunikatu. Metoda ta jest jednakże tylko jednym z etapów, przez które przejść musi komunikat na swej „drodze życiowej” — pozostałe etapy skrywają się wewnątrz procedur implementacyjnych biblioteki VCL. Mimo ich „skrytego” charakteru, dobrze jest mieć świadomość ich istnienia, zaś dla zaawansowanych programistów szczegóły obsługi komunikatów trafiających do metod obiektów z pewnością okażą się interesujące.
A więc: dla komunikatów wysyłanych za pomocą PostMessage() — nazwijmy je komunikatami kolejkowanymi — pierwszym „przystankiem” jest metoda Application. ProcessMessage(), cyklicznie wywoływana w ramach metody Application.ProcessMessages(), aż do wyczerpania kolejki wejściowej:
procedure TApplication.ProcessMessages;
var
Msg: TMsg;
begin
while ProcessMessage(Msg) do {loop};
end;
Istotą metody ProcessMessage() jest wygenerowanie zdarzenia OnMessage:
function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Handled: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
begin
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False;
if Assigned(FOnMessage)
then
FOnMessage(Msg, Handled);
if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and
not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end
else
FTerminate := True;
end;
end;
Jak łatwo zauważyć, zdarzenie to nie jest generowane dla komunikatu WM_QUIT. Jeżeli komunikat nie został całkowicie obsłużony w ramach zdarzenia OnMessage (Handled = False), jest on (pod pewnymi warunkami) kierowany do funkcji DispatchMessage() wywołującej funkcję StdWndProc(); ta ostatnia kieruje komunikat do obiektu docelowego:
function StdWndProc(
Window: HWND;
Message, WParam: Longint;
LParam: Longint): Longint; stdcall; assembler;
asm
XOR EAX,EAX
PUSH EAX
PUSH LParam
PUSH WParam
PUSH Message
MOV EDX,ESP
MOV EAX,[ECX].Longint[4]
CALL [ECX].Pointer
ADD ESP,12
POP EAX
end;
Komunikaty wysyłane za pośrednictwem SendMessage() — nazwijmy je komunikatami bezpośrednimi — nie trafiają do kolejki wejściowej. Funkcja SendMessage() przekazuje je bezpośrednio do funkcji StdWndProc() — jest więc jasne, że „omijają” one metodę ProcessMessage() i wobec tego nie powodują wystąpienia zdarzenia OnMessage().
Począwszy od funkcji StdWndProc(), dalszy los komunikatu jest już niezależny od sposobu jego wygenerowania (PostMessage() czy SendMessage()). Miejscem, do którego przekazała go procedura StdWndProc(), jest metoda MainWndProc() obiektu docelowego, zdefiniowana w klasie TWinControl:
procedure TWinControl.MainWndProc(var Message: TMessage);
begin
try
try
WindowProc(Message);
finally
FreeDeviceContexts;
FreeMemoryContexts;
end;
except
Application.HandleException(Self);
end;
end;
Właściwość WindowProc reprezentuje procedurę, której adres znajduje się w polu FWindowProc kontrolki:
property WindowProc: TWndMethod read FWindowProc write FWindowProc;
Domyślnie pole to zawiera wskazanie na metodę WndProc() dokonującą standardowej dla VCL obsługi komunikatu; wskazanie to jest mu przypisywane w konstruktorze kontrolki:
constructor TControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FWindowProc := WndProc;
…
end;
Jeżeli więc programista będzie chciał zmienić standardowy sposób obsługi komunikatu przez daną kontrolkę, może to uczynić zmieniając właściwości WindowProc.
Metoda WndProc(), po wstępnym przetworzeniu komunikatu, kieruje go do metody Dispatch():
procedure TControl.WndProc(var Message: TMessage);
var
Form: TCustomForm;
KeyState: TKeyboardState;
WheelMsg: TCMMouseWheel;
begin
if (csDesigning in ComponentState) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and (Form.Designer <> nil) and
Form.Designer.IsDesignMsg(Self, Message) then Exit
end;
if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit;
end
else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then
begin
if not (csDoubleClicks in ControlStyle) then
case Message.Msg of
WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK:
Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
end;
case Message.Msg of
WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message);
WM_LBUTTONDOWN, WM_LBUTTONDBLCLK:
begin
if FDragMode = dmAutomatic then
begin
BeginAutoDrag;
Exit;
end;
Include(FControlState, csLButtonDown);
end;
WM_LBUTTONUP:
Exclude(FControlState, csLButtonDown);
else
with Mouse do
if WheelPresent and (RegWheelMessage <> 0) and
(Message.Msg = RegWheelMessage) then
begin
GetKeyboardState(KeyState);
with WheelMsg do
begin
Msg := Message.Msg;
ShiftState := KeyboardStateToShiftState(KeyState);
WheelDelta := Message.WParam;
Pos := TSmallPoint(Message.LParam);
end;
MouseWheelHandler(TMessage(WheelMsg));
Exit;
end;
end;
end
else if Message.Msg = CM_VISIBLECHANGED then
with Message do
SendDockNotification(Msg, WParam, LParam);
Dispatch(Message);
end;
Ostatecznie, metoda Dispatch() przekazuje komunikat do przeznaczonej dla niego metody obiektu:
procedure TObject.Dispatch(var Message);
asm
PUSH ESI
MOV SI,[EDX]
OR SI,SI
JE @@default
CMP SI,0C000H
JAE @@default
PUSH EAX
MOV EAX,[EAX]
CALL GetDynaMethod
POP EAX
JE @@default
MOV ECX,ESI
POP ESI
JMP ECX
@@default:
POP ESI
MOV ECX,[EAX]
JMP dword ptr [ECX].vmtDefaultHandler
end;
Pierwsze dwa bajty struktury Message zawierają identyfikator komunikatu. Metoda Dispatch() sprawdza wpierw, czy jest to identyfikator zerowy — jeżeli tak, to komunikat kierowany jest do obsługi domyślnej przez metodę DefaultHandler(). Kolejny test polega na porównaniu identyfikatora komunikatu z wartością $C000 jako dolną granicą identyfikatorów generowanych przez funkcję RegisterWindowMessage(); jeżeli identyfikator należy do tej właśnie kategorii, od razu kierowany jest do obsługi domyślnej — jak przed chwilą zaznaczyliśmy, identyfikatory z tego zakresu nie mają sensu na etapie projektowania, nie da się im więc przypisać metody z klauzulą message.
Z punktu widzenia wewnętrznych mechanizmów Object Pascala, metoda z klauzulą message jest metodą dynamiczną, a identyfikator komunikatu jest jej indeksem. Odnalezienie żądanej metody dynamicznej obiektu realizowane jest przez funkcję systemową GetDynaMethod(). Otrzymuje ona w rejestrze EAX wartość identyfikującą klasę obiektu (czyli wskaźnik do tablicy VMT), zaś w rejestrze SI — indeks metody; adres żądanej metody zwracany jest w rejestrze ESI (o ile wyzerowana jest flaga ZF). Po powrocie z metody obsługującej komunikat przekazywany jest do obsługi domyślnej. Jeżeli metoda dynamiczna o żądanym indeksie nie istnieje (flaga ZF jest ustawiona), komunikat kierowany jest do obsługi domyślnej od razu.
W ten oto sposób komunikat, z chwilą przekazania go do dedykowanej mu metody obiektu, wymyka się spod kontroli systemu operacyjnego; jeżeli więc ma on być przekazany do obsługi odziedziczonej (inherited), przekazania tego musi dokonać wspomniana metoda.
Ponadto, jak już wcześniej powiedzieliśmy, komunikaty o identyfikatorach wygenerowanych przez metodę RegisterWindowMessage() trafiają od razu do metody DefaultHandler(); jest ona więc najbardziej odpowiednim miejscem do obsługi tego typu komunikatów.
Opisaną wędrówkę komunikatu w ramach biblioteki VCL przedstawia schematycznie rysunek 3.2.
Rysunek 3.2. Obsługa komunikatu w ramach biblioteki VCL
W celu ilustracji obsługi komunikatu na poszczególnych etapach skonstruowaliśmy przykładowy projekt o nazwie CatchIt; jego formularz główny jest przedstawiony na rysunku 3.3.
Rysunek 3.3. Formularz główny projektu CatchIt.dpr
Projekt umożliwia przesłanie komunikatu do formularza za pomocą metody PostMessage() albo SendMessage() w wyniku kliknięcia jednego z przycisków. Sposób przesłania komunikatu identyfikowany jest w jego polu wParam — 1 dla PostMessage() i 0 dla SendMessage():
procedure TMainForm.PostMessButtonClick(Sender: TObject);
{ zakolejkuj komunikat dla formularza }
begin
PostMessage(Handle, SX_MYMESSAGE, 1, 0);
end;
procedure TMainForm.SendMessButtonClick(Sender: TObject);
{ wyślij komunikat bezpośrednio do formularza }
begin
SendMessage(Handle, SX_MYMESSAGE, 0, 0);
end;
Projekt ten stwarza okazję do zakończenia obsługi komunikatu („zjedzenia go”, jak piszą autorzy oryginału) na każdym z czterech etapów — w obsłudze zdarzenia OnMessage, w metodzie WndProc(), w dedykowanej metodzie obsługi i w metodzie DefaultHandler(); owo „zjedzenie” polega po prostu na zaniechaniu jego odziedziczonej obsługi. Kod źródłowy formularza głównego projektu został przedstawiony na wydruku 3.2.
Wydruk 3.2. Kod formularza głównego projektu CatchIt.dpr
unit CIMain;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls, Menus;
const
SX_MYMESSAGE = WM_USER; // komunikat definiowany przez użytkownika
MessString = '%s komunikat jest teraz w %s.';
type
TMainForm = class(TForm)
GroupBox1: TGroupBox;
PostMessButton: TButton;
WndProcCB: TCheckBox;
MessProcCB: TCheckBox;
DefHandCB: TCheckBox;
SendMessButton: TButton;
AppMsgCB: TCheckBox;
EatMsgCB: TCheckBox;
EatMsgGB: TGroupBox;
OnMsgRB: TRadioButton;
WndProcRB: TRadioButton;
MsgProcRB: TRadioButton;
DefHandlerRB: TRadioButton;
procedure PostMessButtonClick(Sender: TObject);
procedure SendMessButtonClick(Sender: TObject);
procedure EatMsgCBClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure AppMsgCBClick(Sender: TObject);
private
{ obsługa komunikatu na szczeblu aplikacji }
procedure OnAppMessage(var Msg: TMsg; var Handled: Boolean);
{ obsługa komunikatu w metodzie okienkowej }
procedure WndProc(var Msg: TMessage); override;
{ obsługa komunikatu przez metodę obiektu }
procedure SXMyMessage(var Msg: TMessage); message SX_MYMESSAGE;
{ domyślna obsługa komunikatu }
procedure DefaultHandler(var Msg); override;
end;
var
MainForm: TMainForm;
implementation
{$R *.DFM}
const
// Łańcuch określający sposób wysłania komunikatu
SendPostStrings: array[0..1] of String = ('Wysłany', 'Zakolejkowany');
procedure TMainForm.FormCreate(Sender: TObject);
begin
// przypisz metodę obsługi na szczeblu aplikacji
Application.OnMessage := OnAppMessage;
// wykorzystaj właściwość Tag pól wyboru do skojarzenia ich z opcjami
// w drugiej sekcji
AppMsgCB.Tag := Longint(OnMsgRB);
WndProcCB.Tag := Longint(WndProcRB);
MessProcCB.Tag := Longint(MsgProcRB);
DefHandCB.Tag := Longint(DefHandlerRB);
// wykorzystaj właściwość Tag opcji do skojarzenia ich z polami wyboru
// w pierwszej sekcji
OnMsgRB.Tag := Longint(AppMsgCB);
WndProcRB.Tag := Longint(WndProcCB);
MsgProcRB.Tag := Longint(MessProcCB);
DefHandlerRB.Tag := Longint(DefHandCB);
end;
procedure TMainForm.OnAppMessage(var Msg: TMsg; var Handled: Boolean);
begin
// sprawdź, czy to ten komunikat:
if Msg.Message = SX_MYMESSAGE then
begin
if AppMsgCB.Checked then
begin
// wyświetl komunikat o aktualnym etapie obsługi i ustaw
// odpowiednią flagę
ShowMessage(Format(MessString, [SendPostStrings[Msg.WParam],
'Application.OnMessage']));
Handled := OnMsgRB.Checked;
end;
end;
end;
procedure TMainForm.WndProc(var Msg: TMessage);
{ procedura okienkowa formularza }
var
CallInherited: Boolean;
begin
CallInherited := True; // załóż a priori obsługę odziedziczoną
if Msg.Msg = SX_MYMESSAGE then // czy to ten komunikat ?
begin
if WndProcCB.Checked then // gdy zaznaczono odpowiednie pole wyboru
begin
// wyświetl komunikat o aktualnym etapie obsługi i ustaw
// odpowiednią flagę
ShowMessage(Format(MessString, [SendPostStrings[Msg.WParam], 'WndProc()']));
// wywołaj obsługę odziedziczoną, jeśli użytkownik nie postanowił
// zakończyć obsługi na niniejszym etapie
CallInherited := not WndProcRB.Checked;
end;
end;
if CallInherited then inherited WndProc(Msg);
end;
procedure TMainForm.SXMyMessage(var Msg: TMessage);
{ metoda obsługi }
var
CallInherited: Boolean;
begin
CallInherited := True; // załóż a priori obsługę odziedziczoną
if MessProcCB.Checked then // gdy zaznaczono odpowiednie pole wyboru
begin
// wyświetl komunikat o aktualnym etapie obsługi i ustaw
// odpowiednią flagę
ShowMessage(Format(MessString, [SendPostStrings[Msg.WParam],
'metodzie obsługującej']));
// wywołaj obsługę odziedziczoną, jeśli użytkownik nie postanowił
// zakończyć obsługi na niniejszym etapie
CallInherited := not MsgProcRB.Checked;
end;
if CallInherited then Inherited;
end;
procedure TMainForm.DefaultHandler(var Msg);
{ domyślna obsługa }
var
CallInherited: Boolean;
begin
CallInherited := True; // załóż a priori obsługę odziedziczoną
if TMessage(Msg).Msg = SX_MYMESSAGE then // czy to ten komunikat?
begin
if DefHandCB.Checked then // gdy zaznaczono odpowiednie pole wyboru
begin
// wyświetl komunikat o aktualnym etapie obsługi i ustaw
// odpowiednią flagę
ShowMessage(Format(MessString, [SendPostStrings[TMessage(Msg).WParam],
'DefaultHandler()']));
// wywołaj obsługę odziedziczoną, jeśli użytkownik nie postanowił
// zakończyć obsługi na niniejszym etapie
CallInherited := not DefHandlerRB.Checked;
end;
end;
if CallInherited then inherited DefaultHandler(Msg);
end;
procedure TMainForm.PostMessButtonClick(Sender: TObject);
{ zakolejkuj komunikat dla formularza }
begin
PostMessage(Handle, SX_MYMESSAGE, 1, 0);
end;
procedure TMainForm.SendMessButtonClick(Sender: TObject);
{ wyślij komunikat bezpośrednio do formularza }
begin
SendMessage(Handle, SX_MYMESSAGE, 0, 0);
end;
procedure TMainForm.AppMsgCBClick(Sender: TObject);
{ zadecyduj o dostępności opcji drugiej sekcji w ogólności }
begin
if EatMsgCB.Checked then
begin
with TRadioButton((Sender as TCheckBox).Tag) do
begin
Enabled := TCheckbox(Sender).Checked;
if not Enabled then Checked := False;
end;
end;
end;
procedure TMainForm.EatMsgCBClick(Sender: TObject);
{ zadecyduj o dostępności poszczególnych opcji w drugiej sekcji }
var
i: Integer;
DoEnable, EatEnabled: Boolean;
begin
// czy dostępne w ogólności ?
EatEnabled := EatMsgCB.Checked;
// iteracja po kontrolkach potomnych drugiej sekcji
// i ustawianie ich dostępności stosownie do stanu
// pól wyboru w pierwszej sekcji
for i := 0 to EatMsgGB.ControlCount - 1 do
with EatMsgGB.Controls[i] as TRadioButton do
begin
DoEnable := EatEnabled;
if DoEnable then DoEnable := TCheckbox(Tag).Checked;
if not DoEnable then Checked := False;
Enabled := DoEnable;
end;
end;
end.
Notatka
Zwróć uwagę, iż w ramach metod WndProc() i DefaultHandler() występuje nazwa metody; w tym przypadku „obsługa odziedziczona” polega po prostu na wywołaniu odziedziczonej metody z klasy macierzystej.
Jak zapewne zauważyłeś, metoda DefaultHandler() posiada parametr amorficzny; jest to zrozumiałe, gdyż musi być ona przygotowana na każdą postać rekordu komunikatu: jedynym czynionym przez nią założeniem jest to, że pierwsze dwa bajty rekordu zawierają identyfikator komunikatu. Odwoływanie się do poszczególnych pól komunikatu wymaga jego rzutowania na odpowiedni typ, na przykład TMessage.
Związek komunikatów ze zdarzeniami Delphi
Wspominaliśmy już niejednokrotnie, iż większość zdarzeń VCL to „wysokopoziomowe” otoczki komunikatów Windows. Wybrane przykłady zdarzeń generowanych przez odpowiadające im komunikaty przedstawia tabela 3.3.
Tabela 3.3. Niektóre zdarzenia VCL i odpowiadające im komunikaty Windows
Zdarzenie |
Komunikat(y) |
OnActivate |
WM_ACTIVATE |
OnClick |
WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN |
OnCreate |
WM_CREATE |
OnDblClick |
WM_XBUTTONDBLCLICK |
OnKeyDown |
WM_KEYDOWN |
OnKeyPress |
WM_CHAR |
OnKeyUp |
WM_KEYUP |
OnPaint |
WM_PAINT |
OnResize |
WM_SIZE |
OnTimer |
WM_TIMER |
Wskazówka
Należy unikać bezpośredniego obsługiwania komunikatu w sytuacji, gdy w VCL istnieje odpowiadające mu zdarzenie. Ze względu na niekontraktowy charakter obsługi, zdarzenia są mechanizmem pewniejszym i bezpieczniejszym niż komunikaty Windows.
Podsumowanie
Delphi, oferując wygodny mechanizm zdarzeń, nie uniemożliwia bynajmniej bezpośredniego operowania komunikatami, koniecznego w niektórych sytuacjach. W niniejszym rozdziale przedstawiliśmy więc ogólną naturę komunikatów Windows oraz szczegóły ich obsługi w ramach biblioteki VCL. Z komunikatami będziemy spotykać się jeszcze wielokrotnie, w tym i następnym tomie niniejszej książki; nie należy jednak zapominać, iż są one mechanizmem charakterystycznych dla systemu Windows i nie znajdują zastosowania w aplikacjach międzyplatformowych, używających innych sposobów komunikowania się, opisanych w rozdziale 13.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 C:\HELION\DELPHI6VP\POLISH-WORK\03-komunikaty.doc
Ta kontrolka aplikacji, do której kierowane są zdarzenia pochodzące od klawiatury, nazywa się kontrolką skupioną (focused control) , posiada on bowiem pewien wyróżniony stan, zwany skupieniem (focus); w danej chwili tylko jedna kontrolka może posiadać skupienie; system operacyjny, „wywłaszczając' kontrolkę ze stanu skupienia (na rzecz innej kontrolki) wysyła do niej komunikat WM_KILLFOCUS. Kontrolka to powinna wóczas jedynie przyjąć do wiadomości, iż pozbawia się ją skupienia, nie czyniąc z tym skupieniem niczego więcej, w szczególności — nie próbując przenieść go na inną kontrolkę (weszłaby wóczas „w drogę” systemowi operacyjnemu).