常駐アプリでは「アイコンを左クリックするとウィンドウが出たり引っ込んだりし、右クリックでメニューが開く」という操作が定番です。アイコン自体へのクリックやマウスの出入りは 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()にも渡します)。
