メニューイベントハンドラー
Tauri v2 でメインスレッドをブロックせずにメニューイベントを正しく処理する方法。
重要なルール
⚠️ Warning
on_menu_event はメインスレッドで実行される。ハンドラー内でブロッキング操作(I/O、ネットワーク、プロセス管理)を直接実行してはならない。メインスレッドのブロックはアプリケーション UI 全体をフリーズさせる。
メニューの作成
Tauri v2 はメニュー構築に 2 つのスタイルを提供する。どちらも動作し、複雑さに応じて選択する。
ビルダースタイル(シンプルなメニュー)
単純なメニューには SubmenuBuilder を使用する:
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
let app_menu = SubmenuBuilder::new(app, "MyApp")
.about(None)
.separator()
.quit()
.build()?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.select_all()
.build()?;
let view_menu = SubmenuBuilder::new(app, "View")
.item(
&MenuItemBuilder::with_id("refresh", "Refresh")
.accelerator("CmdOrCtrl+R")
.build(app)?,
)
.item(
&MenuItemBuilder::with_id("devtools", "Toggle Developer Tools")
.accelerator("CmdOrCtrl+Alt+I")
.build(app)?,
)
.separator()
.item(
&MenuItemBuilder::with_id("zoom_in", "Zoom In")
.accelerator("CmdOrCtrl+=")
.build(app)?,
)
.item(
&MenuItemBuilder::with_id("zoom_out", "Zoom Out")
.accelerator("CmdOrCtrl+-")
.build(app)?,
)
.build()?;
let menu = MenuBuilder::new(app)
.item(&app_menu)
.item(&edit_menu)
.item(&view_menu)
.build()?;
app.set_menu(menu)?;
明示的スタイル(完全な制御)
完全な制御には Menu::new、Submenu::new、PredefinedMenuItem を使用する:
use tauri::menu::{Menu, PredefinedMenuItem, Submenu};
let handle = app.handle();
let menu = Menu::new(handle)?;
// App menu (macOS only)
#[cfg(target_os = "macos")]
{
let app_menu = Submenu::new(handle, "zudotext", true)?;
app_menu.append(
&PredefinedMenuItem::about(handle, Some("About zudotext"), None)?,
)?;
app_menu.append(&PredefinedMenuItem::separator(handle)?)?;
app_menu.append(&PredefinedMenuItem::services(handle, None)?)?;
app_menu.append(&PredefinedMenuItem::separator(handle)?)?;
app_menu.append(&PredefinedMenuItem::hide(handle, None)?)?;
app_menu.append(&PredefinedMenuItem::hide_others(handle, None)?)?;
app_menu.append(&PredefinedMenuItem::show_all(handle, None)?)?;
app_menu.append(&PredefinedMenuItem::separator(handle)?)?;
app_menu.append(&PredefinedMenuItem::quit(handle, None)?)?;
menu.append(&app_menu)?;
}
// File menu with custom items
let file_menu = Submenu::new(handle, "File", true)?;
file_menu.append(
&tauri::menu::MenuItemBuilder::with_id("new_window", "New Window")
.accelerator("CmdOrCtrl+N")
.build(handle)?,
)?;
file_menu.append(&PredefinedMenuItem::separator(handle)?)?;
file_menu.append(
&PredefinedMenuItem::close_window(handle, None)?,
)?;
menu.append(&file_menu)?;
app.set_menu(menu)?;
📝 Note
定義済みメニュー項目(コピー、ペースト、Undo など)は OS によって自動的に処理され、on_menu_event には表示されない。MenuItemBuilder::with_id() で作成したカスタム項目のみがハンドラーをトリガーする。
イベント処理パターン
非ブロッキング:WebView eval(メインスレッドで安全)
単純な WebView の評価は高速であり、ハンドラー内で直接実行しても安全である:
app.on_menu_event(|app_handle, event| {
let id = event.id().0.as_str();
match id {
"reload" => {
if let Some(w) = app_handle.get_webview_window("main") {
let _ = w.eval("window.location.reload()");
}
}
"toggle_devtools" => {
if let Some(w) = app_handle.get_webview_window("main") {
if w.is_devtools_open() {
w.close_devtools();
} else {
w.open_devtools();
}
}
}
"zoom_in" => {
if let Some(w) = app_handle.get_webview_window("main") {
let _ = w.eval(
"{ const z = Math.min(3, \
parseFloat(document.body.style.zoom || '1') * 1.1); \
document.body.style.zoom = z.toString(); }",
);
}
}
_ => {}
}
});
ブロッキング:バックグラウンドスレッドを生成する
I/O を伴う操作(サイドカー管理、ファイル操作、ネットワークリクエスト)では、スレッドを生成する:
app.on_menu_event(|app_handle, event| {
match event.id().as_ref() {
"refresh" => {
// Clone handle for the background thread
let handle = app_handle.clone();
std::thread::spawn(move || {
do_refresh(&handle); // This does I/O, waits for server
});
}
_ => {}
}
});
⚠️ Warning
この例の do_refresh 関数はサイドカープロセスを kill し、ポートが利用可能になるのを待ち、新しいプロセスを生成し、準備完了をポーリングする。これらの操作のいずれも数秒間ブロックする可能性がある。メインスレッドで実行するとアプリがフリーズする。
タイムアウトポーリングパターン
開発サーバーやサイドカーの準備完了をポーリングする際は、Instant::elapsed() に対してチェックされる bool フラグを使用する:
fn wait_for_ready(timeout: Duration) {
let start = Instant::now();
while start.elapsed() < timeout {
let code = check_ready();
if code != "000" && code != "err" {
// Server is ready
return;
}
thread::sleep(Duration::from_secs(1));
}
// Timeout reached
eprintln!("Server did not respond within timeout");
}
💡 Tip
タイムアウトチェックには保存された SystemTime から経過時間を再計算するのではなく、Instant::now() と elapsed() を使用する。Instant は単調であり、システムクロックの調整の影響を受けない。
メニューハンドラーからのウィンドウ作成
メニューハンドラーから新しいウィンドウを作成するには、App 参照ではなく AppHandle が必要である:
"new_window" => {
if let Err(e) = window::create_main_window(&handle) {
eprintln!("Failed to create new window: {e}");
}
}
ウィンドウ作成自体はブロッキング I/O ではないため、ハンドラー内で直接実行できる。ただし、ウィンドウ作成が開発サーバーのポーリングをトリガーする場合(開発ワークフローの場合)、そのポーリングは create_main_window 内でバックグラウンドスレッドに生成される必要がある。
ハンドラーの登録
メニュー作成とイベント処理は専用モジュールに保持する:
// native/menu.rs
pub fn create_menu(app: &tauri::App) -> tauri::Result<Menu<tauri::Wry>> {
// ... build menu
}
pub fn register_menu_handler(app: &tauri::App) {
let handle = app.handle().clone();
app.on_menu_event(move |app_handle, event| {
// ... handle events
});
}
setup() から両方を呼び出す:
.setup(|app| {
let menu = native::menu::create_menu(app)?;
app.set_menu(menu)?;
native::menu::register_menu_handler(app);
Ok(())
})
重要なポイント
on_menu_eventを絶対にブロックしない — メインスレッドで実行される- I/O には
thread::spawnを使用する —AppHandleをクローンしてスレッドにムーブする - WebView eval は安全 —
w.eval()は非ブロッキングでメインスレッド上で問題ない - 定義済み項目は
on_menu_eventをトリガーしない — カスタムMenuItemBuilder::with_id項目のみがトリガーする - タイムアウトには
Instantを使用する — 単調クロックであり、システム時刻の変更に影響されない - 作成と処理を分離する —
create_menuとregister_menu_handlerを別々の関数として保持する