Umíme to s Delphi: 72. díl – tray ikona, 2. část

V minulém části seriálu jsme zahájili nesnadný úkol: vytvoření komponenty pro zobrazení ikony ve stavové části hlavního panelu. Dnes můžeme směle pokračovat v díle a zapracovat na tvorbě této komponenty.
Na úvod zrekapitulujme, čeho jsme dosáhli před týdnem: implementovali jsme metodu SendTrayMessage, která zapouzdřuje volání funkce WinAPI Shell_NotifyIcon a která má za úkol také naplnit všechny potřebné datové složky správnými hodnotami. Získali jsme také jednoznačný identifikátor, který použijeme pro zprávy z tray.

Rozhraní pro příjem zpráv z tray ikony

Dnes se na úvod zamyslíme nad tím, jakým způsobem bude aplikace komunikovat se svou tray ikonou. V dílech 64 – 66 jsme se zabývali zprávami systému Windows: ano, řešením je samozřejmě použití zpráv. Celý mechanismus bude vypadat takto: ve stavové části hlavního panelu vznikne nějaká událost (např. klepnutí myší). Systém vytvoří zprávu říkající „na ikoně X v tray se kleplo myší“ a pošle ji. Příjemce zprávu zpracuje a rozhodne, co se bude dít (např. zobrazí hlavní okno aplikace).

Otázka však zní, kdo bude příjemcem. Jistě to bude aplikace, které tato ikona patří. Otázkou však je, které okno?

Kdybychom vytvářeli aplikaci (nikoliv komponentu), použili bychom samozřejmě přímo okno aplikace (hlavní formulář). Protože však komponenta žádné okno nemá, nezbývá nám, než si vytvořit vlastní okno. Protože však naše komponenta žádné okno nepotřebuje a ani nechce, bylo by ideální vytvořit pouze nějaké fiktivní okno, jehož jediným úkolem by bylo přijímat zprávy, ale které by se nikde nezobrazovalo. Naštěstí tento úkol není neřešitelný: pomůže nám funkce definovaná v Delphi v jednotce Classes: AllocateHWnd.

Celou akci tedy provedeme tak, že definujeme pomocnou třídu TIconManager, která bude sloužit právě jako rozhraní mezi stavovou oblastí a výslednou aplikací. Objekt IconManager si vytvoří pomocí AllocateHWnd okno určené pro příjem a zpracování zpráv. Později zařídíme, aby všechny zprávy ze stavové oblasti hlavního panelu byly odesílány právě tomuto oknu zpravovanému objektem IconManager.

Podívejme se na implementaci objektu IconManager:

type
  TIconManager = class
  private
    FHWindow: HWnd;
    procedure TrayWndProc(var Message: TMessage);
  public
    constructor Create;
    destructor Destroy; override;
    property HWindow: HWnd read FHWindow write FHWindow;
  end;

Popis: IconManager bude mít privátní atribut FHWindow, v němž budeme uchovávat handle okna, které pomocí tohoto objektu spravujeme. Veřejnou vlastností (pracující s atributem FHWindow) je HWindow, která při čtení nebo zápisu pouze vrátí / změní hodnotu atributu FHWindow. Pokud některý ze čtenářů netuší, o čem to zde hovořím, odkazuji jej na díly 44 – 47, v nichž se zabýváme tvorbou komponent a v nichž najde podrobný popis všeho zmíněného.

IconManager bude mít konstruktor, resp. destruktor (v něm budeme vytvářet, resp. odstraňovat okno) a kromě toho jedinou metodu: TrayWndProc je vlastně okenní procedurou (window procedure) spravovaného okna. V implementaci této procedury, jak později uvidíme, budeme odchytávat a zpracovávat zprávy ze stavové oblasti. Pojem okenní procedura je vysvětlen v 64. dílu seriálu.

