Umíme to s Delphi se vrací: 64. díl – Delphi a zprávy systému Windows

Po odmlce, která byla rozhodně delší než jsem před prázdninami sliboval, se opět setkáváme u pokračování seriálu Umíme to s Delphi. Od této chvíle se opět budeme setkávat pravidelně – každé pondělí, abychom společně poznávali další a další rysy vývojového nástroje Delphi. Dnes si řekneme úvodní informace o zprávách systému Windows a o tom, jakým způsobem s nimi pracovat v Delphi.

Ze všeho nejdříve bych se ovšem rád omluvil všem čtenářům, kteří začátkem října očekávali splnění mého slibu a zveřejnění nového dílu seriálu. Jak jsem uvedl v diskusi pod 63. dílem (a jak jsem také napsal některým z vás na e-mail), po svém návratu jsem nebyl schopen zvládnout nápor nevyřízených povinností a musel odsunout některé ze svých základních životních funkcí (mezi něž nepochybně patří psaní tohoto seriálu) na pozdější dobu. Ještě jednou se omlouvám a věřím, že dlouhá odmlka u vás způsobila alespoň takovou lačnost po nových dílech, jako u mne :-)

A nyní již k náplni dnešního dílu. Než se ponoříme do pokročilých a rozsáhlých témat (databáze, komponentní technologie COM apod.), zvolil jsem „na rozjezd“ problematiku zpráv systému Windows. Většina z vás asi podvědomě tuší, o co se jedná, nicméně jistě neuškodí souhrn všech podstatných údajů.

Co jsou zprávy ve Windows?

„Zpráva“ je jedním z nejdůležitějších pojmů, jaké lze v souvislosti s operačním systémem Windows vyslovit. Nejprve se pokusíme o vágní, avšak doufejme pochopitelný popis, teprve potom se zaměříme na přesnost informací. Vágně řečeno, zpráva je informace o tom, že se kdesi v systému něco stalo. Skutečně: když uživatel klepne (nebo i jen pohne) myší, zapíše nějaký text, stiskne klávesu... po provedení jakékoliv akce vytvoří Windows zprávu, která obsahuje všechny podstatné informace o tom, co a kde se vlastně stalo. Představte

Co se pak s touto zprávou děje? Windows ji zašlou aplikaci, které se tato zpráva týká. Každá z běžících aplikací má implicitně svou vlastní frontu zpráv. Nová zpráva je tedy zařazena na konec této fronty. V „nitru“ aplikace běží tzv. smyčka zpráv, jejíž úkol je jediný: převzít zprávu z fronty zpráv a předat ji ke zpracování. Je asi zřejmé, že zpracování zprávy musí provést nějaká specializovaná procedura (metoda). V obecném případě se tato metoda nazývá „okenní procedura“ (window procedure). Okenní procedura, která v aplikaci existuje pro každé okno (viz dále), zprávu „ošetří“, tj. vhodným způsobem na ni zareaguje.

Možná vám tohle všechno něco připomíná. Ano, princip programování v Delphi se nazývá „událostmi řízený“ a zde se dostáváme k samému kořeni filozofie „událostmi řízeného programování“. Bez ohledu na konkrétní implementaci, pozornému čtenáři je asi jasné, že mezí událostmi, které ošetřujeme v Delphi, a zprávami, které jsme si právě popsali, existuje souvislost. Je tomu přesně tak – zjednodušeně řečeno, Delphi pro nás zapouzdřuje zprávy, které lze obdržet od Windows, do podoby snadno pochopitelných a zpracovatelných událostí.

Princip zpráv ve Windows

Zopakujme ještě jednou názorně vznik a „pochod“ zpráv systémem Windows. Zdůrazněme, že se prozatím stále zabýváme „čistým systémem“, tedy tím, jak je filozofie zpráv chápána na úrovni Windows. Nejrůznější vývojové nástroje (Delphi nevyjímaje) mohou toto schéma poopravit k „obrazu svému“, obvykle zavedením pomocných datových typů a metod, díky nimž je pro vývojáře práce se zprávami značně jednodušší a příjemnější.

Klíčové jsou čtyři lokace:

1. NĚJAKÉ MÍSTO (zde dojde k provedení nějaké akce – ke vzniku události a tedy ke generování příslušné zprávy).

