zudo-tauri

Type to search...

to open search from anywhere

メニューイベントハンドラー

作成2026年3月29日更新2026年3月29日Takeshi Takatsudo

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::newSubmenu::newPredefinedMenuItem を使用する:

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(())
})

重要なポイント

  1. on_menu_event を絶対にブロックしない — メインスレッドで実行される
  2. I/O には thread::spawn を使用するAppHandle をクローンしてスレッドにムーブする
  3. WebView eval は安全w.eval() は非ブロッキングでメインスレッド上で問題ない
  4. 定義済み項目は on_menu_event をトリガーしない — カスタム MenuItemBuilder::with_id 項目のみがトリガーする
  5. タイムアウトには Instant を使用する — 単調クロックであり、システム時刻の変更に影響されない
  6. 作成と処理を分離するcreate_menuregister_menu_handler を別々の関数として保持する