Electron で Tray を使う（macOS, Windows）

Electron の `Tray` クラスについては公式ドキュメントにも、Apple や Microsoft の公式ドキュメントにも詳細が載っていないので、悩む部分が多かったです。そこで、自分で実際に調べ、得たものも含めて、ここに書いてシェアしようと思います。

## サンプルアプリケーション

これらの `Tray` を理解する手助けとして、サンプルアプリケーションを用意しました。シンプルな３分間タイマーで、その残り時間をパーセントに変換して、`Tray` にアイコン表示するというものです。これ以降、コードを記載するときは、ここにアップロードしてある実際のソースコードから抜粋しています。

SimpleTimer - GitHub
<https://github.com/hibara/SimpleTimer>
![SnapCrab_SimpleTimer_2020-10-4_17-16-16_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/b131fdb5-eb03-7c85-0824-f5543b59bf9b.png)
レポジトリには、「node_modules」が含まれておりませんので、ビルドする場合はローカルに pull した後に、当ディレクトリで `npm install` コマンドを実行してから行ってください。

実際の動作を見たい場合は、アプリケーションをビルドしておきました。ダウンロードしてお試しください。
<https://github.com/hibara/SimpleTimer/releases>

Windows：
<https://github.com/hibara/SimpleTimer/releases/download/v1.0.6/SimpleTimer-win32-ia32.zip>

macOS：
<https://github.com/hibara/SimpleTimer/releases/download/v1.0.6/SimpleTimer-darwin-x64.zip>

## Tray とは？（macOS, Windowsのそれぞれにおいて）

クラス: Tray
<https://www.electronjs.org/docs/api/tray>

Electronの `Tray` とは、macOS のデフォルトでは右上にある「**メニューバー**」に表示するアイコン、

![macOS-menubar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/9de87a6b-28c5-f434-4c0d-fe74422f234b.png)

Windows のデフォルトでは、右下にある「**タスクトレイ**」を指します。

![Windows-tasktray.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/cb59565f-a161-3097-4b5b-45feac4d7268.png)

主な機能は、アイコンを変えることでアプリケーションの状態をコンパクトにユーザーへ伝えることと、そこに表示されるアイコンをクリックすることでコンテキストメニューを出すこともできます。

## 表示アイコンのサイズ

タスクバー、またはタスクトレイにアイコンを表示するには、macOS, Windows 用にそれぞれ別途用意する必要があります。

### macOS の場合

タスクバー表示に必要となる分の、`.png` ファイルを用意します。

そのタスクバーに表示されるアイコンのサイズを知るには、実際の画面パーツを Apple 公式からダウンロードして確かめることができます。

<https://developer.apple.com/design/resources/>

ただ、実際 Photoshop などで開いたところで、内部でどのサイズ、解像度を用意すれば良いか分かりません。サイズ、解像度については以下にファイル規則など含め書かれていますが、肝心の「タスクバー」のアイコンサイズは見つかりません。

<https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/image-size-and-resolution/>

ですので、実際にトライ＆エラーで試してサイズと解像度を出してみました。私が独自調査した結果は以下の通りです。

![image-size@1x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/32d14ee0-d170-ed5c-20bf-db8187ff6e09.png)
縦（高さ）だけは、点線部分（18px）で揃えると、他のタスクバーアイコンと揃うと思います。
![image-size@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30945/6eefb7e0-5b0a-c8e7-b97c-b2807de45a39.png)
もちろん、これ以上のサイズで作成することも可能ですが、タスクバーから盛大にはみ出ます（特に高さ）。幅は、「any」という記述もあったので、横長のアイコンも表示可能でしょうが、今回は検証しません。

### macOSのトレイアイコン名の規則

ここで、気になってくるのは、macOS にある独特の画像ファイルの命名規則です。

これは macOS 側が、将来的に画像解像度を上げてきたとしても、自動的に適用できるための「仕様」として機能しています。

たとえば、「100.png」というファイル名があったとして、コード内で `100.png` を指定したとしても、自動的にその環境に最適な解像度のファイル、`100@1x.png` や `100@2x.png` を選択してくれます。ちなみに、`100@1x.png` = `100.png` です。どちらのファイル名でも大丈夫です。

`@1x` は、オリジナルサイズの解像度を指し、この画像サイズが「24 x 24px」ならば、`@2x` は、「48 x 48px」となり、もし `@3x` が存在するならば、「72 x 72px」です。

## Windows の場合

Windows の場合は、macOS と異なり、`.ico`ファイルを指定します。ただ、Windows の場合も、明確な公式ドキュメントが見つかりませんでした。

Windowsアイコンの仕様については、以下を信じることにします。
<https://docs.microsoft.com/ja-jp/windows/win32/uxguide/vis-icons?redirectedfrom=MSDN#size-requirements>

* フルセット：16x16, 32x32, 48x48, 256x256
* クラシックモード（フルセット）： 16x16, 24x24, 32x32, 48x48, 64x64

どう考えてもタスクトレイに入るアイコンにしては、大きすぎる `256x256` サイズが含まれていますが、一応従っておきましょう（たぶん入っていなくても表示はされるとは思いますが・・・）。

### Windows アイコンの作成方法

ネット検索していただければ、オンラインで作成する方法はたくさんあると思います。