Zbývá zodpovědět poslední otázku: kde vytvořit objekt IconManager? Je asi zřejmé, že musíme zvolit takové místo, které bude provedeno hned na začátku, před startem vlastní aplikace. Ideálně k tomu poslouží sekce inicialization našeho modulu (tedy modulu s vytvářenou komponentou TrayIkona). Připomeňme, že paměť, kterou alokujeme (tedy objekty, které vytvoříme) v sekci inicialization, je nutné uvolnit v sekci finalization. Uvedeme si obě sekce tak, jak na současném stupni vývoje (včetně kódu vytvořeného před týdnem) vypadají (nejdříve vytvoříme globální proměnnou IconMgr, která pak bude obsahovat vytvořený objekt):

var
  ...
  IconMgr: TIconManager;

initialization
  DDGM_TRAYICON := RegisterWindowMessage(TrayMsgStr);
  IconMgr := TIconManager.Create;  // vytvoření objektu IconManager
finalization
  IconMgr.Free;  // uvolnění objektu IconManager

Nyní musíme implementovat konstruktor a destruktor objektu IconManager. Co vlastně potřebujeme? Musíme zajistit, aby byla při vytvoření objektu naplněna vlastnost HWindow, v níž budeme uchovávat handle okna pro příjem zpráv ze stavové oblasti. K tomu použijeme funkci AllocateHWnd, která slouží pro vytvoření okna, jež není asociováno z žádnou „okenní“ komponentou. Tato funkce se používá typicky právě v situacích, v nichž je nutné vytvořit nějaké okno, které nemá být viditelné pro uživatele, ale které může přijímat zprávy. Tuto funkci interně používá např. komponenta Timer, která není vidět, přesto však přijímá zprávy časovače z Windows. Jediným parametrem této funkce je název okenní procedury, kterou okno použije pro práci se zprávami. AllocateHWnd navíc vrací handle na vytvořené okno, takže návratovou hodnotu lze rovnou přiřadit do atributu FHWindow. AllocateHWnd se nalézá v jednotce Classes:

constructor TIconManager.Create;
begin
  FHWindow := Classes.AllocateHWnd(TrayWndProc);
end;

Analogicky pro dealokaci vytvořeného okna se používá funkce DeallocateHWnd, jíž se jako parametr předá handle okna určeného k odstranění z paměti:

destructor TIconManager.Destroy;
begin
  if FHWindow <> 0 then Classes.DeallocateHWnd(FHWindow);
  inherited Destroy;
end;

Implementaci objektu IconManager máme prakticky hotovou, zbývá „pouze“ okenní procedura TrayWndProc. Na ni se podíváme za okamžik.

Vlastnosti komponenty TrayIkona

Ještě předtím se však vrátíme ke třídě TTrayIkona a zamyslíme se nad tím, co by vlastně měla svému uživateli (tedy programátorovi aplikací) poskytovat – jakými vlastnostmi by měla disponovat.

Je nesporné, že jednou z klíčových vlastností bude ikona, aby si mohl uživatel zvolit, jak bude jeho aplikace v stavové oblasti prezentována. První vlastností tedy bude Icon. Připomeňme, že pro každou vlastnost je nutné vytvořit privátní atribut, v němž se uchovává její hodnota, a dále přístupové metody pro čtení a zapisování hodnoty této vlastnosti (pro podrobnosti odkazuji opět na díly 44 – 47 tohoto seriálu, které se zabývají tvorbou komponent). Deklarujeme tedy atribut pro vlastnosti Icon a vlastnost samotnou:

  TTrayNotifyIcon = class(TComponent)
  private
    ...
    FIcon: TIcon;  // privátní atribut pro vlastnost Icon
    ...
  published
    ...
    property Icon: TIcon read FIcon write SetIcon;    // vlastnost Icon
    ...

