開いている全ウィンドウのリストを取得する

getAllWebviewWindows()(権限は core:default に含まれる)と Rust の webview_windows() で全ウィンドウを列挙する。状態の読み取り、まとめて閉じる処理、開閉に合わせた一覧の更新も示す。

ウィンドウ 対象: Tauri 2.x 更新日: 読了目安: 約9分 win-029
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 一覧と各ウィンドウの状態を取得する
  4. 自分以外をまとめて閉じる
  5. 開閉に合わせて一覧を更新する
  6. 2. バックエンドから実装する (Rust)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

「設定ウィンドウがもう開いているか確かめる」「メイン以外をまとめて閉じる」「開いている文書をメニューに並べる」には、アプリが今持っているウィンドウの一覧が必要です。JS では @tauri-apps/api/webviewWindow の getAllWebviewWindows()、Rust では webview_windows() で取得します。一覧の各要素はラベル(ウィンドウを作るときに付ける名前)で識別されるハンドルで、非表示のウィンドウも含まれ、並び順は決まっていません。取得後の開閉への備えと、一覧を最新に保つ方法も説明します。

前提条件

プラグインは不要です。getAllWebviewWindows() と、ラベルで 1 つ探す WebviewWindow.getByLabel() の権限 core:window:allow-get-all-windows は core:window:default に含まれます。title() / isVisible() / isFocused() / isMinimized() などの読み取りも同じなので、core:default があれば追加は不要です。一覧のウィンドウを操作するなら、閉じる core:window:allow-close、確認なしで閉じる core:window:allow-destroy、前面に出す core:window:allow-set-focus を追加します。権限は操作される側ではなく、呼び出す側のウィンドウの capability で判定されます。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:allow-close",
    "core:window:allow-destroy",
    "core:window:allow-set-focus"
  ]
}

取りたいものと API の対応は次のとおりです。

取りたいものJSRust
全ウィンドウgetAllWebviewWindows()app.webview_windows()
ラベルで 1 つWebviewWindow.getByLabel(label)app.get_webview_window(label)
自分自身getCurrentWebviewWindow()コマンド引数の tauri::WebviewWindow

@tauri-apps/api/window の getAllWindows() も同じ一覧を Window 型で返しますが、ページ側の操作もできる WebviewWindow 版が無難です。

1. フロントエンドから実装する (TypeScript)

一覧と各ウィンドウの状態を取得する

返ってくる WebviewWindow はラベルを持つだけのハンドルで、タイトルや表示状態はメソッドで 1 つずつ問い合わせます。問い合わせは非同期なので、forEach(async ...) では結果を待たずに先へ進んでしまいます。Promise.all() でまとめて待ちます。また、一覧を取ってから状態を読むまでの間にそのウィンドウが閉じられると「window not found」で失敗するため、1 件ずつ例外を拾います。

import { getAllWebviewWindows } from '@tauri-apps/api/webviewWindow';

export type WindowInfo = { label: string; title: string; visible: boolean; focused: boolean };

export async function listWindows(): Promise<WindowInfo[]> {
  const wins = await getAllWebviewWindows();
  const infos = await Promise.all(
    wins.map(async (w): Promise<WindowInfo | null> => {
      try {
        const [title, visible, focused] = await Promise.all([w.title(), w.isVisible(), w.isFocused()]);
        return { label: w.label, title, visible, focused };
      } catch {
        return null; // 読み取りの途中で閉じられたウィンドウ
      }
    }),
  );
  // 並び順は保証されないので、ラベルで並べ替える
  return infos.filter((x): x is WindowInfo => x !== null).sort((a, b) => a.label.localeCompare(b.label));
}

開いているかだけなら WebviewWindow.getByLabel('settings') が null かどうかを見ます(開いていれば前面に出す例は win-017)。

自分以外をまとめて閉じる

close() はユーザーが閉じるボタンを押したのと同じ扱いで、閉じる前の確認 が登録されたウィンドウはそこで止まることがあります。確認を飛ばして閉じるなら destroy() です。どちらも Promise が解決した時点では、まだ一覧に残っていることがあります。閉じ終わりを知りたいときは次の節の tauri://destroyed を使います。

