Rust から特定のウィンドウを操作する(WebviewWindow)

Rust でラベルから WebviewWindow を取り出し、呼び出し元や別のウィンドウを操作する。WebviewWindow と Window の違い、閉じたウィンドウの扱い、1 つのウィンドウへのイベント送信も示す。

Rust バックエンド 対象: Tauri 2.x 更新日: 読了目安: 約9分 rust-009
目次
  1. 前提条件
  2. 1. バックエンドから実装する (Rust)
  3. 呼び出し元とラベルで取る
  4. WebviewWindow と Window の違い
  5. 1 つのウィンドウにだけ送る・イベントを受ける
  6. 取った値を持ち続けない
  7. 2. フロントエンドから呼び出す (TypeScript)
  8. 動作確認
  9. よくあるエラーと対処法
  10. OS ごとの違いと注意点
  11. 関連レシピ

Tauri v2 に WindowHandle という名前の型はなく、Rust から特定のウィンドウを操作するときは WebviewWindow という値を手に入れて、そのメソッドを呼びます。手に入れ方は、コマンドの呼び出し元を引数で受け取るか、AppHandle からラベルで探すかの 2 通りです。ここでは取り方と WebviewWindow / Window の違い、閉じた後の扱いを説明し、サイズやタイトルなど個々の操作はウィンドウカテゴリの各レシピに任せます。

前提条件

プラグインは不要です。Rust から直接呼ぶウィンドウ操作は capability の権限の対象外なので、JS に set_size などの権限を渡したくない場合にも使えます。ラベルは tauri.conf.json の label(省略すると main)か、ウィンドウを作るときに付けた名前で、使える文字は英数字と - / : _ です。

やりたいこと取り方
コマンドを呼んだウィンドウを操作する引数に window: tauri::WebviewWindow
ラベルで指定したウィンドウを操作するapp.get_webview_window("settings")
開いているウィンドウをすべて扱うapp.webview_windows()(ラベルと値の HashMap)
特定のウィンドウのイベントを受ける取った値の on_window_event()

取った後の操作は、それぞれのレシピを参照してください。

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

呼び出し元とラベルで取る

get_webview_window() は Option を返し、そのラベルのウィンドウが無ければ(まだ作られていない、閉じられた)None です。use tauri::Manager; が必要です。

use tauri::{AppHandle, Emitter, Manager, WebviewWindow};

/// 呼び出し元のウィンドウ。ラベルを知らなくても受け取れる
#[tauri::command]
fn whoami(window: WebviewWindow) -> String {
    window.label().to_string()
}

/// ラベルで指定したウィンドウを、隠れていても最小化されていても前面に出す
#[tauri::command]
fn bring_to_front(app: AppHandle, label: String) -> Result<(), String> {
    let target = app
        .get_webview_window(&label)
        .ok_or_else(|| format!("window '{label}' not found"))?;
    target.unminimize().map_err(|e| e.to_string())?;
    target.show().map_err(|e| e.to_string())?;
    target.set_focus().map_err(|e| e.to_string())
}

/// 呼び出し元以外のウィンドウをすべて閉じ、閉じたラベルを返す
#[tauri::command]
fn close_others(window: WebviewWindow) -> Result<Vec<String>, String> {
    let mut closed = Vec::new();
    for (label, other) in window.app_handle().webview_windows() {
        if label != window.label() {
            other.close().map_err(|e| e.to_string())?;
            closed.push(label);
        }
    }
    Ok(closed)
}

close() は × ボタンと同じく閉じる前の確認を通り、destroy() は確認なしで閉じます。

WebviewWindow と Window の違い

WebviewWindow は「ウィンドウ」と「中の Web ページ 1 枚」の組で、設定ファイルや WebviewWindowBuilder で作る普通のウィンドウはすべてこれです。Window は枠だけを指し、ページの操作はできません。

WebviewWindowWindow
ラベルで取るget_webview_window()get_window()(unstable 機能が必要)
コマンドの引数window: tauri::WebviewWindowwindow: tauri::Window
枠の操作(set_title() など)できるできる
ページの操作(eval() / reload() / navigate() / set_zoom())できるできない

Window が本来役立つのは 1 つのウィンドウに複数のページを並べる場合で、その機能自体が unstable の扱いです。普段は WebviewWindow を使えば足ります。

/// 枠の操作だけなら Window で受けてもよい
#[tauri::command]
fn toggle_maximize(window: tauri::Window) -> Result<bool, String> {
    let maximized = window.is_maximized().map_err(|e| e.to_string())?;
    if maximized {
        window.unmaximize().map_err(|e| e.to_string())?;
    } else {
        window.maximize().map_err(|e| e.to_string())?;
    }
    Ok(!maximized)
}

