Edited at
DelphiDay 1

Delphi + FireMonkey で TaskTray / StatusBar にアイコンとメニューを実装する


TaskTray / StatusBar

まずはコレをご覧下さい。

Windows10 TaskTray

Tasktray.png

macOS High Sierra StatusBar

StatusBar.png

という感じで、TaskTray / StatusBar にアイコンを登録する方法を紹介します。

ライブラリのソースコードは最後の「まとめ」にリンクがあります。

当然アプリケーション側は1ソースです。

FormCreate で下記のようにコードを書くと上記のアイコンとメニューが表示されます。

procedure TForm1.FormCreate(Sender: TObject);

var
Bmp: TBitmap;
begin
// TaskTray Icon ライブラリを生成
FTrayIcon := TTrayIcon.Create;

// アイコンそのものをクリックされたときのイベント(Windows Only)
FTrayIcon.RegisterOnClick(TrayIconClickHandler);

// メニューの設定
FTrayIcon.AddMenu('メニューアイテム1', MenuClickedHandler);
FTrayIcon.AddMenu('メニューアイテム2', MenuClickedHandler);
FTrayIcon.AddMenu('-', nil);
FTrayIcon.AddMenu('メニューアイテム3', MenuClickedHandler);

// TaskTray / StatusBar に登録する画像の指定
Bmp := ImageList1.Bitmap(TSizeF.Create(24, 24), 0); // 画像は ImageList が管理しているので削除するとエラーになる

// 画像の登録(複数登録可能。状態変更時 ChangeIcon を使って使うアイコンを切り替えられる)
FTrayIcon.RegisterIcon('Normal', Bmp);
// 使う画像を指定(指定しないと表示されないので注意!)
FTrayIcon.ChangeIcon('Normal', 'ヒント');

// TaskTray / StatusBar に表示
FTrayIcon.Apply;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
FTrayIcon.DisposeOf;
end;

procedure TForm1.MenuClickedHandler(Sender: TObject);
begin
ShowMessage('メニューが押されたよ!');
end;

procedure TForm1.TrayIconClickHandler(Sender: TObject);
begin
ShowMessage('TaskTray アイコンがクリックされたよ!');
end;


TTrayIcon ライブラリ

ということで、Windows / macOS それぞれにアイコンとメニューを実装できたわけですが、それぞれの仕組みを解説します。

作成した TrayIcon ライブラリには以下の4つのファイルがあります。

ソースファイル名
役割

PK.TrayIcon.pas
プログラムから扱う本体

PK.TrayIcon.Default.pas
インターフェースの定義

PK.TrayIcon.Win.pas
Windows TaskTray 用ソース

PK.TrayIcon.Mac.pas
macOS StatusBar 用ソース

ソースから実際に使うのは PK.TrayIcon.pas です。

コレを interface 部で uses して、TForm1 のメンバーとして定義します。

unit Unit1;

interface

uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
System.ImageList,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.ImgList,
PK.TrayIcon; // ←コレ!

type
TForm1 = class(TForm)
ImageList1: TImageList;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FTrayIcon: TTrayIcon; // インスタンスを宣言
procedure MenuClickedHandler(Sender: TObject); // メニューがクリックされたときのイベント
procedure TrayIconClickHandler(Sender: TObject); // TrayIcon がクリックされたときのイベント
public
end;

後の使い方は最初のソースの通りです。


TaskTray 編

Windows 用の PK.TaskTray.Win.pas の仕組みですが…ぶっちゃけ何もやっていません!

というのは VCL には TTrayIcon という純正の TrayIcon コンポーネントがあるじゃないですか!

ということで、コレを内部で使っているだけです。

ただ1つだけ問題がありまして…

プロジェクトの依存関係を確認中...

Project1.dproj をコンパイル中 (Debug, Win32)
dcc32 の "Project1.dpr" コマンド ライン
[dcc32 ヒント] H2161 Warning: Duplicate resource: Type 12 (CURSOR GROUP), ID 32761; File d:\programs\embarcadero\studio\20.0\lib\Win32\release\Controls.res resource kept; file d:\programs\embarcadero\studio\20.0\lib\Win32\release\FMX.Controls.Win.res resource discarded.
[dcc32 ヒント] H2161 Warning: Duplicate resource: Type 12 (CURSOR GROUP), ID 32762; File d:\programs\embarcadero\studio\20.0\lib\Win32\release\Controls.res resource kept; file d:\programs\embarcadero\studio\20.0\lib\Win32\release\FMX.Controls.Win.res resource discarded.
[dcc32 ヒント] H2161 Warning: Duplicate resource: Type 12 (CURSOR GROUP), ID 32763; File d:\programs\embarcadero\studio\20.0\lib\Win32\release\Controls.res resource kept; file d:\programs\embarcadero\studio\20.0\lib\Win32\release\FMX.Controls.Win.res resource discarded.
[dcc32 ヒント] H2161 Warning: Duplicate resource: Type 12 (CURSOR GROUP), ID 32766; File d:\programs\embarcadero\studio\20.0\lib\Win32\release\Controls.res resource kept; file d:\programs\embarcadero\studio\20.0\lib\Win32\release\FMX.Controls.Win.res resource discarded.
[dcc32 ヒント] H2161 Warning: Duplicate resource: Type 12 (CURSOR GROUP), ID 32767; File d:\programs\embarcadero\studio\20.0\lib\Win32\release\Controls.res resource kept; file d:\programs\embarcadero\studio\20.0\lib\Win32\release\FMX.Controls.Win.res resource discarded.
成功
経過時間: 00:00:01.1

