トレイアイコンがクリックされた時の処理を書く

Rust の on_tray_icon_event や JS の TrayIcon.new() の action でトレイアイコンのクリックを受け、左クリックでウィンドウを出し入れする。押下と離上の二重発火や Linux の非対応も示す。

メニュー・トレイ 対象: Tauri 2.x 更新日: 読了目安: 約9分 menu-012
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 2. バックエンドから実装する (Rust)
  4. 動作確認
  5. よくあるエラーと対処法
  6. OS ごとの違いと注意点
  7. 関連レシピ

常駐アプリでは「アイコンを左クリックするとウィンドウが出たり引っ込んだりし、右クリックでメニューが開く」という操作が定番です。アイコン自体へのクリックやマウスの出入りは TrayIconEvent として届き、Rust ではトレイを作るときの on_tray_icon_event()、JS では TrayIcon.new() の action で受け取ります。メニュー項目が押されたときの処理は別の仕組みなので、トレイアイコンのメニューを作る を参照してください。

前提条件

トレイを使うには src-tauri/Cargo.toml で tauri の tray-icon 機能が必要です(npm run tauri add では入りません。menu-009 参照)。

[dependencies]
tauri = { version = "2", features = ["tray-icon"] }

イベントはトレイを作るときに渡した関数へ届くので、受け取るための追加の権限は要りません。JS でウィンドウを出し入れする場合、isVisible() / isMinimized() は core:default に含まれますが、show() / hide() / unminimize() / setFocus() の権限は含まれないので足します。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:app:allow-default-window-icon",
    "core:window:allow-show",
    "core:window:allow-hide",
    "core:window:allow-unminimize",
    "core:window:allow-set-focus"
  ]
}

届くイベントは次の 5 種類です。どれにも、マウスの位置 position と、アイコンの位置と大きさ rect が物理ピクセルで入っています。

種類届くとき固有の値
Clickボタンを押したときと離したとき(1 回のクリックで 2 回)button(Left / Right / Middle)、buttonState(Down / Up)
DoubleClickダブルクリック(Windows のみ)button
Enter / Move / Leaveマウスが乗った・動いた・離れたなし

1. フロントエンドから実装する (TypeScript)

action に渡した関数が上のイベントを受け取ります。type で種類を見分け、Click なら button と buttonState で絞り込みます。Click かどうかだけで判定すると、押したときに表示して離したときに隠す、という動きになります。メニューを付けたアイコンは既定で左クリックでもメニューが開くので、左クリックを使うなら showMenuOnLeftClick: false にします。

import { TrayIcon, type TrayIconEvent } from '@tauri-apps/api/tray';
import { defaultWindowIcon } from '@tauri-apps/api/app';
import { getCurrentWindow } from '@tauri-apps/api/window';

const win = getCurrentWindow();

// 見えていれば隠し、隠れているか最小化されていれば前に出す
async function toggleWindow() {
  if ((await win.isVisible()) && !(await win.isMinimized())) {
    await win.hide();
  } else {
    await win.unminimize();
    await win.show();
    await win.setFocus();
  }
}

function onTrayEvent(event: TrayIconEvent) {
  // 左ボタンを離したとき (Up) だけ処理する
  if (event.type === 'Click' && event.button === 'Left' && event.buttonState === 'Up') {
    void toggleWindow();
  }
}

// 再読み込みでアイコンが増えないよう、同じ ID があれば消してから作る(menu-009 参照)
if (await TrayIcon.getById('main')) await TrayIcon.removeById('main');
await TrayIcon.new({
  id: 'main',
  icon: (await defaultWindowIcon()) ?? undefined,
  tooltip: 'My App',
  showMenuOnLeftClick: false,
  action: onTrayEvent,
});

action はページの中で動くので、ページを再読み込みしたりウィンドウを閉じたりすると呼ばれなくなります。ウィンドウの状態に関係なくクリックを受けたいときは、次の Rust 版で処理します。Rust で作ったアイコンには JS から action を後付けできないので、画面側でも反応したいときは Rust から emit() したイベントを listen() で受けます。

2. バックエンドから実装する (Rust)

TrayIconBuilder::on_tray_icon_event() に渡した関数が、このアイコンのイベントを受け取ります。Rust ではフィールド名が button_state になります。tray.app_handle() からウィンドウを取り出して操作します。

use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Manager};

