「はい・いいえ」の確認ダイアログを出す

dialog プラグインの ask()(はい/いいえ)と confirm()(OK/キャンセル)で確認し、結果を boolean で受け取る。3 択の作り方、Rust での書き方、window.confirm() の落とし穴も示す。

ダイアログ・通知 対象: Tauri 2.x 更新日: 読了目安: 約9分 dlg-002
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. ask() と confirm() で確認する
  4. 3 択にする
  5. window.confirm() を使わない
  6. 2. バックエンドから実装する (Rust)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

削除や上書きのように取り消せない操作の前には、「本当に実行しますか?」と確認を取ります。dialog プラグインの ask()(はい / いいえ)か confirm()(OK / キャンセル)を await すると、押されたボタンが true / false で返ります。3 択は message() にボタンを指定して作ります。知らせるだけのダイアログは メッセージダイアログ・警告ダイアログを表示する で扱います。

前提条件

dialog プラグインを追加します。

npm run tauri add dialog

ask()・confirm()・message() はどれも権限 dialog:allow-message で動きます。dialog:default には含まれますが、core:default には含まれません。dialog:allow-ask / dialog:allow-confirm はその別名として残る古い名前で、v3 で削除される予定です。

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

3 つの関数の違いは、ボタンと戻り値です。

関数ボタン戻り値
ask()はい / いいえ「はい」なら true
confirm()OK / キャンセル「OK」なら true
message() + buttons指定した 1〜3 個押したボタンの名前

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

ask() と confirm() で確認する

true になるのは「はい」「OK」が押されたときだけで、それ以外の閉じ方はすべて false です。「false なら中止」と書けば、想定外の閉じ方でも安全側に倒れます。

import { ask, confirm } from '@tauri-apps/plugin-dialog';

export async function deleteNote(name: string) {
  const yes = await ask(`「${name}」を削除します。元に戻せません。よろしいですか?`, {
    title: '削除の確認',
    kind: 'warning',
  });
  if (!yes) return; // 「いいえ」やそれ以外の閉じ方はすべて中止
  console.log('削除します:', name);
}

export async function overwriteFile(path: string): Promise<boolean> {
  // ボタン名を操作の内容にすると、よく読まずに押されても誤操作になりにくい
  return confirm(`${path} は既に存在します。上書きしますか?`, {
    title: '上書きの確認',
    kind: 'warning',
    okLabel: '上書きする',
    cancelLabel: 'やめる',
  });
}

ボタン名を変えるときは okLabel と cancelLabel を必ず両方渡します。片方だけだと、もう一方は英語の No(ask())か Cancel(confirm())になります。ボタン名を指定すると ask() と confirm() の違いは無くなります。

3 択にする

message() の buttons に yes / no / cancel のラベルを渡すと、押されたボタンのラベルが返ります。比較には同じ定数を使い、どれにも当たらない値はキャンセル扱いにします。

import { message } from '@tauri-apps/plugin-dialog';

const SAVE = '保存する';
const DISCARD = '保存しない';

export type SwitchChoice = 'save' | 'discard' | 'cancel';

// 別のファイルを開く前に、編集中の内容をどうするか聞く
export async function askBeforeSwitch(fileName: string): Promise<SwitchChoice> {
  const pressed = await message(`「${fileName}」への変更を保存しますか?`, {
    title: '未保存の変更',
    kind: 'warning',
    buttons: { yes: SAVE, no: DISCARD, cancel: 'キャンセル' },
  });
  if (pressed === SAVE) return 'save';
  if (pressed === DISCARD) return 'discard';
  return 'cancel'; // キャンセルや想定外の値は、何もしない扱い
}

buttons: 'YesNoCancel' と文字列で指定すると OS 標準の名前のボタンになり、'Yes' / 'No' / 'Cancel' が返ります。× で閉じる前の確認は ウィンドウを閉じる前に確認ダイアログを出す で扱います。

window.confirm() を使わない

dialog プラグインを入れると、window.confirm() は結果を Promise で返す関数に置き換わります。Promise はオブジェクトなので、ブラウザー向けによく書く if (confirm('削除しますか?')) は押したボタンに関係なく真になり、確認をすり抜けます。型定義は boolean のままなので TypeScript も警告しません。プラグインの ask() / confirm() を import して await します。ライブラリが window.confirm() を呼んでいる場合も同じです。

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

Rust では .buttons(MessageDialogButtons::YesNo) などでボタンを決め、次のどれかで結果を受け取ります。

  • blocking_show(): 閉じられるまで待ち、「はい」「OK」なら true を返します。
  • blocking_show_with_result(): 押されたボタンを返します。ラベルを指定したボタンは Custom(ラベル) です。
  • show(): 待たずに戻り、結果をコールバックで受け取ります。

async の付かないコマンド、setup、イベントハンドラーはメインスレッドで動くので、そこで blocking_show() を呼ぶとアプリが固まります。待つ版は async コマンドか別スレッドで使います。また Rust から出すダイアログは、.parent() を付けないと呼び出し元のウィンドウに結び付きません。

