AppHandle を使ってアプリ全体を操作する

コマンドの引数や setup で AppHandle を受け取り、clone() して別スレッドへ渡す。Manager・Emitter トレイトで使える操作と、終了やウィンドウ作成で困る場面も示す。

Rust バックエンド 対象: Tauri 2.x 更新日: 読了目安: 約7分 rust-008
目次
  1. 前提条件
  2. 1. バックエンドから実装する (Rust)
  3. 取り方
  4. 別スレッドへ渡して、進み具合を知らせる
  5. 引数で渡せない場所から使う・終了する
  6. 2. フロントエンドから呼び出す (TypeScript)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

AppHandle は、実行中のアプリ全体を指す Rust の値です。ウィンドウをラベルで探す、State を読む、全ウィンドウへイベントを送る、アプリを終了するといった「特定のウィンドウに属さない操作」は、ここから行います。安く複製でき、別スレッドへ渡せるので、バックグラウンドの処理から画面へ知らせるときの基本の道具でもあります。取り方、スレッドへの渡し方、use が必要なトレイトを順に説明します。

前提条件

プラグインも capability の権限も要りません。Rust から直接呼ぶ操作は権限の対象外です。ただし、AppHandle のメソッドの多くはトレイトで提供されるので、使うトレイトを use します。

やりたいことメソッド必要な use
ウィンドウをラベルで取る・一覧するget_webview_window() / webview_windows()tauri::Manager
State を読む・登録するstate() / try_state() / manage()tauri::Manager
アプリ用フォルダのパスを得るpath()tauri::Manager
フロントエンドへイベントを送るemit() / emit_to()tauri::Emitter
Rust でイベントを受けるlisten() / listen_any()tauri::Listener
終了・再起動するexit() / restart()不要
メインスレッドで実行するrun_on_main_thread()不要

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

取り方

場所取り方
コマンド引数に app: tauri::AppHandle と書く
setupapp.handle()(参照なので、持ち出すなら .clone())
WebviewWindow を持っているときwindow.app_handle()
メニュー・トレイ・run() のコールバック引数で &AppHandle が渡される

setup の引数の App は別スレッドへ渡せませんが、AppHandle は渡せます。clone() しても同じアプリを指す小さな値が増えるだけなので、複製のコストは気にしなくて構いません。

別スレッドへ渡して、進み具合を知らせる

コマンドはすぐに戻り、処理は別スレッドで続けて、進み具合をイベントで送る例です。スレッドへは clone() した AppHandle を move で渡します。State も State 型のままでは持ち出せないので、スレッドの中で state() から取り出します(State と Mutex でアプリの状態を管理する)。

use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tauri::{AppHandle, Emitter, Manager};

/// 処理中かどうか(二重に始めないため)
#[derive(Default)]
struct Job {
    running: AtomicBool,
}

/// 時間のかかる処理を別スレッドで始め、コマンド自体はすぐに戻る
#[tauri::command]
fn start_job(app: AppHandle) -> Result<(), String> {
    if app.state::<Job>().running.swap(true, Ordering::SeqCst) {
        return Err("すでに実行中です".into());
    }
    let handle = app.clone(); // スレッドに渡す分を複製する(指すアプリは同じ)
    std::thread::spawn(move || {
        for percent in (20..=100).step_by(20) {
            std::thread::sleep(Duration::from_millis(500)); // 実際の処理の代わり
            let _ = handle.emit("job-progress", percent); // 全ウィンドウへ送る
        }
        handle.state::<Job>().running.store(false, Ordering::SeqCst);
    });
    Ok(())
}

async の処理なら、std::thread::spawn の代わりに tauri::async_runtime::spawn(async move { ... }) へ同じように渡します(非同期(async)コマンドを定義する)。イベントの受け方は Rust からのイベントを受信する で扱います。特定のウィンドウだけを操作したり送ったりする方法は WindowHandle で特定のウィンドウを操作する を参照してください。

引数で渡せない場所から使う・終了する

外部ライブラリのコールバックのように、引数で AppHandle を受け取れない場所から使いたいときは、setup で OnceLock に入れておく方法があります。渡せる場所では引数で渡す方が、依存が見えて分かりやすくなります。

use std::sync::OnceLock;

/// 引数で渡せない場所から使うための置き場(setup で 1 回だけ入れる)
static APP: OnceLock<AppHandle> = OnceLock::new();

/// 例: 外部ライブラリから呼ばれるコールバック。setup の前なら何もしない
fn on_device_changed(name: &str) {
    if let Some(app) = APP.get() {
        let _ = app.emit("device-changed", name);
    }
}

#[tauri::command]
fn quit(app: AppHandle) {
    app.exit(0); // 終了のイベントを経てから終わる
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .manage(Job::default())
        .setup(|app| {
            // app.handle() は参照なので、持ち出すときは clone() する
            let _ = APP.set(app.handle().clone());
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![start_job, quit])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

exit() は終了のイベントを経て終わるので、終了時の後始末 が動きます。std::process::exit() はそれを飛ばすので、アプリの終了には使いません。

2. フロントエンドから呼び出す (TypeScript)

AppHandle の引数は JS から渡しません。イベントの購読(core:event:default)は core:default に含まれます。

import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

const unlisten = await listen<number>('job-progress', (event) => {
  console.log(`進み具合 ${event.payload}%`);
  if (event.payload === 100) unlisten();
});

await invoke('start_job'); // すぐに戻る
try {
  await invoke('start_job'); // 実行中なのでエラーになる
} catch (e) {
  console.log(e);
}

動作確認

npm run tauri dev で起動して上のコードを実行すると、DevTools のコンソールに次のように出ます。2 回目の start_job はすぐに失敗し、進み具合は 0.5 秒ごとに届きます。すべて届いた後なら、もう一度始められます。

すでに実行中です
進み具合 20%
進み具合 40%
進み具合 60%
進み具合 80%
進み具合 100%

invoke('quit') を呼ぶと、ウィンドウが閉じてアプリが終わります。

よくあるエラーと対処法

  • 「no method named get_webview_window found for struct AppHandle<R> in the current scope」: use tauri::Manager; がありません。emit で同じエラーなら tauri::Emitter、listen なら tauri::Listener です。
  • 「borrowed data escapes outside of closure」: setup の中で app.handle() の参照をそのままスレッドへ渡しています。app.handle().clone() を渡します。app 自体を渡したときは「cannot be sent between threads safely」になります。
  • ウィンドウを作ろうとすると固まる(Windows): async でないコマンドや、メニューなどのイベント処理の中で WebviewWindowBuilder の build() を呼んでいます。コマンドを async にするか、別スレッドで作ります。
  • 再起動したら終了時の後始末が動かない: restart() はメインスレッドから呼ぶと(async でないコマンドもここに当たります)終了のイベントを飛ばして再起動します。後始末が要るなら request_restart() を使います。

OS ごとの違いと注意点

  • Windows: 上のとおり、同期コマンドやイベント処理の中でウィンドウを作ると固まることがあります。
  • macOS: set_activation_policy()(Dock に出すか)、set_dock_visibility()、アプリ全体を隠す hide() / show() は macOS 専用です。#[cfg(target_os = "macos")] で囲んで呼びます。
  • スレッド: ウィンドウ操作など Tauri の API は別スレッドから呼べます。run_on_main_thread() が要るのは、OS の API を直接呼ぶなど、メインスレッドで動かす必要がある処理だけです。
  • 型: アプリのコードでは tauri::AppHandle と書けば足ります。プラグインなど、複数の実行環境に対応させるコードでは AppHandle<R>(R: tauri::Runtime)と総称型で受けます。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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