/// ページの操作は WebviewWindow でしかできない
#[tauri::command]
fn reload_window(app: AppHandle, label: String) -> Result<(), String> {
    let target = app.get_webview_window(&label).ok_or("window not found")?;
    target.reload().map_err(|e| e.to_string())
}

1 つのウィンドウにだけ送る・イベントを受ける

emit() は、どのウィンドウの値から呼んでも全ウィンドウへ送ります。1 つに絞るには emit_to() でラベルを指定し、受ける側は getCurrentWebviewWindow().listen() で購読します。@tauri-apps/api/event の listen() は宛先に関係なく受け取るので、絞った意味がなくなります。

特定のウィンドウのイベントは、取った値の on_window_event() で受けます。次の例は、メインウィンドウが閉じたら、サブウィンドウが残っていてもアプリを終えます(終了時の処理)。

/// 指定したウィンドウにだけ送る
#[tauri::command]
fn send_to(app: AppHandle, label: String, text: String) -> Result<(), String> {
    app.emit_to(&label, "message", text).map_err(|e| e.to_string())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            // setup の時点で、設定ファイルのウィンドウは作られている
            if let Some(main) = app.get_webview_window("main") {
                let handle = app.handle().clone();
                main.on_window_event(move |event| {
                    if let tauri::WindowEvent::Destroyed = event {
                        handle.exit(0);
                    }
                });
            }
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            whoami,
            bring_to_front,
            close_others,
            toggle_maximize,
            reload_window,
            send_to
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

取った値を持ち続けない

WebviewWindow は複製してスレッドや State に入れられますが、ウィンドウが閉じた後も値は残り、操作は効かなかったりエラーになったりします。長く持つのはラベルの文字列にして、使う直前に get_webview_window() で取り直します。ウィンドウごとのデータは、ラベルをキーにした HashMap で State に持ちます(State と Mutex でアプリの状態を管理する)。

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

WebviewWindow の引数は JS から渡さず、Tauri が呼び出し元を入れます。

import { invoke } from '@tauri-apps/api/core';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

// このウィンドウ宛て(emit_to で指定されたもの)だけを受け取る
await getCurrentWebviewWindow().listen<string>('message', (event) => {
  console.log(`受信: ${event.payload}`);
});

console.log(await invoke<string>('whoami'));
await invoke('send_to', { label: 'main', text: 'こんにちは' });
await invoke('bring_to_front', { label: 'settings' }).catch((e) => console.log(e));

JS だけで別のウィンドウを扱うなら WebviewWindow.getByLabel() を使いますが、操作ごとに権限が要ります(開いている全ウィンドウのリストを取得する)。

動作確認

npm run tauri dev で起動し、settings ウィンドウが無い状態で上のコードを実行すると、コンソールに次のように出ます。

main
受信: こんにちは
window 'settings' not found

サブウィンドウ を開いてからメインウィンドウを閉じると、サブウィンドウも一緒に閉じてアプリが終わります。

よくあるエラーと対処法

  • 「no method named get_webview_window found for struct AppHandle<R> in the current scope」: use tauri::Manager; がありません。window.app_handle() も同じトレイトのメソッドです。
  • get_webview_window() が None になる: ラベルの綴り違いか、まだ作られていないか、閉じられた後です。webview_windows() のキーを出力して確かめます。
  • 「a window with label settings already exists」: 同じラベルで 2 回作ろうとしています。作る前に get_webview_window() で確かめ、あれば前に出します。
  • 「Window labels must only include alphanumeric characters, ...」を含むエラー: ラベルに . や空白など使えない文字が入っています。ファイル名などから作るときは置き換えます。
  • 別のウィンドウにもイベントが届く: 送る側が emit() か、受ける側がグローバルの listen() です。emit_to() とウィンドウの listen() を組み合わせます。

OS ごとの違いと注意点

  • OS のウィンドウハンドル: 「WindowHandle」で探している場合、OS の API や他のライブラリに渡すネイティブのハンドルのことかもしれません。Tauri では Windows の hwnd()、macOS の ns_window()、Linux の gtk_window() がそれに当たり、どれもその OS でしか使えないので #[cfg(target_os = "...")] で囲みます。gtk_window() はメインスレッドでしか使えません。
  • 権限: Rust の操作は capability に縛られない分、JS から任意のラベルを受け取るコマンドは、どのウィンドウでも操作できる入り口になります。対象のラベルを決め打ちにするか、許可するラベルを確かめてから操作します。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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