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 と書く |
setup | app.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_windowfound for structAppHandle<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)と総称型で受けます。
