設定画面やヘルプのように、メインとは別のウィンドウを開く方法です。サブウィンドウ専用の HTML を Vite の Multi-page 機能で用意し、JS の new WebviewWindow() か Rust の WebviewWindowBuilder で開きます。つまずきやすいのは「本番ビルドだけサブウィンドウにメイン画面が出る」問題と、新しいウィンドウ側の権限です。
ここでは開くところまでを扱います。続きは 親ウィンドウに対してモーダル表示する、別のウィンドウ作成時にデータを渡す、ウィンドウを閉じる前に確認ダイアログを出す を参照してください。
前提条件
プラグインは不要です。JS からウィンドウを作る core:webview:allow-create-webview-window は core:default に含まれないので追加します。既に開いているウィンドウを前に出す setFocus() / unminimize() と、サブウィンドウ自身の「閉じる」ボタンで使う close() の権限も足します。
権限は呼び出したウィンドウのラベルで判定されます。サブウィンドウから close() などを呼ぶなら、そのラベル(ここでは settings)も windows に入れます。入れ忘れると、メインでは動くコードがサブウィンドウでだけ失敗します。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main and settings windows",
"windows": ["main", "settings"],
"permissions": [
"core:default",
"core:webview:allow-create-webview-window",
"core:window:allow-set-focus",
"core:window:allow-unminimize",
"core:window:allow-close"
]
}
この書き方ではサブウィンドウにも作成の権限が付くので、避けたいなら main 用と settings 用に capability ファイルを分けます。同じ画面を複数開くならラベルを note-1、note-2 のように変え、windows に "note-*" と書きます。
1. フロントエンドから実装する (TypeScript)
サブウィンドウ用のページを Vite に追加する
サブウィンドウ用の HTML をプロジェクトの直下に置き、専用のスクリプトを読み込ませます。
.
├── index.html # メインウィンドウ
├── settings.html # サブウィンドウ
├── src/
│ ├── main.ts
│ └── settings.ts
├── src-tauri/
└── vite.config.ts
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>設定</title>
</head>
<body>
<h1>設定</h1>
<button id="close">閉じる</button>
<script type="module" src="/src/settings.ts"></script>
</body>
</html>
開発サーバーは置いた HTML をそのまま配信するので npm run tauri dev ではこれだけで開けますが、本番ビルドで出力されるのは既定で index.html だけです。vite.config.ts のビルド入力に 2 つ目のページを加えます。
import { defineConfig } from 'vite';
export default defineConfig({
// clearScreen や server など、テンプレートにある既存の設定はそのまま残す
build: {
// Vite 7 以前は rollupOptions(Vite 8 でも別名として使える)
rolldownOptions: {
input: {
main: 'index.html',
settings: 'settings.html',
},
},
},
});
Vite のドキュメントの例は resolve(__dirname, 'settings.html') のような絶対パスですが、@types/node の無いテンプレートではエディターが node:path の行を型エラーにします(ビルドは通ります)。npm スクリプトはプロジェクトの直下で動くので、相対パスなら Node の型は要りません。
ウィンドウを開く
new WebviewWindow() はウィンドウの作成を依頼するだけで、成否は tauri://created / tauri://error イベントで届きます。await で待てるように Promise に包みます。url はアプリ内のパスで、開発中は開発サーバー、本番ではアプリに埋め込んだファイルが読まれます。
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
const LABEL = 'settings';
// 作成の成否を Promise で受け取る(登録は new の直後に同期で行う)
function waitCreated(win: WebviewWindow): Promise<void> {
return new Promise((resolve, reject) => {
void win.once('tauri://created', () => resolve());
void win.once<string>('tauri://error', (e) => reject(new Error(String(e.payload))));
});
}
export async function openSettings(): Promise<WebviewWindow> {
// 開いていれば新しく作らず、最小化を戻して前に出す
const existing = await WebviewWindow.getByLabel(LABEL);
if (existing) {
await existing.unminimize();
await existing.setFocus();
return existing;
}
const win = new WebviewWindow(LABEL, {
url: '/settings.html',
title: '設定',
width: 600,
height: 400,
resizable: false,
center: true,
});
await waitCreated(win);
return win;
}
document.querySelector('#open-settings')?.addEventListener('click', () => {
openSettings().catch((e) => console.error('サブウィンドウを開けません:', e));
});
tauri://created はウィンドウができた時点で届き、サブウィンドウのページの読み込みはまだ終わっていません。直後にイベントでデータを送ると取りこぼすので、受け渡しは win-019 の手順で行います。
サブウィンドウ側のコード
settings.ts はメインとは別の JS として動くので、変数や状態は共有されません。自分自身は getCurrentWindow() で取れます。
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
console.log(`label: ${win.label}`); // "settings"
// ボタンか Esc キーで閉じる(core:window:allow-close が必要)
document.querySelector('#close')?.addEventListener('click', () => void win.close());
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') void win.close();
});
2. バックエンドから実装する (Rust)
Rust のコマンドで開けば、JS にウィンドウ作成の権限を渡さずに済みます。この権限があるとスクリプトから任意の URL のウィンドウを開けるので、開く画面が決まっているならこちらが安全です。
ウィンドウを作るコマンドは async にします(理由は「OS ごとの違い」を参照)。大きさなどを設定ファイルで管理したいなら、app.windows に "create": false を付けて定義し、WebviewWindowBuilder::from_config() で作る方法もあります。
メインを閉じても、サブウィンドウが 1 つでも残っていればアプリは終了しません。まとめて終わらせたい場合は、main の Destroyed イベントで exit() を呼びます。
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder, WindowEvent};
#[tauri::command]
async fn open_settings(app: tauri::AppHandle) -> Result<(), String> {
if let Some(existing) = app.get_webview_window("settings") {
existing.unminimize().map_err(|e| e.to_string())?;
return existing.set_focus().map_err(|e| e.to_string());
}
WebviewWindowBuilder::new(&app, "settings", WebviewUrl::App("settings.html".into()))
.title("設定")
.inner_size(600.0, 400.0)
.resizable(false)
.center()
.build()
.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| {
// main が閉じられたら、残っているサブウィンドウごと終了する
if window.label() == "main" {
if let WindowEvent::Destroyed = event {
window.app_handle().exit(0);
}
}
})
.invoke_handler(tauri::generate_handler![open_settings])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
await invoke('open_settings'); // この場合 main の capability に作成の権限は要らない
動作確認
npm run tauri dev で起動し、#open-settings のボタンを押すと 600 x 400 の「設定」ウィンドウが画面中央に開きます。もう一度押しても 2 つ目は開かず、既存のウィンドウが前に出ます。開いた状態でメイン側から次のコードを実行すると、ラベルの一覧が確かめられます。
import { getAllWebviewWindows } from '@tauri-apps/api/webviewWindow';
console.log((await getAllWebviewWindows()).map((w) => w.label)); // ["main", "settings"]
最後に npm run tauri build で作ったアプリでも、サブウィンドウに設定画面が出ることを確かめます。
よくあるエラーと対処法
- 「webview.create_webview_window not allowed. Permissions associated with this command: core:webview:allow-create-webview-window」: JS から作る権限がありません。この文面は開発ビルドのもので、リリースビルドでは「Command plugin:webview|create_webview_window not allowed by ACL」になります。
- サブウィンドウの「閉じる」だけが効かない: DevTools に
window.close not allowed on window "settings", webview "settings", URL: localで始まるエラーが出ていれば、settingsが capability のwindowsに入っていません。 - 本番ビルドだけサブウィンドウにメイン画面が出る:
settings.htmlがビルドされていません。Tauri は見つからないページの代わりにindex.htmlを表示するため、エラーにならずメイン画面が出ます。vite.config.tsのinputを確かめます。 - 「a window with label
settingsalready exists」: 同じラベルで 2 回作ろうとしています。ボタンの連打でも起こるので、getByLabel()で確かめるか、開く処理の間はボタンを無効にします。 - 「runtime error: Window labels must only include alphanumeric characters,
-,/,:and_.」: ラベルに.や空白が入っています。使えるのは英数字と-/:_だけです。
OS ごとの違いと注意点
- Windows: Rust の同期コマンドやイベントハンドラーの中でウィンドウを作ると固まることが知られています。コマンドは
asyncにするか、別スレッドで作ります。JS のnew WebviewWindow()は内部で非同期のコマンドを使うので、この問題は起きません。 - 共通: 起動時に作って隠しておき、必要なときに表示する方法もあります。開くたびにページを読み込まないので速く、入力途中の状態も残ります(ウィンドウを表示・非表示にする)。
