Umíme to s Delphi: 65. díl – Delphi a zprávy systému Windows, 2. část

Před týdnem jsme se začali zabývat mechanismem zpráv systému Windows. Vysvětlili jsme si podstatu zpráv, jejich význam a způsob implementace. Dnes uvidíme, jak zpracovávat příchozí zprávy i na úrovni celé aplikace či naopak na úrovni jednotlivé komponenty. Kromě toho si vysvětlíme důležitý rozdíl mezi vlastnictvím komponenty a jejím rodičovstvím (mezi vlastnostmi Owner a Parent).
Jen připomeňme, že v příkladu z minulého dílu odchytil formulář zprávu WM_KEYDOWN a jako reakci na její přijetí zmenšil sám sebe o 10 pixelů v obou rozměrech. V závěru článku jsme si nastolili otázku, jak zařídit, abychom mohli zprávy odchytávat i v jiné komponentě než ve formuláři, například v editačním poli.

Řekli jsme si totiž, že obslužná procedura musí být implementována jako metoda nějaké třídy, tedy pravděpodobně třídy formuláře.

Vytvořme si tedy opět fiktivní problém (s nímž bychom si ovšem snadno poradili i beze zpráv:-)), jehož řešením si ukážeme vše podstatné. Dejme tomu, že umístíme na formulář (Form1) editační pole a budeme požadovat, aby tato komponenta fungovala úplně stejně jako komponenta Edit, jen s tím rozdílem, že stiskne-li uživatel při psaní textu klávesu Esc, dojde k ukončení aplikace.

Ukážeme si jedno z možných řešení, které je velmi snadné, přestože v principu budeme vytvářet novou komponentu. Pro účely tohoto programu vytvoříme komponentu TMujEdit, která bude potomkem komponenty TCustomEdit (což je bezprostřední předek třídy TEdit určený právě k odvozování komponent „podobných“ komponentě Edit). Jediným rozdílem bude, že tato třída bude odchytávat příjem zprávy WM_KEYDOWN.

1. Nejprve je nutné vytvořit deklaraci třídy TMujEdit:

type
  TMujEdit = class(TCustomEdit)
  private
    procedure WMKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;
  end;

Tímto programovým kódem jsme zajistili, že třídu TMujEdit budou „zajímat“ zprávy WM_KEYDOWN a pokud nějaká přijde, bude provedena procedura WMKeyDown.

2. Dále musíme zajistit, že se komponenta po spuštění aplikace zobrazí na formuláři. Ošetříme tedy událost OnCreate formuláře Form1:

procedure TForm1.FormCreate(Sender: TObject);
var SE: TMujEdit;
begin
  SE := TMujEdit.Create(Self);
  SE.Parent := Form1;
  SE.Visible := True;
  SE.Left := 50;
  SE.Top := 50;
  SE.Show;
end;

Je důležité nastavit rodiče nového editačního pole, aby „vědělo“, kde se zobrazit. Mimochodem, nejsem si jist, zda jsme se již v seriálu zabývali rozdílem mezi „rodičovstvím“ komponenty a jejím „vlastnictvím“ (tedy mezi vlastnostmi Owner a Parent). Protože se jedná o poměrně významnou záležitost, učiníme nyní cimrmanovský úkrok stranou a vysvětlíme si obě vlastnosti.

Kdo je rodič, kdo je majitel?

Při práci v Delphi jste již pravděpodobně zaregistrovali, že většina ovládacích prvků a komponent disponuje vlastnostmi Owner a Parent. Při běžné práci se o ně prakticky nestaráme, ale v okamžiku, kdy se pustíme do nějaké pokročilejší operace, bychom měli rozdíl rozpoznat. V tomto okamžiku bych si dovolil volně parafrázovat větu z knihy mého oblíbeného profesora: „Kdo z vážených čtenářů nehodlá rozlišovat mezi vlastnostmi Owner a Parent, ať se věnuje raději včelařství, nikoliv však programování v Delphi.“ :-)