Za datový typ vlastnosti Icon jsme zvolili TIcon, díky čemuž bude moci uživatel komponenty používat vestavěný editor tohoto typu vlastnosti (a vybírat tedy ikonu z dialogu namísto zapisování cesty k souboru). Protože ikona bude ve stavovém pruhu zobrazena i v době návrhu, zajistíme, aby se ikona měnila vždy při změně odpovídající vlastnost v Object Inspectoru. Vytvoříme tedy metodu pro zápis vlastnosti Icon a zavoláme v ní SendTrayMessage. Pro čtení hodnoty vlastnosti nebudeme vytvářet speciální metodu a vystačíme si pouze s aktuální hodnotou atributu FIcon.

procedure TTrayNotifyIcon.SetIcon(Value: TIcon);
begin
  FIcon.Assign(Value); 
  if FIconVisible then SendTrayMessage(NIM_MODIFY, NIF_ICON);
end;

Poznámka: ikonu měníme pouze v případě, že je hodnota atributu FIconVisible (viz dále) kladná. Jak později uvidíme, tento atribut je spjat s vlastností IconVisible, pomocí které uživatel komponenty povoluje / zakazuje zobrazování ikony.

Další vlastností, na níž jsme narazili již před týdnem, bude Hint, do níž se bude zapisovat plovoucí nápověda určená k zobrazení při ponechání kurzoru myši nad ikonou. Pro tuto vlastnost platí prakticky totéž, co pro vlastnost Icon. Také vytvoříme atribut FHint a pro zápis vytvoříme metodu SetHint. Podobným způsobem budeme později vytvářet většinu dalších vlastností, proto již nebudeme tento postup opakovat:

  TTrayNotifyIcon = class(TComponent)
  private
    FHint: string;    // privátní atribut pro vlastnost Hint
    ...
  published
    ...
    property Hint: String read FHint write SetHint;    // vlastnost Hint
    ...

Metoda pro zápis vlastnosti Hint:

procedure TTrayNotifyIcon.SetHint(Value: string);
begin
  if FHint <> Value then    // pokud je aktuální Hint jiný
  begin
    FHint := Value;        // změníme hodnotu příslušného atributu
    if FIconVisible then    // a pokud se má zobrazovat ikona, modifikujeme ji
      SendTrayMessage(NIM_MODIFY, NIF_TIP);
  end;
end;

Nyní již jen stručně popíšeme další vlastnosti, které bude komponenta TrayIkona poskytovat:

Vlastnost Odpovídající atribut Popis Přístupové metody
HideTask FHideTask Logická hodnota říkající, zda při minimalizaci má být aplikace viditelná na hlavním panelu pro čtení není, pro zápis SetHideTask
IconVisible FIconVisible Logická hodnota říkající, má-li být ikona v tray zobrazena pro čtení není, pro zápis SetIconVisible
PopupMenu FPopupMenu Rozbalovací menu, které se má ukázat při klepnutí pravým tlačítkem na ikonu pro čtení není, pro zápis SetPopupMenu

Vraťme se ještě podrobněji k vlastnosti HideTask. Všimněte si, že obvykle aplikace, které mají svou ikonu ve stavové části hlavního panelu, nezobrazují své další tlačítko na hlavním panelu. Není zkrátka možné přepnout se do dané aplikace jinak než klepnutím na ikonu ve stavové části. Vlastnost naší komponenty HideTask zajistí možnost volby, zda má toto standardní nastavení platit nebo zda se má aplikace zobrazovat i na „klasickém“ hlavním panelu.

Vlastnost HideTask bude implementována stejně jako předchozí uvedené vlastnosti: bude mít privátní atribut FHideTask, pro čtení nebude disponovat žádnou metodou a pro zápis napíšeme metodu SetHideTask. V jejím těle využijeme funkci ShowWindow. Pojďme si funkci ShowWindow podrobněji vysvětlit.

Funkce Windows API – ShowWindow

Pomocí funkce ShowWindow můžeme nastavit pro zvolené okno rozmanité atributy související s jeho vzhledem a zobrazením:

function ShowWindow(hWnd: HWND, nCmdShow: int): BOOL;

