ウィンドウを閉じる前に確認ダイアログを出す

Recipe ID: win-016

ユーザーがウィンドウを閉じようとした際に、誤操作防止のための確認ダイアログを表示する方法を紹介します。

ドキュメントの編集内容が保存されていない場合や、重要な処理の実行中に、うっかりアプリを閉じてしまう事故を防ぎます。
フロントエンドの onCloseRequested メソッドを使用した実装方法を解説します。

前提条件

Permissions (権限) の設定

src-tauri/capabilities/default.json に以下の権限を追加します。

destroy メソッドを使用する場合は、core:window:allow-destroy が必要です。

{
  "permissions": [
    ...,
    "core:window:default",
    "core:window:allow-destroy"
  ]
}

1. フロントエンドから変更する (TypeScript)

onCloseRequested メソッドを使用し、ウィンドウが閉じられるイベントをフックします。
ユーザーがキャンセルを選択した場合、event.preventDefault() を呼び出すことでウィンドウの終了を阻止できます。

サンプルコード

import { getCurrentWindow } from '@tauri-apps/api/window';

const appWindow = getCurrentWindow();

await appWindow.onCloseRequested(async (event) => {
  const confirmed = window.confirm('保存せずに終了しますか?');
  if (!confirmed) {
    // ユーザーがキャンセルした場合、ウィンドウを閉じる動作をキャンセル
    event.preventDefault();
  }
});

2. 非同期の確認処理を行う場合 (TypeScript)

window.confirm は処理をブロックしますが、独自のUIを使用した確認ダイアログなどは非同期で結果を待ちます。
このような場合、onCloseRequested 内で即座に event.preventDefault() を呼び出して一旦終了をキャンセルし、ユーザーの確認が取れた後に destroy メソッドでウィンドウを破棄します。

注意: ユーザー確認後に close メソッドを使うと、再度 onCloseRequested が発火して無限ループになる恐れがあるため、destroy を使用して強制的に閉じます。 destroy メソッドを使用するには、権限設定に core:window:allow-destroy が必要です。
import { getCurrentWindow } from '@tauri-apps/api/window';

const appWindow = getCurrentWindow();

await appWindow.onCloseRequested(async (event) => {
  // 非同期処理を行う前に、一旦イベントをキャンセルしておく
  event.preventDefault();

  // 独自の確認ダイアログを表示する関数(ここでは window.confirm を非同期的にラップして代用)
  const confirmed = await new Promise<boolean>((resolve) => {
    // setTimeout を使い、非同期処理をシミュレート
    setTimeout(() => {
      const result = window.confirm('【非同期】保存せずに終了しますか?');
      resolve(result);
    }, 0);
  });

  if (confirmed) {
    // ユーザーが許可した場合、再チェックなしで強制的に破棄する
    await appWindow.destroy();
  }
});

3. バックエンドで検知してフロントエンドに通知する (Rust)

Rust 側で CloseRequested イベントをフックし、フロントエンドに通知する方法です。
バックエンドの状態に応じて制御したい場合などに有効です。

Rust 側 (src-tauri/src/lib.rs)

Tauri v2 では lib.rsrun 関数に記述するのが一般的です。
api.prevent_close() でウィンドウが閉じるのを防ぎ、window.emit で通知を送ります。
emit を使用するには tauri::Emitter トレイトのインポートが必要です。

use tauri::{Emitter, WindowEvent};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .on_window_event(|window, event| {
            if let WindowEvent::CloseRequested { api, .. } = event {
                // ウィンドウのクローズを一旦キャンセル
                api.prevent_close();

                // フロントエンドへイベントを通知
                // ペイロードは必要に応じて変更してください
                let _ = window.emit("close-requested", ());
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンド側での受け取り (TypeScript)

フロントエンドではイベントを受け取り、確認後に destroy メソッドで終了します。

注意: destroy メソッドを使用するには、権限設定に core:window:allow-destroy が必要です。
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';

const appWindow = getCurrentWindow();

listen('close-requested', async () => {
  const confirmed = await window.confirm('終了しますか?');
  if (confirmed) {
    // 確認が取れたら強制的に閉じる
    await appWindow.destroy();
  }
});