WinFormsのOK・キャンセルボタン:AcceptButtonとDialogResult

WinFormsのダイアログのOK・キャンセルボタンを.NET 10で整理。AcceptButtonとCancelButton、DialogResultの伝わり方、FormClosingのe.Cancelで入力ミス時に閉じない方法。

2026年11月改訂: この記事は2021年に韓国語で書いた記事を、日本語向けに書き直したものです。例を .NET 10 の WinForms(net10.0-windows)でコンパイルし直し、入力チェック付きのダイアログの例を新しく足しました。WinForms は Windows 専用のため、Windows向けに.NET 10でコンパイル済み、このマシン(Apple Silicon の Mac)では実行していません。 実行結果は載せておらず、動作の説明は Microsoft Learn と WinForms のオープンソースのコード(dotnet/winforms の Form.cs)に基づいています。2021年版の例の Console.WriteLine が WinForms アプリでは何も表示しない点も直しました。

WinForms で「名前を入力」のような小さなダイアログを作るときに、Enter で確定、Esc でキャンセル、そして入力ミスなら閉じないようにしたい人向けの記事です。要点を先に書くと、OK ボタンに DialogResult = OK、フォームに AcceptButton と CancelButton を設定すればキーボード操作は完成です。入力チェックは FormClosing で e.Cancel = true にすれば止められ、そのとき WinForms が DialogResult を None に戻してくれます。

AcceptButton・CancelButton・Button.DialogResult

2021年版に書いた3つの手順は、今もそのまま正しいです。

  1. OK ボタンの DialogResult プロパティを OK に、キャンセルボタンを Cancel にする。
  2. どのボタンが押されたかは、ShowDialog の戻り値か、閉じた後のフォームの DialogResult プロパティで調べる。
  3. Enter キーはフォームの AcceptButton、Esc キーは CancelButton に結び付ける。

これに2つ付け加えます。

  • CancelButton はボタンの DialogResult を自動で Cancel にします。 WinForms のソース(Form.cs)の setter はこうなっています。
set
{
    Properties.AddOrRemoveValue(s_propCancelButton, value);

    if (value is not null && value.DialogResult == DialogResult.None)
    {
        value.DialogResult = DialogResult.Cancel;
    }
}

一方 AcceptButton の setter は既定ボタンの表示を更新するだけで、DialogResult には触れません。そのため OK ボタンの DialogResult = OK は自分で設定します。これを忘れると、Enter を押しても Click イベントが起きるだけでダイアログは閉じません。

  • ボタンの DialogResult でフォームが閉じるのは、ShowDialog で開いたときだけです。 Microsoft Learn の Button.DialogResult のページには、親フォームが ShowDialog で表示されている場合はクリックするとイベント処理なしでフォームが閉じる、と書かれています。Show で開いたモードレスフォームでは、同じボタンを押しても閉じません。両者の違いはモーダル・モードレスフォームで扱っています。

2021年版の例を .NET 10 でビルドすると

2021年版の C# の例をそのまま Form1 のメソッドに入れてビルドしました。エラーも警告もありません(警告0件)。ただしコードには問題が3つありました(… は省略)。

// フォームを作成
Form1 f = new Form1 ();
// フォームを表示
if (f.ShowDialog ( this ) == DialogResult.OK)
{
    Console.WriteLine ("OK ボタンが押されました。");
}
// DialogResult プロパティを確認
if (f.DialogResult == DialogResult.OK) { /* … */ }
else if (f.DialogResult == DialogResult.Cancel) { /* … */ }
f.Dispose();
  • Console.WriteLine は何も表示しません。 WinForms テンプレートの出力の種類は WinExe で、コンソールが付いていません。確認用なら Debug.WriteLine(Visual Studio の出力ウィンドウ)、ユーザー向けならラベルや MessageBox を使います。
  • Form1 の中で Form1 をもう1つ開いています。 説明のために名前を使い回しただけですが、実際にはダイアログ用のクラスを別に作ります(下の NameDialog)。
  • 同じ判定を2回しています。 戻り値と DialogResult プロパティは同じ値です。ShowDialog で開いたフォームは閉じても非表示になるだけで破棄されないので、後からプロパティを読むこともできますが、戻り値だけで十分です。Dispose は using に任せます。

DialogResult が伝わる順番

Enter を押してから ShowDialog が戻るまでは、次の順に進みます。

フローチャート。上の段:Enter を押すか OK をクリックすると、フォームの DialogResult が OK になり FormClosing が起きる。判断:入力は正しいか。はいなら ShowDialog は OK を返す。いいえならハンドラーが e.Cancel = true にし、WinForms が DialogResult を None に戻し、ダイアログは開いたまま再入力を待つ。下の段:Esc、キャンセルボタン、タイトルバーの閉じるボタンは DialogResult を Cancel にし、チェックなしで ShowDialog は Cancel を返す。
Enter キーから ShowDialog が戻るまでの流れ。FormClosing を取り消すと DialogResult は None に戻り、ダイアログは開いたまま、呼び出し側も待ち続ける。
  1. Enter → AcceptButton(OK ボタン)がクリックされる → ボタンの DialogResult(OK)がフォームの DialogResult にコピーされる。
  2. モーダルループが DialogResult が None でないことに気づいてクローズを始め、FormClosing を発生させる。
  3. どのハンドラーも e.Cancel にしなければ、フォームは非表示になり ShowDialog が OK を返す。
  4. e.Cancel = true にすると、WinForms が DialogResult を None に戻す。ダイアログは開いたままで、呼び出し側は待ち続ける。