2. FRONTA ZPRÁV APLIKACE (zpráva vygenerovaná v kroku 1 je umístěna na konec fronty zpráv aplikace, které se tato zpráva „týká“)

3. SMYČKA ZPRÁV APLIKACE (z čela fronty zpráv dané aplikace se vezme první čekající zpráva a předá se okenní proceduře)

4. OKENNÍ PROCEDURA (okenní procedura provede nějakou akci – např. vypíše informační hlášku nebo překreslí okno – jako následek příslušné zprávy)

Vysvětleme si exaktně všechny zmíněné pojmy:

  • fronta zpráv: každá běžící aplikace obsahuje frontu zpráv, v níž se „hromadí“ zprávy v mezidobí mezi okamžiky „zpráva vznikla“ a „na zprávu bylo zareagováno“. Podotkněme, že jsou dvě možnosti, jak se fronta zpráv chová v případě, že do ní přijde zpráva, která v ní již je. První (častější) varianta spočívá v tom, že se nová zpráva zařadí na konec fronty a ve frontě jsou tedy příslušné zprávy dvě. Příkladem je třeba zpráva o pohybu myší: posunete-li myš poprvé (zpráva A) a zakrátko podruhé (zpráva B), zpráva B bude na konec fronty zařazena i v případě, že by v ní zpráva A ještě někde byla. Druhá varianta spočívá ve sloučení obou zpráv do jedné; takto se chová například zpráva o vypršení daného časového intervalu (o doběhnutí časovače), takže např. není možné zjistit, kolikrát časovač „tikl“.
  • smyčka zpráv: mechanismus, který zajišťuje vybrání zprávy z fronty a její předání ke zpracování okenní proceduře. Jinak řečeno – smyčka zpráv vždy doručí zprávu z čela fronty příslušnému oknu v aplikaci. Pokud je fronta prázdná, systém Windows umožní zpracování zpráv dalším aplikacím.
  • okenní procedura: v aplikaci může být více oken a každé z nich disponuje svou vlastní okenní procedurou, která ošetřuje zprávy (reaguje na zprávy) příslušející odpovídajícímu oknu. Okenní procedura (procedura okna) může také systému vracet určitou hodnotu jako reakci na příjem a zpracování zprávy.

Jak jsou zprávy implementovány?

Už víme, jak, kde a proč zprávy vznikají a co se s nimi poté děje. Zatím ale na zprávu pohlížíme jako na jakousi magickou (leč černou) skříňku, která obsahuje veškeré podstatné informace.

Implementace zprávy vás možná překvapí, možná i zklame. Jedná se o „obyčejný“ záznam. Následující programový výpis ukazuje, jak je zpráva implementována v Delphi. Rád bych upozornil na to, že implementace v systému (windows.h) je prakticky totožná (kromě formálních rozdílů jazyků Pascal a C). Následující deklaraci můžete spatřit v souboru windows.pas (nachází se tedy v jednotce Windows):

type
  TMsg = packed record
    hwnd: HWND;
    message: UINT;
    wParam: WPARAM;
    lParam: LPARAM;
    time: DWORD;
    pt: TPoint;
  end;

Kdykoliv tedy hovoříme o tom, že „aplikaci byla zaslána zpráva“, došlo k tomu, že do fronty zpráv byl zařazen výše uvedený záznam.

Vysvětleme si složky záznamu TMsg:

  • hwnd: handle (manipulátor) okna, jemuž zpráva náleží. Důležité je, že oknem nemusíme (a ani nemůžeme) rozumět pouze „okno z hlediska uživatele“, ale také veškeré „okenní“ ovládací prvky, např. tlačítka, seznamy, editační pole apod.
  • message: tato složka nese informaci, o jakou zprávu se vlastně jedná. Její hodnotou je obvykle některá z řady předdefinovaných konstant; ať již standardních (definovaných v jednotce System) nebo uživatelsky definovaných.
  • wParam, lParam: dodatečné parametry, které jsou „individuální“ pro tu kterou zprávu. Často také obsahují ukazatel na nějakou paměťovou oblast, v níž se nalézají nějaká „důležitá“ data.
  • time: čas, kdy zpráva vznikla.
  • pt: pozice kurzoru myši v okamžiku, kdy zpráva vznikla.

Zprávy a Delphi