/// 見えていれば隠し、隠れているか最小化されていれば前に出す
fn toggle_main_window(app: &AppHandle) -> tauri::Result<()> {
    let Some(window) = app.get_webview_window("main") else {
        return Ok(()); // 閉じて破棄された後は None になる
    };
    if window.is_visible()? && !window.is_minimized()? {
        window.hide()
    } else {
        window.unminimize()?;
        window.show()?;
        window.set_focus()
    }
}

fn on_tray_event(tray: &TrayIcon, event: TrayIconEvent) {
    if let TrayIconEvent::Click {
        button: MouseButton::Left,
        button_state: MouseButtonState::Up,
        ..
    } = event
    {
        let _ = toggle_main_window(tray.app_handle());
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            TrayIconBuilder::with_id("main")
                .icon(tauri::include_image!("icons/32x32.png"))
                .tooltip("My App")
                .show_menu_on_left_click(false) // 左クリックはウィンドウの切り替えに使う
                .on_tray_icon_event(on_tray_event)
                .build(app)?;
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

設定ファイルの app.trayIcon で作ったアイコンには、処理が何も付いていません。その場合は Builder::on_tray_icon_event() で、すべてのアイコンのイベントをまとめて受けます。event.id() でどのアイコンかを見分けます。

use tauri::tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent};

/// tauri.conf.json の app.trayIcon(ID は既定で "main")のクリックを受ける
/// toggle_main_window() は上のブロックのもの
pub fn run_with_config_tray() {
    tauri::Builder::default()
        .on_tray_icon_event(|app, event| {
            if event.id() != "main" {
                return;
            }
            if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
                let _ = toggle_main_window(app);
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

動作確認

npm run tauri dev で起動し、トレイのアイコンを左クリックするとウィンドウが隠れ、もう一度クリックすると前面に戻ります。最小化してからクリックすると元の大きさに戻ります。JS 版の onTrayEvent の先頭に console.log(event.type, event.type === 'Click' ? event.buttonState : '') を入れ、アイコンを 1 回クリックしてからマウスを離すと、DevTools のコンソールに次の順で出ます(マウスを動かすと間に Move が入ります)。

Enter
Click Down
Click Up
Leave

よくあるエラーと対処法

  • クリックするとウィンドウが一瞬出てすぐ消える: 押したとき(Down)と離したとき(Up)の両方で切り替えています。buttonState === 'Up'(Rust は MouseButtonState::Up)のときだけ処理します。
  • 左クリックでメニューが開いてしまう: メニューがあると既定で左クリックでも開きます。showMenuOnLeftClick: false(Rust は show_menu_on_left_click(false))にします。
  • ダブルクリックでシングルクリックの処理も動く: ダブルクリックでは、DoubleClick の前後に Click も届きます。両方に別の処理を割り当てず、どちらか一方にします。
  • 「window.show not allowed. Permissions associated with this command: core:window:allow-show」: JS 版の権限不足です。リリースビルドでは「Command plugin:window|show not allowed by ACL」とだけ出ます。hide / unminimize / set_focus も個別に足します。
  • 設定ファイルで作ったアイコンのクリックに反応しない: TrayIconBuilder を使っていないので、アイコンごとの処理は登録されていません。上の Builder::on_tray_icon_event() で受けるか、app.tray_by_id("main") で取り出して on_tray_icon_event() を登録します。

OS ごとの違いと注意点

  • Linux: クリックやマウスの出入りのイベントは届きません。アイコンは表示され、右クリックでメニューも開くので、ウィンドウを出す操作は必ずメニューにも入れておきます(menu-010)。showMenuOnLeftClick も効きません。
  • Windows: DoubleClick が届くのは Windows だけです。macOS と Linux にも出すアプリでは、シングルクリックを基本にします。
  • iOS / Android: トレイはありません。
  • 共通: × でウィンドウを閉じると破棄され、get_webview_window("main") は None を返すようになります。トレイから呼び戻す前提なら、× で閉じずに隠す設定にします(最小化ボタンでトレイに格納するようにする)。表示・非表示の API は ウィンドウを表示・非表示にする にまとめています。
  • 位置に合わせる: position と rect は物理ピクセルです。アイコンのすぐ近くに小さなウィンドウを出したいときは、Positioner プラグイン のトレイ基準の位置指定を使うと、座標の計算をしなくて済みます(プラグインの tray-icon 機能を有効にし、トレイのイベントをプラグインの on_tray_event() にも渡します)。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する