🔖

Electronでバックグラウンド通知

2024/02/04に公開

Electron の理解を深めるために、バックグラウンドで動作し通知するタイマーを作ってみました。
関連するコードをまとめましたので、Electron を触り始めた人の参考になれば幸いです。

作ったものについて

時間と分を入力すると、その時間に通知を表示してくれるアプリです。

リポジトリ
アプリケーション

実装

準備

テンプレート作成
$ pnpm create @quick-start/electron .
✔ Package name: … electron-timer
✔ Select a framework: › react
✔ Add TypeScript? … Yes
✔ Add Electron updater plugin? … No
✔ Enable Electron download mirror proxy? … No

この時のコミット

バックグラウンドで動作させる

作成したテンプレートではアプリケーションを起動するとウィンドウが表示されます。
ウィンドウを閉じるとアプリケーションが終了するようになっています。

起動時のウィンドウ表示をやめて、その状態でもアプリケーションが落ちないようにします。

起動時のウィンドウ表示をやめる

基本的にはアプリケーション起動時の処理(app.whenReady().then( ...)にあるウィンドウを開く処理を消せば対応できます。
MacOS ではウィンドウの有無に関わらずアプリケーション起動時にアイコンが表示されます。
ドックに表示させない処理が必要になります。

/src/main/index.ts
app.whenReady().then(() => {
  if (process.platform === 'darwin') {
    app.dock.hide()
  }
}

ウィンドウ閉じても終了しないようにする

作成したテンプレートでは、ウィンドウを全て閉じるとアプリケーションが終了するコードが書かれています。

/src/main/index.ts
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

このコードによると、darwin(MacOS)以外の OS にて全てのウィンドウを閉じたときにアプリケーションを終了するようになっています。
逆にいうと、MacOS では全てのウィンドウが閉じられると自動的にアプリケーションが終了するので何もしていないということになります。

というわけで、Windows ではウィンドウを閉じた時のapp.quit()をやめるだけでいいのですが、 MacOS では特別な設定が必要になります。
ビルド時の設定を以下のように変更します。

package.json
  "build": {
    "mac": {
      "extendInfo": {
        "LSUIElement": true
      }
    }
  }

ビルド時の設定なので、開発者モードではドックにアイコンが表示されたままになっています。
申し訳程度にアプリケーションの準備が整ったタイミングでドックから隠しておきます。

/src/main/index.ts
app.whenReady().then(() => {
  // package.jsonのbuild設定で、起動時にドックに表示しないようにしているので不要
  // 開発者モードの挙動を合わせるために消しておく
  if (process.platform === 'darwin') {
    app.dock.hide()
  }
}

Tray を作る

ウィンドウを閉じてもアプリケーションが終了しないようにできましたが、操作する口がありません。
Tray を表示させて、アプリの起動確認と操作をできるようにします。
Tray とは MacOS ではメニューバー、Windows では通知領域のことを指します。
以下のように、Tray を追加し、ウィンドウを開くメニューとアプリケーションを終了するメニューを追加します。

/src/main/index.ts
app.whenReady().then(() => {

  ...

  // テンプレートを作った時の画像を16pxに加工
  const trayIcon = nativeImage.createFromPath(icon).resize({ width: 16 });
  // Tray作成
  const tray = new Tray(trayIcon);
  // Trayにメニュー追加
  const contextMenu = Menu.buildFromTemplate([
    {
      label: "ウィンドウを開く",
      click: () => {
        createWindow();
      },
    },
    {
      label: "終了",
      click: () => {
        app.quit();
      },
    },
  ]);
  tray.setContextMenu(contextMenu);
});

実行してみると、メニューバーに表示されました。
クリックすると、設定したメニューが表示されています。

ドックの表示を切り替える(MacOS のみ)

アプリケーション起動時にはドックに表示しないようにしましたが、ウィンドウ表示時にはドックに表示させたいです。(個人的にそういう動作をするアプリが多いイメージがあります)

ウィンドウ表示時にドックにも表示させる
ウィンドウ表示用のメソッドにドック表示のコードを追加します。

/src/main/index.ts
function createWindow(): void {
  if (process.platform === 'darwin') {
    app.dock.show()
  }

  ...
}

ウィンドウを消した時にドックからも消す

/src/main/index.ts
app.on('window-all-closed', () => {
  if (process.platform === 'darwin') {
    app.dock.hide()
  }
})

タイマーを作る

Tray 経由でウィンドウを開くことができるようになったので、タイマー機能を作ります。

時間の入力フォームを作る

時分の入力フォームとボタンを一つ用意しました。

ボタンをクリックした時に、メインプロセス側に入力した時間を送信します。

/src/render/App.tsx
  const onClick = (): void => {
    // time = {
    //  hours: number
    //  minutes: number
    // }
    window.electron.ipcRenderer.invoke('setTimer', time)
  }

入力した時間をメインプロセスに保存する

時間と分を記憶するオブジェクトを定義し、レンダラー側の入力を受け付けるようにしました。

src/main/index.ts
type Time = {
  hours: number;
  minutes: number;
};

let notifyTime: Time | null = null;

ipcMain.handle("setTimer", (event, time: Time) => {
  console.debug("setTimer", time);
  notifyTime = time;
});

入力した時間が来たときに通知する

保存した時間を検知する処理を書きます。
まずは、1 分ごとに発火する EventEmitter を作成します。(setIntervalで十分だったのですが使ってみたかったので、EventEmitter にしました)

src/main/index.ts
// 1分ごとにtimeイベントをemitする
const intervalEventEmitter = new EventEmitter();
setInterval(() => {
  intervalEventEmitter.emit("time", new Date());
}, 1000 * 60);

作った EventEmitter のイベントが発火した時に時刻を調べて、通知する処理です。

src/main/index.ts
intervalEventEmitter.on("time", (now: Date) => {
  if (notifyTime == null) {
    return;
  }

  if (
    notifyTime.hours === now.getHours() &&
    notifyTime.minutes === now.getMinutes()
  ) {
    const notification = new Notification({
      title: "時間になりました。",
    });
    notification.show();
  }
});

実際の通知はこんな感じです。

Discussion