Programujeme-li v Delphi, nemusí nás poměrně dlouhou dobu nic z výše uvedeného zajímat. Delphi zapouzdřuje zprávy do podoby událostí; smyčka zpráv je navíc součástí jednotky Forms. Výsledkem je, že se o přebírání, rozdělování a zpracování zpráv nemusíme starat: naším jediným úkolem je stanovit, co se má stát po vzniku té či oné události.

Delphi nepoužívá k ukládání informací o zprávách datový typ TMsg, nýbrž typ TMessage. Z jeho implementace je patrné, že neobsahuje všechny informace, které nalézáme v TMsg:

type
  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;

Typ TMessage je určen k reprezentaci zpráv pro nejrůznější metody Delphi (v jejichž parametru je nutné předat právě objekt typu TMessage). Součásti záznamu jsou asi intuitivně zřejmé: Msg je konstanta označující zprávu, WParam a LParam (resp. WParamLo, WParamHi a LParamLo, LParamHi) jsou parametry zprávy; pole Result (resp. ResultHi, ResultLo) uchovávají návratovou hodnotu zprávy, tedy hodnotu, kterou okenní procedura předá systému poté, co byla zpráva ošetřena.

Třída TMessage je obecnou, generickou datovou struktorou pro uchování jakékoliv zprávy. Aby ovšem mohla být práce se zprávami dostatečně zjednodušena, nalézáme v Delphi ještě nespočet dalších datových typů. Pro každý typ zprávy Windows nalézáme speciální datový typ. Deklarace všech těchto typů jsou uvedeny v souboru Messages.pas. Ptáte se na důvody jejich existence? V obecném záznamu (TMsg resp. TMessage) by bylo nutné zjišťovat všechny parametry zprávy z položek WParam a LParam. Vzpomeňme si však, že každá zpráva může mít tyto parametry různé, podle toho, čeho se příslušná zpráva týká.

Ukažme si například datový typ TWMKey, který reprezentuje zprávy klávesnice.

  TWMKey = packed record
    Msg: Cardinal;
    CharCode: Word;
    Unused: Word;
    KeyData: Longint;
    Result: Longint;
  end;

Jednotlivé složky záznamu asi není nutné podrobně rozebírat, proto jen stručně tři nejdůležitější:

  • Msg: konstanta (nebo číselný identifikátor) označující zprávu
  • CharCode: kód stisknuté klávesy. Může se jednat buď o ANSI znak nebo o konstantu, např. VK_MENU pro klávesu Alt.
  • KeyData: doplňkové informace o tom, co se stalo (např. počet opakování, scan kód apod.)

Tento datový typ TWMKey je dále „specializován“ a vznikají tak datové typy TWMChar, TWMKeyDown, TWMKeyUp, TWMSysChar, TWMSysKeyDown a TWMSysKeyUp pro jednotlivé specializované znakové zprávy.

Jak v Delphi obsluhovat zprávy?

Jak už řečeno několikrát – základní formou obsluhy zpráv v Delphi je obsluha příslušných událostí. S touto větou si vystačíte ve většině běžných programátorských situací. Pokud ne, může nastat jedna ze dvou situací:

  • potřebujeme ošetřit „standardní“ zprávu (např. zprávu o klepnutí myší) „nestandardním“ způsobem;
  • potřebujeme ošetřit nějakou speciální zprávu, takovou, která není zapouzdřena do žádné události.

Dnes skončíme u bodu 1., uživatelskými zprávami se budeme zabývat za týden.

Co si tedy počít, chceme-li sami provést ošetření nějaké zprávy? První příklad, který si ukážeme, nebude mít skutečně žádný praktický význam; stejného efektu bychom snadno dosáhli ošetřením události formuláře OnKeyDown. Nám však jde o demonstraci, kterak ošetřovat zprávy zasílané systémem Windows.

Dejme tomu, že má formulář zaměření. Chceme zajistit, aby po stisku libovolné klávesy došlo ke zmenšení formuláře o 10 pixelů na výšku i na šířku.