Rodičem komponenty A (A.Parent) je taková komponenta, na níž se komponenta A zobrazí. Rodičem všech komponent na formuláři je tedy formulář, leda že by „mezi“ nimi a formulářem byl třeba ještě panel – ten by pak byl jejich rodičem. Nastavení vlastnosti Parent se provede buď automaticky (při práci v návrhové fázi) nebo je možné programově.

Vlastníkem komponenty A (A.Owner) je komponenta, která je zodpovědná za „likvidaci“ komponenty A z paměti. Jinak řečeno – zanikne-li komponenta B, dojde také k uvolnění všech komponent, které komponenta B vlastnila. Vlastníka opět nemusíme řešit při práci v návrhové fázi. Vytváříme-li komponentu C programově, jejího vlastníka stanovujeme parametrem konstruktoru Create:

  C := TEdit.Create(Form1);   // vlastníkem bude formulář Form1

Zpět ke zprávám

Vraťme se však k našemu příkladu se zprávami a editačním polem.

3. Posledním krokem je implementace obslužné procedury WMKeyDown:

procedure TMujEdit.WMKeyDown(var Msg: TWMKeyDown);
begin
  if (Msg.CharCode = VK_ESCAPE) then
    Application.Terminate
  else
    inherited;
end;

Není-li vám tělo této metody jasné, doporučuji vrátit se k minulému dílu seriálu: najdete v něm vysvětlení parametru Msg (včetně položky CharCode), stejně jako popis významu klíčového slova inherited.

Po spuštění aplikace bude na formuláři normální editační pole, které však ukončí aplikaci po stisku klávesy Esc. Zdůrazněme, že Esc musí být stisknuto ve chvíli, kdy má editační pole zaměření, neboť žádná jiná komponenta (ani formulář) událost WM_KEYDOWN neodchytává! Je asi zbytečné dodávat, že tímto jednoduchým postupem jsme si elegantně připravili půdo pro to, aby komponenta TMujEdit mohla odchytávat jakékoliv zprávy, které nás zajímají. Pro jistotu si uveďme také kompletní zdrojový kód:

unit prjZpravy;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, AppEvnts;

type
  TMujEdit = class(TCustomEdit)
  private
    procedure WMKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;
  end;

  TForm1 = class(TForm)
    Edit1: TEdit;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  SpecEdit: TEdit;

implementation

{$R *.dfm}

procedure TMujEdit.WMKeyDown(var Msg: TWMKeyDown);
begin
  if (Msg.CharCode = VK_ESCAPE) then
    Application.Terminate
  else
    inherited;
end;


procedure TForm1.FormCreate(Sender: TObject);
var SE: TMujEdit;
begin
  SE := TMujEdit.Create(Form1);
  SE.Parent := Form1;
  SE.Visible := True;
  SE.Left := 50;
  SE.Top := 50;
  SE.Show;
end;

end.

Application.OnMessage

Dalším důležitým článkem při úvahách o zprávách je samozřejmě událost OnMessage objektu Application. Protože jsme se jí dosud v seriálu patřičně nezabývali, pustíme se do popisu.

Tato událost je generována vždy, když aplikace obdrží od systému Windows jakoukoliv zprávu. Hned v tomto okamžiku tedy stojí za to dodat, že ať bude naším cílem cokoliv, nikdy bychom toho neměli dosahovat umístěním časově náročnějšího kódu do obsluhy této události. Důvod? Obsluha události OnMessage je volána v případě, že jakákoliv okenní komponenta obdrží nějakou zprávu; jedná se tedy o jednu z nejvytíženějších procedur v aplikaci.

Obsluha události OnMessage se o zprávě „dozví“ samozřejmě ještě dříve, než její konkrétní adresát. Zprávy, které nejsou odchyceny v OnMessage, „projdou“ dál do svého místa určení. Je ovšem možné ošetřit zprávu v OnMessage a zajistit, aby ji dostal i původní příjemce. Všechno si ukážeme na příkladu.

Dodejme, že hlavní přínos této události oceníme až v okamžiku, kdy se naučíme vytvářet a posílat své vlastní, uživatelské zprávy. Do toho okamžiku totiž bude všechno jen hrabez valného praktického významu. Považují však za vhodnější seznámit se s novými koncepty na jednoduchých, pochopitelných příkladech a v závěru vše použít v grandiózním finále:-) Pokud nesouhlasíte, diskuse je vaše.

