最小化ボタンでトレイに格納するようにする

Recipe ID: menu-013

「ウィンドウの閉じるボタン(×ボタン)を押してもアプリを終了せず、トレイに格納(ウィンドウを非表示)する」という、常駐型アプリでよくある挙動を実装する方法を解説します。

Tauri v2 では、JavaScript API (Window.onCloseRequested) を使用して、閉じるイベントをキャンセルし、代わりにウィンドウを隠す処理を記述できます。

前提条件

Cargo.toml の設定

Tauri v2 ではトレイ機能は Core (本体) に統合されているため、追加のプラグイン(tauri-plugin-tray など)のインストールは不要です。
ただし、src-tauri/Cargo.tomltray-icon 機能を有効にする必要があります。

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

Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

{
  "permissions": [
    ...,
    "core:window:default",
    "core:window:allow-hide",
    "core:window:allow-show",
    "core:tray:default",
    "core:app:allow-default-window-icon"
  ]
}

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

getCurrentWindow() で現在のウィンドウを取得し、onCloseRequested イベントをリッスンします。
イベント内で event.preventDefault() を呼び出すことで、デフォルトの閉じる動作(アプリ終了/ウィンドウ破棄)を防ぎます。

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

const appWindow = getCurrentWindow();

// 1. トレイアイコンを作成
const icon = await defaultWindowIcon();
if (icon) {
  const tray = await TrayIcon.new({
    tooltip: 'My App',
    icon: icon, // アプリのデフォルトアイコンを使用
    action: async (event) => {
      // トレイアイコンクリックでウィンドウの表示/非表示を切り替え
      if (event.type === 'Click') {
        const visible = await appWindow.isVisible();
        if (visible) {
          await appWindow.hide();
        } else {
          await appWindow.show();
          await appWindow.setFocus();
        }
      }
    },
  });
}

// 2. ウィンドウの「閉じる」ボタンの挙動をオーバーライド
await appWindow.onCloseRequested(async (event) => {
  // デフォルトの閉じる動作(アプリ終了/ウィンドウ破棄)をキャンセル
  event.preventDefault();
  
  // ウィンドウを非表示にする(トレイに格納されたように見える)
  await appWindow.hide();
  
  console.log('Window hidden to tray');
});

2. バックエンドで実装する (Rust) - 推奨

Rust 側では on_window_event を使用して、CloseRequested イベントを捕捉します。フロントエンドよりも確実に動作を保証できます。

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

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let _tray = TrayIconBuilder::with_id("tray")
                .icon(app.default_window_icon().unwrap().clone())
                .on_tray_icon_event(|tray, event| match event {
                    TrayIconEvent::Click {
                        button: MouseButton::Left,
                        button_state: MouseButtonState::Up,
                        ..
                    } => {
                        let app = tray.app_handle();
                        if let Some(window) = app.get_webview_window("main") {
                            if window.is_visible().unwrap_or(false) {
                                let _ = window.hide();
                            } else {
                                let _ = window.show();
                                let _ = window.set_focus();
                            }
                        }
                    }
                    _ => {}
                })
                .build(app)?;
            Ok(())
        })
        .on_window_event(|window, event| match event {
            WindowEvent::CloseRequested { api, .. } => {
                // デフォルトの閉じる動作を防止
                api.prevent_close();

                // ウィンドウを隠す
                window.hide().unwrap();
            }
            _ => {}
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

補足

* アプリの完全終了: この実装を行うと、ウィンドウの×ボタンでアプリが終了しなくなります。トレイメニューなどに必ず「終了(Quit)」項目を用意し、そこで process.exit()app.exit() を呼び出すようにしてください。