Za tímto účelem si zvolíme zprávu WM_KEYDOWN, která je zasílána při stisku klávesy (zjednodušeně řečeno). Abychom mohli v Delphi „odchytávat“ zprávy (bez použití událostí), musíme napsat vlastní obslužnou proceduru dané zprávy. Na tuto proceduru jsou tři požadavky:

  • Musí se jednat o metodu nějaké třídy. Je to logické: aplikace tak bude vědět, že hodláme ošetřovat zprávu vzniklou právě v této třídě. Příklad: vytvoříme obsluhu zprávy WM_RBUTTONUP třídy TForm5. Pak bude zpráva WM_RBUTTONUP „odchytávána“ a ošetřována pouze v případě, že pravé tlačítko bylo stisknuto na formuláři Form5.
  • V deklaraci procedury se musí objevit parametr specifikující zprávu, tedy buď typu TMessage nebo jiného konkrétního typu, např. TWmRButtonUp apod.
  • V deklaraci kromě toho musíme použít direktivu message, za níž je uvedena zpráva, kterou hodláme příslušnou procedurou ošetřovat.

V našem triviálním případě tedy musíme doplnit následující řádek do sekce Private deklarace formuláře TForm1:

    procedure WMKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;

Dále je nutné implementovat tělo obslužné procedury (v implementaci se za hlavičkou již nemusí vyskytovat direktiva message):

procedure TForm1.WMKeyDown(var Msg: TWMKeyDown);
begin
  Self.Width := Self.Width - 10;
  Self.Height := Self.Height - 10;
  inherited;
end;

Toť vše. Zkusíte-li si nyní aplikaci přeložit a spustit, po každém stisku klávesy se formulář vždy zmenší. Uvedeme si pro jistotu kompletní zdrojový kód modulu:

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
  private
    { Private declarations }
    procedure WMKeyDown(var Msg: TWMKeyDown); message WM_KEYDOWN;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation
procedure TForm1.WMKeyDown(var Msg: TWMKeyDown);
begin
  Self.Width := Self.Width - 10;
  Self.Height := Self.Height - 10;
  inherited;
end;

{$R *.dfm}

end.

Poznámky k aplikaci:

  • Obslužná procedura se jmenuje stejně jako příslušná zpráva, jen s velkým písmenem na začátku každého slova a bez podtržítek. Nejedná se o nutnost, ale o konvenci, kterou však doporučuji dodržovat.
  • Všimněte si prosím slůvka inherited, které je uvedeno v implementaci obslužné procedury. Říkáme jím, že po naší obsluze zprávy WM_KEYDOWN chceme provést ještě standardní ošetření této zprávy, tak, jak je definováno v obslužné proceduře předka. V této nesrozumitelné větě se pokouším vyjádřit, že pokud nezamýšlíme svou obsluhou provést něco zcela výjimečného, vždy bychom měli použít klíčové slovo inherited a vyvolat tak standardní obsluhu. V našem konkrétním případě by absence sice neškodila (stisk klávesy na formuláři neprovede zhola nic), ale raději si zvykněte inherited používat. Proč? Když třeba „vylepšujete“ ošetření zprávy WM_PAINT a zapomněli byste ve své obsluze zavolat obsluhu předka, nedojde k vykreslení vůbec ničeho (leda že byste vše zvládli ručně). Podotkněme, že v určitých situacích potlačujeme provedení standardní obsluhy zcela záměrně, ale častější je opačná situace. Za slovem inherited není nutné uvádět žádné další informace, Delphi „ví“, o jakou metodu (o jakého předka) se jedná: pozná to podle specifikace zprávy uvedené za klíčovým slovem message v deklaraci.

V souvislosti s předvedenou aplikací ovšem vyvstává otázka: co si počít, pokud nechceme ošetřovat zprávy vzniklé „na formuláři“, ale na některé z jeho komponent. Jinak řečeno: co kdybychom měli na formuláři editační pole a chtěli ošetřit zprávu WM_KEYDOWN tohoto editačního pole? Řekli jsme si, že obslužná procedura musí být metodou příslušného objektu (v našem případě tedy editačního pole). Ale kde vzít objekt typu editační pole, když standardně v aplikaci máme jen objekt formuláře?

Řešení této otázky si ukážeme za týden; stejně jako vytváření uživatelských zpráv a práci s nimi. Po dnešním kurzu by měl vážený čtenář mít povědomost o zprávách ve Windows, o jejich podstatě a o jejich „pohybu“ po systému. Dokážete také v Delphi ošetřit kteroukoliv zprávu – prozatím pouze týká-li se formuláře. V ostatních situacích možná prozatím tápete – tento nemilý stav však potrvá jen do příštího týdne. Tedy za předpokladu, že si najdete čas pro přečtení dalšího dílu seriálu.

Diskuze (6) Další článek: Notebooky Premio půjdou Aceru po krku

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