Takže: vytvoříme aplikaci, která vychází z našeho předchozího příkladu. Doplníme jej však o tlačítko, po jehož stisku se do titulku formuláře vypíše celkový počet zpráv, které aplikace dosud obdržela. Kromě toho bude editační pole „zamčeno“: pokud se do něj uživatel pokusí něco napsat, dojde k vypsání informační hlášky a žádný vstup nebude přijat (a v editačním poli zobrazen).

1. Máme připravenu aplikaci dokončenou v předchozím odstavci.

2. Na formulář přidáme tlačítko Button1, jeho popisek bude „Vypiš“.

3. Upravíme deklaraci třídy TForm1, do níž připíšeme privátní atribut PocetZprav a také proceduru AppMessage, kterou později „svážeme“ s událostí OnMessage. Tučně je uveden text, který bylo nutné přidat:

  TForm1 = class(TForm)
    Label1: TLabel;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure AppMessage(var Msg: TMsg; var Handled: Boolean);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    PocetZprav: Integer;
  public
    { Public declarations }
  end;

4. Provedeme zmíněné „svázání“ procedury AppMessage s událostí OnMessage. Využijeme k tomu událost OnCreate formuláře Form1, která již obsahuje příkazy pro vytvoření a zobrazení editačního pole. Tučně je uveden přidaný kód:

procedure TForm1.FormCreate(Sender: TObject);
var SE: TMujEdit;
begin
  SE := TMujEdit.Create(Form1);
  SE.Parent := Form1;
  SE.Visible := True;
  SE.Left := 50;
  SE.Top := 50;
  SE.Show;

  Form1.PocetZprav := 0;
  Application.OnMessage := AppMessage;
end;

5. Implementujeme metodu AppMessage:

procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  Handled := false;
  Inc(Form1.PocetZprav);

  if (msg.message = WM_KEYDOWN) then
  begin
    ShowMessage(`Nesahej na tu klávesnici!`);
    Handled := True;
  end;
end;

V implementaci této metody si prosím povšimněte práce s parametry Msg a Handled. V prvním z nich je předávána aktuální zpráva a pracujeme s ním stejně jako v minulém případě (je to struktura typu TMsg). Parametr Handled označuje, zda má zpráva dále vystupovat jako zpracovaná nebo nikoliv: pokud ne, dojde k jejímu zpracování „standardní cestou“. Touto „standardní cestou“ může být myšlena jedna z těchto variant:

  • pro zprávu (resp. pro její odpovídající událost) je vytvořen event handler (jednoduše řečeno, v Object Inspectoru jsme pro danou událost vytvořili obslužnou proceduru)
  • pro zprávu není vytvořeno nic, a tak provede svou „default“ akci, kterou jí dal do vínku programátor komponenty nebo jejího předka. Narážím na to, že pokud bychom do těla podmínky if (msg.message = WM_KEYDOWN) nenapsali Handled := True, došlo by sice k vypsání hlášky ShowMessage, ale v editačním poli by se zadaný znak objevil. Tím, že prohlásíme zprávu za ošetřenou, však potlačíme „default“ chování editačního pole a nevypíše se zhola nic.

6. Zbývá ošetřit událost OnClick tlačítka Button1:

procedure TForm1.Button1Click(Sender: TObject);
begin
  Form1.Caption := `Poèet zpráv: ` + IntToStr(Form1.PocetZprav);
end;

Tím je aplikace dokončena. Zkuste si s ní chvíli „pracovat“; pohybujte oknem, klikejte myší, stiskávejte klávesy ... pak si klepněte na tlačítko Vypiš. Uvidíte, že počet zpráv, s nímž se musí aplikace (a především AppMessage) vypořádat, není zrovna malý.

Pro úplnost nezatajím ani tentokráte kompletní zdrojový kód:

unit prjZpravy;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, AppEvnts;

