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

# TaskTray / StatusBar 

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

Windows10 TaskTray
![Tasktray.png](https://qiita-image-store.s3.amazonaws.com/0/12977/292b9456-fb57-fb6d-e3a3-d189020d17b6.png)

macOS High Sierra StatusBar
![StatusBar.png](https://qiita-image-store.s3.amazonaws.com/0/12977/434f7b0f-3531-8b48-dd9d-1bf9f15fd22e.png)

という感じで、TaskTray / StatusBar にアイコンを登録する方法を紹介します。
ライブラリのソースコードは最後の「まとめ」にリンクがあります。

当然アプリケーション側は１ソースです。
FormCreate で下記のようにコードを書くと上記のアイコンとメニューが表示されます。

```pascal
procedure TForm1.FormCreate(Sender: TObject);
var
  Bmp: TBitmap;
begin
  // TaskTray Icon ライブラリを生成
  FTrayIcon := TTrayIcon.Create;

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

  // メニューの設定
  FTrayIcon.AddMenu('メニューアイテム１', MenuClickedHandler);
  FTrayIcon.AddMenu('メニューアイテム２', MenuClickedHandler);
  FTrayIcon.AddMenu('-', nil);
  FTrayIcon.AddMenu('メニューアイテム３', 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 ライブラリには以下の４つのファイルがあります。


| ソースファイル名        | 役割                      |
|-------------------------|---------------------------|
| 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 のメンバーとして定義します。

```pascal
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](http://docwiki.embarcadero.com/Libraries/Rio/ja/Vcl.ExtCtrls.TTrayIcon) という純正の TrayIcon コンポーネントがあるじゃないですか！
ということで、コレを内部で使っているだけです。

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

```console
プロジェクトの依存関係を確認中...
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 クラス）を結びつけています。

```pascal
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 から呼び出せされます。
でも、実装はたった↓これだけ。

```pascal
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 の設定
肝心のメニューを追加すると所と、アイコンを設定するところはこんな感じです。

```pascal
// メニューの追加
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;
```

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

# まとめ
[ソースはこちらから](https://bitbucket.org/JunHosokawa/trayicon/)

前に書いた[「FireMonkey で Window Drag」](https://qiita.com/pik/items/907641319e4de4d74805)と組み合わせたり、もちろん普通に常駐するプログラムを書いたりと、TrayIcon でアプリケーションの幅が広がりそうです。