import { getAllWebviewWindows, getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

export async function closeOthers(force = false) {
  const me = getCurrentWebviewWindow().label;
  const others = (await getAllWebviewWindows()).filter((w) => w.label !== me);
  // force なら確認を飛ばす(core:window:allow-destroy)。通常は close(core:window:allow-close)
  await Promise.allSettled(others.map((w) => (force ? w.destroy() : w.close())));
}

開閉に合わせて一覧を更新する

一覧を何秒かおきに取り直す必要はありません。ウィンドウが作られるとアプリ全体に tauri://window-created(TauriEvent.WINDOW_CREATED)が送られ、ペイロードに新しいラベルが入ります。閉じられたときはそのウィンドウ宛てに tauri://destroyed が届くので、各ハンドルの once() で受け取ります。

import { listen, TauriEvent } from '@tauri-apps/api/event';
import { getAllWebviewWindows, WebviewWindow } from '@tauri-apps/api/webviewWindow';

const labels = new Set<string>();
const render = () => console.log('open windows:', [...labels].sort().join(', '));

async function track(w: WebviewWindow) {
  labels.add(w.label);
  await w.once(TauriEvent.WINDOW_DESTROYED, () => {
    labels.delete(w.label);
    render();
  });
}

for (const w of await getAllWebviewWindows()) await track(w);
render();

await listen<{ label: string }>(TauriEvent.WINDOW_CREATED, async ({ payload }) => {
  const w = await WebviewWindow.getByLabel(payload.label);
  if (w) {
    await track(w);
    render();
  }
});

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

webview_windows() は HashMap<String, WebviewWindow> を返すので、列挙の順序は毎回変わりえます。画面に出すならラベルなどで並べ替えます。Manager トレイトのメソッドなので use tauri::Manager; が必要です。なお windows() / get_window() はフィーチャー unstable を有効にしないと使えないため、通常は webview_windows() / get_webview_window() を使います。

Rust 側では、全ウィンドウに共通のイベントを Builder::on_window_event で受け取れます。次の例は、ウィンドウが閉じられるたびにラベルをフロントエンドへ知らせます。

use tauri::{Emitter, Manager};

#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct WindowSummary {
    label: String,
    title: String,
    visible: bool,
    focused: bool,
}

#[tauri::command]
fn list_windows(app: tauri::AppHandle) -> Vec<WindowSummary> {
    let mut list: Vec<WindowSummary> = app
        .webview_windows()
        .into_values()
        .map(|w| WindowSummary {
            label: w.label().to_string(),
            title: w.title().unwrap_or_default(),
            visible: w.is_visible().unwrap_or(false),
            focused: w.is_focused().unwrap_or(false),
        })
        .collect();
    list.sort_by(|a, b| a.label.cmp(&b.label)); // HashMap の順序は決まっていない
    list
}

/// 呼び出し元以外のウィンドウを閉じる
#[tauri::command]
fn close_others(window: tauri::WebviewWindow) -> Result<(), String> {
    for (label, other) in window.app_handle().webview_windows() {
        if label != window.label() {
            other.close().map_err(|e| e.to_string())?;
        }
    }
    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .on_window_event(|window, event| {
            if let tauri::WindowEvent::Destroyed = event {
                let _ = window.app_handle().emit("window-closed", window.label());
            }
        })
        .invoke_handler(tauri::generate_handler![list_windows, close_others])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

type WindowSummary = { label: string; title: string; visible: boolean; focused: boolean };

console.table(await invoke<WindowSummary[]>('list_windows'));
await listen<string>('window-closed', ({ payload }) => console.log('closed:', payload));
await invoke('close_others'); // Rust 側で閉じるので JS の close 権限は不要

動作確認

npm run tauri dev で起動し、メインから editor-1 と settings を開き、settings を hide() で隠してから listWindows() を呼ぶと次のようになります。隠したウィンドウも一覧に残ります。

[
  { label: "editor-1", title: "memo.txt", visible: true,  focused: false },
  { label: "main",     title: "My App",   visible: true,  focused: true  },
  { label: "settings", title: "設定",     visible: false, focused: false }
]

追従のコードを読み込んだ状態で editor-1 を閉じると、open windows: main, settings と出ます。

よくあるエラーと対処法

  • 「window not found」: 一覧を取った後に閉じられたウィンドウを操作しています。取得と操作の間は短くし、失敗を try/catch で拾います。
  • 「window.close not allowed. Permissions associated with this command: core:window:allow-close」: 他のウィンドウを閉じる権限がありません。この文面はデバッグビルドのもので、リリースビルドでは「Command plugin:window|close not allowed by ACL」だけになります。
  • 閉じたはずのウィンドウが一覧に入っている: hide() しただけのウィンドウは一覧に残ります。見えているものだけなら isVisible() で絞ります(表示・非表示)。
  • Rust で no method named windows という趣旨のコンパイルエラー: windows() はフィーチャー unstable が必要です。webview_windows() を使います。
  • 一覧の並びが毎回違う: 順序は保証されません。ラベルやタイトルで並べ替えます。

OS ごとの違いと注意点

  • 一覧に入るのは Tauri が作ったウィンドウだけです。ファイル選択などのダイアログは入りません。
  • 設定ファイルで "create": false にしたウィンドウは、作られるまで一覧に入りません。
  • ラベルは作成時に決まり、後から変えられません。「同じ文書は 1 つのウィンドウで開く」なら、文書の ID をラベルに入れておくと getByLabel() だけで判定できます(別のウィンドウ作成時にデータを渡す)。
  • 取得した WebviewWindow は長く保持せず、使う直前に取り直します。閉じられた後に操作すると「window not found」になります。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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