type
  TMujEdit = class(TCustomEdit)
  private
    procedure WMKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;
  end;

  TForm1 = class(TForm)
    Label1: TLabel;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure AppMessage(var Msg: TMsg; var Handled: Boolean);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    PocetZprav: Integer;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;
  SpecEdit: TEdit;

implementation

{$R *.dfm}

procedure TMujEdit.WMKeyDown(var Msg: TWMKeyDown);
begin
  if (Msg.CharCode = VK_ESCAPE) then
    Application.Terminate
  else
    inherited;
end;

procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  Handled := false;
  Inc(Form1.PocetZprav);

  if (msg.message = WM_KEYDOWN) then
  begin
    ShowMessage(`Nesahej na tu klávesnici!`);
    Handled := True;
  end;

end;


procedure TForm1.FormCreate(Sender: TObject);
var SE: TMujEdit;
begin
  SE := TMujEdit.Create(Form1);
  SE.Parent := Form1;
  SE.Visible := True;
  SE.Left := 50;
  SE.Top := 50;
  SE.Show;

  Form1.PocetZprav := 0;
  Application.OnMessage := AppMessage;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Form1.Caption := `Poèet zpráv: ` + IntToStr(Form1.PocetZprav);
end;

end.

Důležitá poznámka: vytvořením druhého příkladu došlo prakticky ke zničení naší skvělé vlastnosti editačního pole, které dříve po stisku klávesy Esc ukončilo aplikaci. Zde je hezky vidět, že OnMessage má „přednost“ a dozví se o všech zprávách jako první. Protože v ní nastavíme u události WM_KEYDOWN parametr Handled na True, zpráva se považuje za vyřízenou a k obslužné proceduře editačního pole se vůbec nedostane.

Komponenta ApplicationEvents

Aby naše povídání o možnostech práce se zprávami v Delphi bylo kompletní, musíme se zmínit také o komponentě ApplicationEvents, která je typu TApplicationEvents a nalézá se v paletě Additional.

Tato komponenta slouží primárně k zjednodušení práce s objektem Application. Určitě víte, že tento objekt zapouzdřuje aplikaci jako celek a je automaticky obsažen v kterékoliv vytvářené aplikaci. Je však přístupný pouze za běhu. Tento objekt pochopitelně obsahuje několik událostí, a právě kvůli nim je zde komponenta ApplicationEvents. Aby nebylo nutné pracovat s událostmi objektu Application výhradně za běhu programu, můžeme v návrhové fázi umístit na formulář komponentu ApplicationEvents. Ta disponuje stejnými událostmi jako objekt Application a je tedy možné pohodlně ošetřit všechny potřebné události v návrhové fázi pomocí Object Inspectoru. Za běhu programu pak objekt Application přesměruje každou „svou“ událost objektu ApplicationEvents, který se o ni postará.

Dodejme, že každý formulář může obsahovat svou vlastní komponentu ApplicationEvents a aplikační události jsou pak předávány všem těmto komponentám. Je dokonce možné určovat i pořadí, v jakém události dostanou, případně některou z nich vyšachovat ze hry – to vše prostřednictvím speciálních metod.

Z celého předchozího odstavce bychom si měli odnést asi toliko: ošetření události OnMessage není nutné realizovat jen programově (jako jsme si to ukázali v předchozím příkladu), ale je možné do aplikace umístit komponentu ApplicationEvents a jednoduše ošetřit její událost OnMessage. Zajisté si tím ušetříme trochu práce. Myslím, že tato činnost je natolik zřejmá, že není nutné demonstrovat ji zvláštním příkladem.

Na závěr

Dnes jsme dokončili první část tématu „zprávy“: ošetřování standardních zpráv systému Windows. Už víme prakticky vše podstatné o tom, jak a kde zprávu odchytit, a to na všech úrovních: aplikace, formuláře i jednotlivé komponenty.

Za týden nás konečně čeká něco „zajímavého“: ukážeme si, jak pracovat s uživatelskými zprávami, jak je vytvářet a k čemu je nejlépe používat.

Diskuze (20) Další článek: Toshiba chce lokalizovat Tablet PC do češtiny

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,