11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DelphiAdvent Calendar 2018

Day 1

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

Last updated at Posted at 2018-11-30

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 でアプリケーションの幅が広がりそうです。

11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?