Prvním parametrem této funkce je handle okna, jehož parametry hodláme nastavovat. Pomocí druhého parametru pak nastavujeme vše potřebné. Hodnotou parametru by vždy měla jedna z hodnot uvedených v následující tabulce:

Hodnota Význam
SW_HIDE Okno se skryje a aktivuje se jiné okno.
SW_MAXIMIZE Okno se maximalizuje
SW_MINIMIZE Okno se minimalizuje a je aktivováno další okno „v pořadí“
SW_RESTORE Aktivuje a zobrazí okno. Pokud je toto okno maximalizováno nebo minimalizováno, dojde k obnovení jeho původní velikosti a pozice.
SW_SHOW Okno je aktivováno a zobrazeno na své aktuální pozici a ve své aktuální velikosti
SW_SHOWMAXIMIZED Okno je aktivováno, maximalizováno a zobrazeno
SW_SHOWMINIMIZED Okno je aktivováno, minimalizováno a zobrazeno
SW_SHOWMINNOACTIVE Okno je zobrazeno jako minimalizované. Nedojde však k jeho aktivaci; aktivní okno zůstává aktivním.
SW_SHOWNA Zobrazí okno v jeho aktivní velikosti, nedojde však k jeho aktivaci; aktivní okno zůstává aktivním.
SW_SHOWNOACTIVATE Zobrazí okno v poslední pozici a velikosti, nedojde však k jeho aktivaci; aktivní okno zůstává aktivním.
SW_SHOWNORMAL Aktivuje a zobrazí okno. Pokud je okno minimalizované nebo maximalizované, dojde k jeho obnovení na originální velikost a pozici.

Nyní si již můžeme uvést kód zápisové metody pro vlastnost HideTask:

procedure TTrayIkona.SetHideTask(Value: Boolean);
const
  ShowArray: array[Boolean] of integer = (sw_ShowNormal, sw_Hide);
begin
  if FHideTask <> Value then    // pokud se hodnota bude měnit
  begin
    FHideTask := Value;        // změň ji
    // v návrhové fázi nedělej s oknem nic
    if not (csDesigning in ComponentState) then
    // jinak ukaž okno se změněným atributem
      ShowWindow(Application.Handle, ShowArray[FHideTask]);
  end;
end;

Poslední dnešní maličkost

Ještě předtím, než se pro dnešek rozloučíme, zmíníme poslední maličkost, kterou je nutné provést a která se promítne do výsledného zdrojového kódu: deklarujeme privátní atribut FDefaultIcon, který bude obsahovat „default“ ikonu pro případ, že uživatel komponenty nevybere žádnou jinou:

type
  TTrayIkona = class(TComponent)
  private
    ...
    FDefaultIcon: THandle;
    ...

Kam jsme dospěli?

Protože se nám opět nahromadilo několik operací, které mohly vnést trochu zmatku do výsledného zdrojového kódu, uvedeme si tento kód ve stavu, do něhož jsme jej dnes uvedli. Pro úsporu místa uvedeme kód bez komentářů. Vysvětlující komentáře jsou uvedeny v jednotlivých fragmentech kódu, které podrobně vysvětlujeme v předchozích odstavcích.

unit TrayIcon;

interface

uses Windows, SysUtils, Messages, ShellAPI, Classes, Graphics, Forms, Menus, StdCtrls, ExtCtrls;

type
  TTrayIkona = class(TComponent)
  private
    FDefaultIcon: THandle;
    FIcon: TIcon;
    FHideTask: Boolean;
    FHint: string;
    FIconVisible: Boolean;
    FPopupMenu: TPopupMenu;
    Tnd: TNotifyIconData;
    procedure SetIcon(Value: TIcon);
    procedure SetHideTask(Value: Boolean);
    procedure SetHint(Value: string);
    procedure SetIconVisible(Value: Boolean);
    procedure SetPopupMenu(Value: TPopupMenu);
    procedure SendTrayMessage(Msg: DWORD; Flags: UINT);
  protected
  public
  published
    property Icon: TIcon read FIcon write SetIcon;
    property HideTask: Boolean read FHideTask write SetHideTask default False;
    property Hint: String read FHint write SetHint;
    property IconVisible: Boolean read FIconVisible write SetIconVisible default False;
    property PopupMenu: TPopupMenu read FPopupMenu write SetPopupMenu;
  end;

