処理の完了やエラー、入力の不備を OS 標準のダイアログで伝えるには、dialog プラグインの message() を使います。kind を warning や error にすると警告・エラーのダイアログになり、閉じられるまで await で待てます。「はい・いいえ」を選ばせるなら同じプラグインの ask() / confirm() を使います(確認ダイアログのレシピ)。Rust からも出せるので、画面の準備前に起きた起動時のエラーも伝えられます。
前提条件
dialog プラグインを追加します。npm パッケージ、Rust のクレート、lib.rs の .plugin(tauri_plugin_dialog::init())、権限 dialog:default がまとめて入ります。
npm run tauri add dialog
dialog の権限は core:default に含まれません。dialog:default(メッセージとファイルを開く・保存するダイアログ)か、メッセージだけなら dialog:allow-message を追加します。ask() / confirm() もこの権限で動き、古い記事にある 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"
]
}
目的ごとの使い分けは次のとおりです。
| やりたいこと | 使うもの |
|---|---|
| 知らせるだけ(完了・警告・エラー) | message()(このレシピ) |
| 「はい / いいえ」「OK / キャンセル」を選ばせる | ask() / confirm()(dlg-002) |
| ファイルやフォルダを選ばせる | open() / save()(dlg-004) |
| ウィンドウが裏にあっても気付かせる | OS の通知(dlg-009) |
| 見た目や入力欄を自由に作る | HTML のダイアログ(dlg-011) |
1. フロントエンドから実装する (TypeScript)
情報・警告・エラーを出し分ける
第 2 引数の kind で種類を指定します(省略時は info)。OS によっては種類ごとのアイコンが付きます。title を省略するとアプリ名(productName)になり、第 2 引数に文字列だけを渡すとタイトルとして扱われます。ボタン名は buttons: { ok: '閉じる' } で変えます(以前の okLabel は 2.4.0 から非推奨)。
import { message } from '@tauri-apps/plugin-dialog';
// 情報: kind を省略すると info
await message('エクスポートが完了しました。', '完了');
// 警告: 続けられるが、気付いてほしいことがあるとき
await message('一部の画像は読み込めなかったため、省略しました。', {
title: '読み込みの警告',
kind: 'warning',
});
// エラー: 処理を続けられなかったとき。ボタン名も変えられる
await message('ファイルを保存できませんでした。\nディスクの空き容量を確認してください。', {
title: '保存エラー',
kind: 'error',
buttons: { ok: '閉じる' },
});
await は閉じられるまで待ち、押されたボタン(既定は 'Ok'、名前を指定したときはその文字列)を返します。
入力の不備を警告する
入力ミスは kind: 'warning' で知らせ、閉じた後に直す場所へフォーカスを戻します。入力のたびに出すと煩わしいので、送信時など 1 回の操作につき 1 回にします。
import { message } from '@tauri-apps/plugin-dialog';
export async function validateEmail(input: HTMLInputElement): Promise<boolean> {
if (input.value.includes('@')) return true;
await message('メールアドレスの形式が正しくありません。', { title: '入力の確認', kind: 'warning' });
input.focus(); // 閉じた後に、直す場所へ戻す
return false;
}
処理の失敗をエラーダイアログで知らせる
Rust のコマンドが Err を返すと invoke() は例外になります。受ける値は unknown 型なので文字列に直して表示し、同時に失敗しても重ならないよう 1 つずつ順に出します。
import { invoke } from '@tauri-apps/api/core';
import { message } from '@tauri-apps/plugin-dialog';
// invoke の失敗は Rust の Err の値で届く(Result<T, String> なら文字列)
function describe(e: unknown): string {
if (typeof e === 'string') return e;
if (e instanceof Error) return e.message;
return JSON.stringify(e);
}
// 同時に失敗してもダイアログが重ならないよう、前のものが閉じてから次を出す
let chain: Promise<unknown> = Promise.resolve();
export function showError(title: string, e: unknown): Promise<unknown> {
chain = chain.then(() => message(describe(e), { title, kind: 'error' })).catch(console.error);
return chain;
}
export async function saveReport(text: string) {
try {
const path = await invoke<string>('save_report', { text }); // 2 章のコマンド
await message(`${path} に保存しました。`, '保存しました');
} catch (e) {
await showError('保存できませんでした', e);
}
}
alert() は待たない
dialog プラグインを入れると、window.alert() はこのネイティブのダイアログに置き換わります。ただし閉じられるのを待たずに戻るので、次の行は表示と同時に動きます。権限が無くても例外にならず、表示されないだけです。閉じた後の処理があるなら await message() を使います。window.confirm() の注意点は dlg-002 で扱います。
2. バックエンドから実装する (Rust)
DialogExt を use すると、AppHandle やウィンドウから .dialog().message() でビルダーを作れます。表示の方法は 2 つです。
show(コールバック): すぐに戻り、閉じたときにコールバックが呼ばれます。どのスレッドから呼んでも構いません。blocking_show(): 閉じられるまで待ちます。メインスレッドで呼ぶとアプリが固まるので、メインスレッドで動くsetup・イベントハンドラー・asyncの付かないコマンドではshow()を使います。
JS から出したダイアログは呼び出し元のウィンドウに結び付きますが、Rust からは .parent(&window) を付けない限り親ウィンドウの無いダイアログになります。操作をどこまで止めるかは 操作をブロックするモーダルダイアログにする で扱います。
use tauri::Manager;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
/// レポートを保存する。失敗の理由は文字列で返し、表示は JS 側に任せる
#[tauri::command]
fn save_report(app: tauri::AppHandle, text: String) -> Result<String, String> {
if text.trim().is_empty() {
return Err("保存する内容がありません。".into());
}
let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
std::fs::create_dir_all(&dir).map_err(|e| format!("フォルダを作れません: {e}"))?;
let path = dir.join("report.txt");
std::fs::write(&path, text).map_err(|e| format!("{} に書き込めません: {e}", path.display()))?;
Ok(path.display().to_string())
}
/// 別スレッドで処理し、結果を Rust からダイアログで知らせる
#[tauri::command]
fn start_backup(app: tauri::AppHandle) {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(2)); // 実際の処理の代わり
let result: Result<usize, String> = Ok(42);
let (text, kind) = match result {
Ok(n) => (format!("{n} 件のファイルをバックアップしました。"), MessageDialogKind::Info),
Err(e) => (format!("バックアップに失敗しました。\n{e}"), MessageDialogKind::Error),
};
let builder = app
.dialog()
.message(text)
.kind(kind)
.title("バックアップ")
.buttons(MessageDialogButtons::OkCustom("閉じる".into()));
// メインウィンドウに結び付ける(parent はデスクトップ専用)
#[cfg(desktop)]
let builder = match app.get_webview_window("main") {
Some(w) => builder.parent(&w),
None => builder,
};
builder.blocking_show(); // メインスレッドの外なので、閉じられるまで待ってよい
});
}
起動時の検査で問題が見つかったときは、setup で show() を使ってエラーを出し、閉じたら終了します。
use tauri::Manager;
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
/// 設定ファイルが壊れていないかを確かめる(ファイルが無ければ問題なし)
fn check_settings(app: &tauri::AppHandle) -> Result<(), String> {
let path = app.path().app_config_dir().map_err(|e| e.to_string())?.join("settings.json");
if !path.exists() {
return Ok(());
}
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
serde_json::from_str::<serde_json::Value>(&text)
.map(|_| ())
.map_err(|e| format!("{}\n{e}", path.display()))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
if let Err(e) = check_settings(app.handle()) {
let handle = app.handle().clone();
// setup はメインスレッドで動くので、待たない show() を使う
app.dialog()
.message(format!("設定ファイルを読み込めないため終了します。\n{e}"))
.kind(MessageDialogKind::Error)
.title("起動エラー")
.show(move |_| handle.exit(1)); // 閉じたら終了する
}
Ok(())
})
.invoke_handler(tauri::generate_handler![save_report, start_backup])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
await invoke('start_backup'); // すぐ戻り、約 2 秒後に Rust からダイアログが出る
動作確認
npm run tauri dev で起動し、1 章のコードをボタンなどから呼ぶと、種類ごとのダイアログが順に出ます。saveReport('') は「保存する内容がありません。」のエラーになり、saveReport('test') なら保存先のパスが出ます。start_backup は約 2 秒後に「42 件のファイルをバックアップしました。」を出します。
起動時のエラーは、appConfigDir() のフォルダ(Windows なら %APPDATA%\<identifier>)に中身の壊れた settings.json を置いて起動すると確かめられます。ダイアログを閉じるとアプリが終了します。
よくあるエラーと対処法
- 「dialog.message not allowed. Permissions associated with this command: dialog:allow-ask, dialog:allow-confirm, dialog:allow-message, dialog:default」: 権限の追加漏れです。リリースビルドでは「Command plugin:dialog|message not allowed by ACL」だけになります。追加して
tauri devを再起動します。 - 「plugin dialog not found」:
lib.rsに.plugin(tauri_plugin_dialog::init())がありません。この状態で Rust からapp.dialog()を呼ぶと「state() called before manage()」で始まるパニックになります。 - 本文が「[object Object]」になる: Rust の
Errが構造体だと JS にはオブジェクトで届きます。上のdescribe()のように文字列へ直します。 - Rust から出すとアプリが固まる: メインスレッドで
blocking_show()を呼んでいます。show()に替えるか、asyncコマンドや別スレッドから呼びます。
OS ごとの違いと注意点
- 見た目: OS 標準のダイアログなので見た目は OS ごとに異なり、指定できるのは本文・タイトル・種類・ボタン名だけです。見た目をそろえたいなら HTML/CSS で自作のダイアログ にします。
- iOS / Android: ボタン名を指定しないと英語の「Ok」が表示されます。日本語にするなら
buttonsで指定します。 - Rust の
.parent(): デスクトップ専用です。モバイル向けにもビルドするなら、上のコードのように#[cfg(desktop)]で囲みます。 - 作業の中断: ダイアログは閉じるまでユーザーの手を止めます。急がない知らせは OS のネイティブ通知 の方が向いています。