4は dotnet/winforms の Form.CheckCloseDialog に、if (e.Cancel) の次の _dialogResult = DialogResult.None; として書かれている動作です。Esc、キャンセルボタン、タイトルバーの閉じるボタン(×)はどれも Cancel で終わります。Microsoft Learn の Form.DialogResult のページによると、モーダルダイアログの閉じるボタンを押すとフォームは非表示になり、DialogResult は Cancel になります。コードで this.DialogResult = DialogResult.OK; と代入した場合も、ボタンのクリックと同じ経路で閉じます。

入力ミスなら閉じない:FormClosing と e.Cancel

名前が空のまま OK を押すと、閉じずにエラーアイコンを出すダイアログです。net10.0-windows で警告0件でコンパイルできました。

public sealed class NameDialog : Form
{
    readonly TextBox _name = new() { Left = 12, Top = 12, Width = 260 };
    readonly Button _ok = new() { Text = "OK", Left = 116, Top = 48, DialogResult = DialogResult.OK };
    readonly Button _cancel = new() { Text = "Cancel", Left = 197, Top = 48, CausesValidation = false };
    readonly ErrorProvider _error = new();

    public string UserName => _name.Text.Trim();

    public NameDialog()
    {
        Text = "Your name";
        ClientSize = new Size(284, 84);
        FormBorderStyle = FormBorderStyle.FixedDialog;
        MinimizeBox = MaximizeBox = false;
        ShowInTaskbar = false;
        StartPosition = FormStartPosition.CenterParent;
        Controls.AddRange([_name, _ok, _cancel]);
        AcceptButton = _ok;      // Enter
        CancelButton = _cancel;  // Esc。_cancel.DialogResult も Cancel になる
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        if (DialogResult == DialogResult.OK && UserName.Length == 0)
        {
            e.Cancel = true;                       // WinForms が DialogResult を None に戻す
            _error.SetError(_name, "Enter a name.");
            _name.Focus();
        }
        base.OnFormClosing(e);
    }
}

呼び出し側はこうです。

void AskName()
{
    using var dlg = new NameDialog();
    if (dlg.ShowDialog(this) == DialogResult.OK)
        Text = $"Hello, {dlg.UserName}";
    else
        Debug.WriteLine($"cancelled: {dlg.DialogResult}");
}

設計上の判断をメモしておきます。

  • チェックの条件に DialogResult == DialogResult.OK を入れています。 これがないと、キャンセルや閉じるボタン(×)でもチェックに引っかかり、ユーザーがダイアログから抜けられなくなります。
  • base.OnFormClosing(e) は必ず呼びます。 呼ばないと、外から登録した FormClosing イベントハンドラーが動きません。
  • キャンセルボタンは CausesValidation = false。 テキストボックスに Validating のチェックを付けたとき、キャンセルボタンにフォーカスが移った瞬間にチェックが走ってユーザーを引き留めるのを防ぎます。
  • 別の方法として、OK ボタンの DialogResult を None のままにし、Click ハンドラーで検証が通ったときだけ this.DialogResult = DialogResult.OK; を代入する書き方もあります。チェックが1か所(Click)にまとまるのが利点です。上の方法は、Enter・ボタン・コードからの代入などどの経路で OK として閉じても同じチェックを通るのが利点です。

FormClosing は Windows のシャットダウン(CloseReason.WindowsShutDown)や Application.Exit でも発生します。それまで止めるとサインアウトの邪魔になるので、チェックは DialogResult == OK のときに限るのが安全です。同じ落とし穴はフォームを1つだけ表示する方法の「閉じずに隠す」の節でも出てきます。

まとめ

やりたいこと 設定
Enter で確定 AcceptButton = okButton + okButton.DialogResult = OK
Esc でキャンセル CancelButton = cancelButton(DialogResult は自動で Cancel)
結果を受け取る using var dlg …; if (dlg.ShowDialog(this) == DialogResult.OK)
入力ミスなら閉じない OnFormClosing で DialogResult == OK && 不正 → e.Cancel = true
キャンセル時はチェックしない cancelButton.CausesValidation = false
デバッグ出力 Console.WriteLine ではなく Debug.WriteLine

2021年版の VB.NET の例は同じ API を呼ぶだけなので、今回は C# のみにしました。元の記事には画像がなかったので、削除した画像はありません。

確認環境:.NET SDK 10.0.403、Microsoft.WindowsDesktop.App.Ref 10.0.14、C# 14、macOS(Apple Silicon の Mac)。WinForms のコードは dotnet build -p:EnableWindowsTargeting=true でコンパイルのみ行いました。

よくある質問

CancelButton を設定したら、ボタンの DialogResult も Cancel にする必要がある?

不要です。dotnet/winforms の Form.cs では、CancelButton プロパティの setter が、指定したボタンの DialogResult が None なら Cancel に変えます。一方 AcceptButton は Enter キーをそのボタンに結び付けるだけで DialogResult は変えないので、OK ボタンには DialogResult = OK を自分で設定します。

入力が正しくないとき、OK を押してもダイアログを閉じないようにするには?

FormClosing(または OnFormClosing のオーバーライド)で、DialogResult が OK かつ入力が不正なら e.Cancel = true にします。クローズが取り消されると WinForms が DialogResult を None に戻すので、ダイアログは開いたまま、ShowDialog も戻りません。OK ボタンの DialogResult を None のままにして、Click ハンドラーで検証が通ったときだけ this.DialogResult = DialogResult.OK とする方法もあります。

2021年版の例の Console.WriteLine が何も表示されない

WinForms アプリは出力の種類が WinExe でコンソールウィンドウがないため、Console.WriteLine の出力はどこにも表示されません。デバッグ用なら System.Diagnostics.Debug.WriteLine(Visual Studio の出力ウィンドウ)、ユーザーに見せるならラベルや MessageBox を使います。

  • #C#
  • #WinForms
  • #DialogResult
  • #AcceptButton
  • #FormClosing

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