Window State プラグインで位置を記憶する

Window State プラグインでウィンドウの位置・サイズ・最大化を終了時に保存し、次の起動で復元する。StateFlags での項目の絞り込み、記憶しないウィンドウの指定、保存のタイミングと手動保存も示す。

プラグイン拡張 対象: Tauri 2.x 更新日: 読了目安: 約8分 plugin-005
目次
  1. 前提条件
  2. 保存される項目と保存先
  3. 1. 登録して、記憶する項目とウィンドウを決める (Rust)
  4. 保存と復元のタイミング
  5. 2. 手動で保存する (TypeScript)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

前回閉じたときの位置と大きさで開くのは、デスクトップアプリに当然期待される動きです。公式の Window State プラグインを登録すると、ウィンドウの位置・サイズ・最大化などが終了時にファイルへ保存され、次にそのウィンドウが作られたときに復元されます。保存される項目と絞り込み方、記憶させないウィンドウの指定、ファイルに書かれるタイミング、手動で保存する方法を説明します。

前提条件

npm run tauri add window-state

Windows / macOS / Linux 用で、iOS / Android では使えません。tauri add はクレートをデスクトップ向けだけに追加し、lib.rs に登録を足し、capabilities/desktop.json に window-state:default を書き込みます。自動の保存と復元に権限は要らず、この権限(saveWindowState() / restoreState() / filename() を許可。core:default には含まれない)が要るのは 2 章の JS の API を呼ぶときだけです。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "desktop-capability",
  "platforms": ["macOS", "windows", "linux"],
  "windows": ["main"],
  "permissions": [
    "window-state:default"
  ]
}

保存される項目と保存先

記憶する項目は StateFlags の 6 つで、既定はすべてです。

StateFlags記憶するもの
SIZE内側のサイズ(物理ピクセル)
POSITION外側の位置(物理ピクセル)
MAXIMIZED最大化しているか
VISIBLE表示しているか(復元の後に表示する)
DECORATIONSタイトルバーと枠の有無
FULLSCREENフルスクリーンか

最大化・最小化している間はサイズと位置を記録し直さないので、最大化したまま終了すると次回は最大化で開き、解除すると最大化する前の大きさと位置に戻ります。最小化の状態は記憶しません。

記録はウィンドウのラベルごとに、アプリの設定フォルダ(appConfigDir())の .window-state.json へ書かれます。フォルダは Windows なら %APPDATA%\<識別子>、macOS なら ~/Library/Application Support/<識別子>、Linux なら ~/.config/<識別子> で、<識別子> は tauri.conf.json の identifier です。

1. 登録して、記憶する項目とウィンドウを決める (Rust)

プラグインはウィンドウが作られた時点で復元するので、設定ファイルのウィンドウより先に登録されるよう Builder に直接つなぎます。tauri add が足す行は条件なしなので、モバイル向けにもビルドするなら #[cfg(desktop)] で囲みます。

use tauri::Manager;

// デスクトップ専用なので、プラグインを使うコードには cfg(desktop) を付ける
#[cfg(desktop)]
fn window_state_plugin() -> tauri::plugin::TauriPlugin<tauri::Wry> {
    use tauri_plugin_window_state::{Builder, StateFlags};
    Builder::new()
        // 枠の有無は記憶しない(tauri.conf.json の decorations を変えたらそれが効くように)
        .with_state_flags(StateFlags::all() - StateFlags::DECORATIONS)
        // スプラッシュ画面は対象外
        .with_denylist(&["splash"])
        // "popup-" で始まる一時的なウィンドウも対象外(false を返すと記憶しない)
        .with_filter(|label| !label.starts_with("popup-"))
        .build()
}