という感じで H2161 が出ちゃうんですよね。

H2161 というのはリソースが重複している、というヒントで今回の場合は無視して構いません。

原因は VCL と FMX で同じカーソルリソースを読み込んでいるためです。

TTrayIcon.Win.pas の中で HINT の抑止をしようと思ったのですが、ちょっと難しかったのと、万が一の可能性を考えて残してあります。


StatusBar 編

macOS の上のメニューとか出るところを StatusBar といいます。

ここには実は色々なコントロールを乗せられます。

今回はここにアイコンを乗せます。


StatusItem の生成

まず、コンストラクタでシステムの StatusBar インスタンスを取得して、そこに StatusItem を生成しています。この StatusItem がアイコンを表示するエリアです。

そして、StatusItem と自作のオブジェクト(TTrayMenuItem クラス)を結びつけています。

constructor TTrayIconMac.Create;

begin
inherited Create;

FTrayMenuItem := TTrayMenuItem.Create(Self);

FIcons := TDictionary<String, NSImage>.Create;
FEvents := TDictionary<Pointer, TNotifyEvent>.Create;

// システムの StatusBar を取得
FStatusBar := TNSStatusBar.Wrap(TNSStatusBar.OCClass.systemStatusBar);

// StatusItem を生成
FStatusItem := FStatusBar.statusItemWithLength(NSVariableStatusItemLength);

// StatusItem と TTrayMenuItem と結びつける
FStatusItem.setTarget(FTrayMenuItem.GetObjectID);
FStatusItem.setHighlightMode(true);

FMenu := TNSMenu.Create;
FMenu := TNSMenu.Wrap(FMenu.initWithTitle(StrToNSStr(Application.Title)));
end;


TTrayMenuItem

TTrayMenuItem は TOCLocal を継承したクラスで、Delphi のクラスですが、Objective-C からも呼び出せるクラスです。

今回はメニューのクリックで Objective-C から呼び出せされます。

でも、実装はたった↓これだけ。

constructor TTrayMenuItem.Create(const iOwner: TTrayIconMac);

begin
inherited Create;
FOwner := iOwner;
end;

procedure TTrayMenuItem.DispatchMenuClick(Sender: Pointer);
begin
// クリックされたら TTrayIcon の DispatchMenuClick を呼ぶ
FOwner.DispatchMenuClick(Sender);
end;

function TTrayMenuItem.GetObjectiveCClass: PTypeInfo;
begin
Result := TypeInfo(ITrayMenuItem); // 自分の型情報(NSObject を継承している)を返す
end;


Icon と Menu の設定

肝心のメニューを追加すると所と、アイコンを設定するところはこんな感じです。

// メニューの追加

procedure TTrayIconMac.AddMenu(const iName: String; const iEvent: TNotifyEvent);
var
Item: NSMenuItem;
P: Pointer;
begin
if iName = '-' then // '-' を指定されたらセパレータを追加
Item := TNSMenuItem.Wrap(TNSMenuItem.OCClass.separatorItem)
else
begin
// MenuItem を作って、FTrayMenuItem の DispatchMenuClick と結びつける
Item := TNSMenuItem.Create;
Item :=
TNSMenuItem.Wrap(
Item.initWithTitle(
StrToNSStr(iName),
sel_getUid(MarshaledAString('DispatchMenuClick:')),
StrToNSStr('')
)
);

// GetObjectID は Objective-C 側から見た時のポインタ
Item.setTarget(FTrayMenuItem.GetObjectID);

P := (Item as ILocalObject).GetObjectID;
FEvents.Add(P, iEvent);
end;

// NSMenu に NSMenuItem を追加
FMenu.addItem(Item);
end;

procedure TTrayIconMac.Apply;
begin
// NSMenu を NSStatusItem に設定
FStatusItem.setMenu(FMenu);
end;

procedure TTrayIconMac.ChangeIcon(const iName, iHint: String);
var
Image: NSImage;
begin
// アイコンを NSStatusItem に設定
if FIcons.TryGetValue(iName, Image) then
FStatusItem.setImage(Image);

// ToolTip 文字列を設定
FStatusItem.setToolTip(StrToNSStr(iHint));
end;

// アイコンを登録
procedure TTrayIconMac.RegisterIcon(const iName: String; const iIcon: TBitmap);
begin
// BitmapToMenuBitmap は FMX.Helpers.Mac に定義されている関数
FIcons.Add(iName, BitmapToMenuBitmap(iIcon, 16));
end;

と、こちらも定石通りにプログラムするだけです。


まとめ

ソースはこちらから

前に書いた「FireMonkey で Window Drag」と組み合わせたり、もちろん普通に常駐するプログラムを書いたりと、TrayIcon でアプリケーションの幅が広がりそうです。