デスクトップツールだと、[icofx](https://icofx.ro/) が、確実で使いやすく鉄板でしょうか。特に大量にアイコンファイルを処理したいときにバッチ処理が使えます。ただしシェアウェアなので、購入に躊躇される方もいるかもしれません。

[png2ico](http://winterdrache.de/freeware/png2ico/) というフリーの老舗コマンドラインツールもありますが、最終更新が古すぎて、Windows 7 から導入された、256pxサイズのアイコンには対応していないようです。256px以上のサイズの PNG ファイルを読み込ませようとすると、以下のようなエラーメッセージが表示されてしまいます。

```shell
Width must be multiple of 8 and <256. Height must be <256.
```

そこで、思い切ってアイコン生成アプリケーションを自作してみました。icofx ほどの高機能性はありませんが、最低限の `.ICO`ファイルを作るには十分でしょう。.NET Framework 4.0 製でソースもMITライセンスで上がっています。よろしければお試しください。

Png2WinIco
<https://github.com/hibara/Png2WinIco>

実行ファイル：
<https://github.com/hibara/Png2WinIco/releases>

アイコンファイル作成に必要な、256 x 256px 以上の高解像度を持つ .PNG ファイルをドラッグ＆ドロップするだけで、自動的に各サイズにリサイズ格納されたアイコンファイルを作成することができます。

## 実装する

実装はさして難しくありません。`process.platform` で、macOS か Windows かを判定して、それぞれに合ったアイコンファイルを割り当てます。

このコード例では、簡単なコンテキストメニュー（アプリケーションの終了コマンドのみ）も含めています。

```javascript:main.js
// トレイアイコンを生成する
let tray = null;

const createTrayIcon = () => {
  let imgFilePath;
  if (process.platform === 'win32') { // Windows
    imgFilePath = __dirname + '/images/tray-icon/white/100.ico';
  }
  else{ // macOS
    imgFilePath = __dirname + '/images/tray-icon/black/100.png';
    // 一応、macOS のダークモードに対応しておく
    if ( nativeTheme.shouldUseDarkColors === true ){
      isDarkTheme = true;
      imgFilePath = __dirname + '/images/tray-icon/white/100.png';
    }
  }
  const contextMenu = Menu.buildFromTemplate([
    { label: '終了', role: 'quit' }
  ]);
  tray = new Tray(imgFilePath);
  tray.setToolTip(app.name);
  tray.setContextMenu(contextMenu);
}
```

あとは、タイマーが経過する度に残り時間を計算して、パーセンテージから5%刻みで用意されたアイコンに表示を差し替えます。ゆっくりではありますが、タスクバー（またはタスクトレイ）の円が、アニメーションして変化していくのが分かると思います。

```javascript:main.js
// タイマーの表示
const displayTimer = (valMilliSeconds) => {
  mainWindow.webContents.send("ipc-display-timer", valMilliSeconds);
  // タスクトレイのアイコン表示
  let percent = Math.floor((valMilliSeconds / MAX_MILLI_SECONDS) * 100);
  // 5の倍数で丸める
  let multipleOfFive =  Math.round(percent / 5) * 5;

  let imgFilePath;
  let imgFileName = ('000' + multipleOfFive).slice(-3) + (process.platform === 'win32' ? '.ico' : '.png');
  if ( process.platform === 'win32' || isDarkTheme === true ) {
    imgFilePath = __dirname + '/images/tray-icon/white/' + imgFileName;
  }
  else {
    imgFilePath = __dirname + '/images/tray-icon/black/' + imgFileName;
  }

  tray.setImage(nativeImage.createFromPath(imgFilePath));
  tray.setToolTip(percent + "% - " + app.name);
};
```

## macOS でリアルタイムにダークモード変更されたときにも対応しておく

アプリケーションが動作しているのが、macOS なら「ダークテーマに変更」イベントが発生しますので、それに合わせてアイコンカラーも変更するようにしておくべきでしょう。

```javascript:main.js
// システムカラーの変更イベント
if ( process.platform === 'darwin' ) { 
  nativeTheme.on("updated", () => {
    isDarkTheme = nativeTheme.shouldUseDarkColors === true;
  });
}
```

## Tray アイコンが消えてしまう問題

以下のリンクにある Electron 公式ドキュメントの FAQ にもありますが、変数のスコープによるものらしいです。グローバルではなく、ローカルで Tray を生成すると、ガベージコレクションで消えてしまうようです。

Electron FAQ | Electron
<https://www.electronjs.org/docs/faq#%E6%95%B0%E5%88%86%E7%B5%8C%E3%81%A4%E3%81%A8%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE-tray-%E3%81%8C%E6%B6%88%E5%A4%B1%E3%81%97%E3%81%BE%E3%81%99%E3%80%82>

前述のサンプルコードでも、関数の中にではなく、グローバルに `let tray = null;` と記述してあります。

## ちなみに Windows のタスクトレイアイコンは「まれに」消えない

タスクトレイにアイコンを表示したアプリケーションを終了したとき、「まれに」アイコンが消えないことがあります。これは前述の問題とは関係なく、Windows の仕様です。

これは Electron の問題でもなんでもなく、私が .NET Framework や、C++Builder で Windows アプリケーションを開発していた頃にもあった仕様です。ただ、アイコンが残ってしまったとしても、アプリケーションを終了した後で、そのアイコン部分へマウスカーソルを持って行くと消えてくれます。

これって、Windows XP 以前からある問題なんですが、Microsoft は改善する気はないんですかね・・・