// フォーカスが外れたらファイルに書いておく(強制終了やクラッシュに備える)
#[cfg(desktop)]
fn save_on_blur(window: &tauri::Window, event: &tauri::WindowEvent) {
    use tauri_plugin_window_state::{AppHandleExt, StateFlags};
    if let tauri::WindowEvent::Focused(false) = event {
        let _ = window.app_handle().save_window_state(StateFlags::all());
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let builder = tauri::Builder::default();
    #[cfg(desktop)]
    let builder = builder
        .plugin(window_state_plugin())
        .on_window_event(save_on_blur);
    builder
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
メソッド働き
with_state_flags()記憶する項目を絞る
with_denylist()指定したラベルを対象外にする
with_filter()ラベルごとに判定し、false なら対象外
skip_initial_state()記憶はするが、作成時の自動復元はしない
map_label()ラベルを読み替え、複数のウィンドウで記録を共有する

記録はラベルで引くので、開くたびに違うラベル(UUID など)を付けるウィンドウは、記録が増えるだけで復元されません。map_label(|l| if l.starts_with("editor-") { "editor" } else { l }) のようにまとめます。

保存と復元のタイミング

  • 復元: ウィンドウが作られたときです。実行中に作ったウィンドウも、同じラベルなら前回の状態で開きます。
  • 記録: 移動・リサイズのたびにメモリ上で更新し、ウィンドウを閉じるときにもその時点の状態を控えます。
  • ファイルへの書き込み: アプリの終了時だけです。最後のウィンドウを閉じたときや exit() / relaunch()(Process プラグイン)、Rust の app.exit() では書かれますが、クラッシュや強制終了、std::process::exit() では書かれません。1 章の save_on_blur はその備えです。

tauri.conf.json でウィンドウに "visible": false を付けておくと、プラグインが位置を戻してから表示するので、既定の位置で一瞬表示されるチラつきが出ません。VISIBLE を外した場合は自分で show() します。

2. 手動で保存する (TypeScript)

saveWindowState() は開いているすべてのウィンドウの状態をファイルに書きます。引数を省くと 1 章の with_state_flags() の項目で保存します。トレイに隠す作り(最小化ボタンでトレイに格納するようにする)ではアプリがなかなか終了しないので、隠す前に呼んでおきます。filename() で保存先のフルパスも案内できます。

import { appConfigDir, join } from '@tauri-apps/api/path';
import { filename, saveWindowState } from '@tauri-apps/plugin-window-state';

// 今の配置をファイルに書き、保存先のフルパスを返す
export async function saveLayoutNow(): Promise<string> {
  await saveWindowState(); // window-state:allow-save-window-state が必要
  return join(await appConfigDir(), await filename());
}

動作確認

npm run tauri dev で起動し、ウィンドウを動かして大きさを変えてから × ボタンで閉じます。ターミナルの Ctrl+C や、Rust の変更による自動再起動では正常に終了しないので、プラグインだけでは保存されません(save_on_blur があれば、ターミナルに切り替えた時点で保存されます)。再び起動すると前回の位置と大きさで開きます。拡大率 150% の Windows で論理ピクセル 1024 x 768 のウィンドウなら、ファイルは次のようになります。

{
  "main": {
    "width": 1536,
    "height": 1152,
    "x": 412,
    "y": 180,
    "prev_x": 398,
    "prev_y": 176,
    "maximized": false,
    "visible": true,
    "decorated": true,
    "fullscreen": false
  }
}

prev_x / prev_y は、最大化する前の位置に戻すための値です。

よくあるエラーと対処法

  • 「window-state.save_window_state not allowed. Permissions associated with this command: window-state:allow-save-window-state, window-state:default」: window-state:default がありません。リリースビルドでは「Command plugin:window-state|save_window_state not allowed by ACL」になります。
  • main ウィンドウが前回の位置に戻らない: setup の中で app.handle().plugin() として登録すると、設定ファイルのウィンドウはそれより先に作られるので対象から外れます。Builder につなぎます。
  • tauri.conf.json の width や decorations を変えても効かない: 記録が優先されます。アプリを終了してから .window-state.json を消します(起動中に消しても終了時に書き直されます)。
  • 起動してもウィンドウが出てこない: "visible": false のウィンドウを隠したまま終了すると、次回も表示されません。トレイから表示できるようにするか、VISIBLE を外して自分で show() します。
  • モバイル向けのビルドだけ tauri_plugin_window_state が見つからない趣旨のエラーになる: クレートはデスクトップ向けにしか入っていないので、#[cfg(desktop)] で囲みます。

OS ごとの違いと注意点

  • iOS / Android: 非対応です。
  • モニターの抜き差し: 記録した範囲の四隅のどれかがいずれかのモニターに入っていれば位置を戻し、どこにも入らなければ位置だけ戻さずに開きます。外部モニターを外した後に、画面外に開いて見失うことはありません。
  • 設定ファイルの位置: x / y や center を指定していても記録が優先され、設定ファイルの位置は初回だけ使われます(ウィンドウの位置を指定する)。
  • 自前の保存との併用: ウィンドウのサイズを変更する のように onResized() で自分で保存・復元する処理とはどちらか一方にします。表示と非表示の切り替えは ウィンドウを表示・非表示にする を参照してください。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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