procedure Register;

implementation
procedure Register;
begin
  RegisterComponents(`Ruzne`, [TTrayIkona]);
end;

type
  TIconManager = class
  private
    FHWindow: HWnd;
    procedure TrayWndProc(var Message: TMessage);
  public
    constructor Create;
    destructor Destroy; override;
    property HWindow: HWnd read FHWindow write FHWindow;
  end;

var
  IconMgr: TIconManager;
  DDGM_TRAYICON: Cardinal;

constructor TIconManager.Create;
begin
  FHWindow := Classes.AllocateHWnd(TrayWndProc);
end;

destructor TIconManager.Destroy;
begin
  if FHWindow <> 0 then Classes.DeallocateHWnd(FHWindow);
  inherited Destroy;
end;


{ TTrayIkona }

procedure TTrayIkona.SendTrayMessage(Msg: DWORD; Flags: UINT);
begin
  with Tnd do
  begin
    cbSize := SizeOf(Tnd);
    StrPLCopy(szTip, PChar(FHint), SizeOf(szTip));
    uFlags := Flags;
    uID := UINT(Self);
    Wnd := IconMgr.HWindow;
    uCallbackMessage := DDGM_TRAYICON;
    hIcon  := ActiveIconHandle;
  end;
  Shell_NotifyIcon(Msg, @Tnd);
end;

procedure TTrayIkona.SetHideTask(Value: Boolean);
const
  ShowArray: array[Boolean] of integer = (sw_ShowNormal, sw_Hide);
begin
  if FHideTask <> Value then
  begin
    FHideTask := Value;
    { Don`t do anything in design mode }
    if not (csDesigning in ComponentState) then
      ShowWindow(Application.Handle, ShowArray[FHideTask]);
  end;
end;

procedure TTrayIkona.SetHint(Value: string);
begin
  if FHint <> Value then
  begin
    FHint := Value;
    if FIconVisible then
      SendTrayMessage(NIM_MODIFY, NIF_TIP);
  end;
end;

procedure TTrayIkona.SetIcon(Value: TIcon);
begin
  FIcon.Assign(Value); 
  if FIconVisible then SendTrayMessage(NIM_MODIFY, NIF_ICON);
end;

procedure TTrayIkona.SetIconVisible(Value: Boolean);
const
  MsgArray: array[Boolean] of DWORD = (NIM_DELETE, NIM_ADD);
begin
  if FIconVisible <> Value then
  begin
    FIconVisible := Value;
    SendTrayMessage(MsgArray[Value], NIF_MESSAGE or NIF_ICON or NIF_TIP);
  end;
end;

procedure TTrayIkona.SetPopupMenu(Value: TPopupMenu);
begin
  FPopupMenu := Value;
  if Value <> nil then Value.FreeNotification(Self);
end;

const
  TrayMsgStr = `DDG.TrayNotifyIconMsg`;

initialization
  DDGM_TRAYICON := RegisterWindowMessage(TrayMsgStr);
  IconMgr := TIconManager.Create;
finalization
  IconMgr.Free;
end.

Diskuze (1) Další článek: Kasparov začíná další bitvu proti počítači

Témata článku: Software, Windows, Programování, Hlavní panel, Originální velikost, Hlavní funkce, Celý mechanismus, Hicon, Destroy, TNO, Okno, Ikona, Díl, Výsledné dílo, Icon, Aktivní uživatel, Timer, Read, Self, Private

Určitě si přečtěte


Aktuální číslo časopisu Computer

Zachraňte nefunkční Windows

Jak nakupovat a prodávat kryptoměny

Otestovali jsme konvertibilní notebooky

Velký test 14 herních myší