use tauri::Emitter;
use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder, MessageDialogButtons, MessageDialogKind, MessageDialogResult};

/// 呼び出し元のウィンドウに結び付けたビルダーを作る(parent はデスクトップ専用)
fn dialog_for<R: tauri::Runtime>(window: &tauri::WebviewWindow<R>, text: &str) -> MessageDialogBuilder<R> {
    let builder = window.dialog().message(text);
    #[cfg(desktop)]
    let builder = builder.parent(window);
    builder
}

/// 「はい / いいえ」で聞く。async コマンドはメインスレッドの外で動くので待ってよい
#[tauri::command]
async fn confirm_delete(window: tauri::WebviewWindow, name: String) -> bool {
    dialog_for(&window, &format!("「{name}」を削除します。元に戻せません。"))
        .title("削除の確認")
        .kind(MessageDialogKind::Warning)
        .buttons(MessageDialogButtons::YesNo)
        .blocking_show() // 「はい」なら true
}

/// 3 択の結果を、ラベルではなく決まった文字列で返す
#[tauri::command]
async fn ask_save(window: tauri::WebviewWindow) -> String {
    let result = dialog_for(&window, "変更を保存しますか?")
        .kind(MessageDialogKind::Warning)
        .buttons(MessageDialogButtons::YesNoCancelCustom(
            "保存する".into(),
            "保存しない".into(),
            "キャンセル".into(),
        ))
        .blocking_show_with_result();
    match result {
        MessageDialogResult::Custom(label) if label == "保存する" => "save".into(),
        MessageDialogResult::Custom(label) if label == "保存しない" => "discard".into(),
        _ => "cancel".into(),
    }
}

/// async でないコマンドはメインスレッドで動くので、show() のコールバックで受け取る
#[tauri::command]
fn confirm_reset(window: tauri::WebviewWindow) {
    let target = window.clone();
    dialog_for(&window, "設定を初期値に戻しますか?")
        .buttons(MessageDialogButtons::OkCancelCustom("初期値に戻す".into(), "やめる".into()))
        .show(move |ok| {
            if ok {
                let _ = target.emit("settings-reset", ()); // 結果はイベントで JS に知らせる
            }
        });
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![confirm_delete, ask_save, confirm_reset])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';

const ok = await invoke<boolean>('confirm_delete', { name: 'memo.txt' });
const choice = await invoke<'save' | 'discard' | 'cancel'>('ask_save');
console.log(ok, choice);

// confirm_reset はすぐ戻る。「初期値に戻す」が押されたときだけイベントが届く
await listen('settings-reset', () => console.log('初期値に戻します'));
await invoke('confirm_reset');

動作確認

npm run tauri dev で起動して deleteNote('memo.txt') を呼ぶと、警告の「はい / いいえ」が出ます。「はい」ではコンソールに次のように出て、「いいえ」では何も出ません。overwriteFile() は「上書きする」のときだけ true を返します。

削除します: memo.txt

Rust 版では、confirm_delete で「はい」、ask_save で「保存しない」を選ぶと true discard と出ます。confirm_reset は呼ぶとすぐ戻り、「初期値に戻す」を押したときだけ「初期値に戻します」が出ます。

よくあるエラーと対処法

  • 「dialog.message not allowed. Permissions associated with this command: dialog:allow-ask, dialog:allow-confirm, dialog:allow-message, dialog:default」: 権限がありません。ask() や confirm() から呼んでも dialog.message と出ます。リリースビルドでは「Command plugin:dialog|message not allowed by ACL」だけです。
  • 「いいえ」を押したのに処理が進む: window.confirm() か、await を付けずに if (ask(...)) と書いています。どちらも Promise が条件なので常に真です。
  • ボタンに英語の No や Cancel が出る: okLabel だけを指定しています。cancelLabel も渡します。
  • アプリが応答しなくなる: メインスレッドで blocking_show() を呼んでいます。コマンドを async にするか show() に替えます。
  • モバイル向けのビルドだけ parent が無いという趣旨のコンパイルエラーになる: .parent() はデスクトップ専用なので #[cfg(desktop)] で囲みます。

OS ごとの違いと注意点

  • ボタンの並び順: アプリからは指定できません。案内文では「右のボタン」のように位置で示さず、ボタン名で伝えます。
  • 既定のボタン名: デスクトップでは OS 標準の名前ですが、iOS / Android では英語の Yes / No、Ok / Cancel がそのまま出ます。日本語にするならラベルを指定します。
  • 待っている間も動くもの: 確認を待つ間も JS のタイマーや Rust 側のスレッドは動き続けます。自動保存などで状態が変わりうるなら、答えを受け取った後に対象がまだあるかを確かめてから実行します。
  • どこまで操作を止めるかは 操作をブロックするモーダルダイアログにする、入力欄などを自由に置いた確認画面は HTML/CSS で自作のダイアログを作る で扱います。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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