WinFormsのモーダルとモードレス:ShowDialogとShow(.NET 10)

WinFormsで子フォームをShowDialog(モーダル)とShow(モードレス)で開く違いを.NET 10で整理。DialogResult、オーナー、Disposeの決まり、高DPIの既定値、ShowDialogAsyncまで。

2026年11月改訂: この記事は2021年に韓国語で書いた記事を、日本語向けに書き直したものです。C# の例を .NET 10 の WinForms(net10.0-windows)でコンパイルし直しました。WinForms は Windows 専用のため、Windows向けに.NET 10でコンパイル済み、このマシン(Apple Silicon の Mac)では実行していません。 そのため実行結果は載せていません。実際に確かめたのは、コンパイル結果(警告・エラーのコード)、テンプレートが生成するコード、そして .NET 9 と .NET 10 でのコンパイル結果の違いです。実行時の動作は、本文で名前を挙げた Microsoft Learn のページに基づいて説明します。2021年版にあった Visual Studio 2019 の画面2枚は外しました(理由は最後に)。

WinForms でメインフォームから別のフォーム(設定画面、入力ダイアログ、ツールウィンドウ)を開くとき、ShowDialog と Show のどちらを使うべきか迷っている人向けの記事です。結論はシンプルで、次の行に進む前にユーザーの答えが要るなら ShowDialog(this) を使って using で破棄します。メインフォームと行き来しながら使うなら Show(this) を使い、結果はイベントで受け取ります。

モーダルとモードレス:2021年版の例を .NET 10 で

2021年版の C# の例はそのままコンパイルできました(警告0件)。以下はコメントだけ短くしたものです。

// Form2 のインスタンス
Form2 f = new Form2();
// モーダルで開く。オーナーは this
f.ShowDialog(this);
// 閉じたら破棄
f.Dispose();
// Form2 のインスタンス
Form2 f = new Form2();
// モードレスで開く
f.Show();

2つの違いは3点にまとめられます。

判断の図。問い:次の行が動く前に結果が必要か。はいなら ShowDialog(this)。呼び出し側は待ち、ShowDialog は DialogResult を返し、フォームは隠れるだけなので using var などで自分で破棄する。いいえなら Show(this)。呼び出しの後のコードはすぐ動き、閉じるとフォームは破棄され、結果は FormClosed や独自のイベントで受け取る。
ShowDialog と Show の違いは3つ。呼び出し側が待つかどうか、DialogResult が戻るかどうか、そしてフォームを誰が破棄するか。
  • 呼び出し側が待つか。 ShowDialog はフォームが閉じるまで戻りません。その間、同じスレッドのほかのウィンドウ(メインフォームなど)は入力を受け付けません。Show はウィンドウを出してすぐ戻るので、次の行がすぐに動きます。
  • 結果が返るか。 ShowDialog は DialogResult を返します。Show は void です。
  • 誰が破棄するか。 Microsoft Learn の Form.ShowDialog のページには、モーダルフォームは閉じるボタンや DialogResult の設定で閉じても Close は呼ばれず、非表示になるだけだと書かれています。そのため自分で Dispose が必要です。Form.Close のページにも「ShowDialog で表示したフォームは Close で破棄されない」という例外が載っています。2021年版の「モーダルは Dispose、モードレスは不要」という説明は正しいです。

補足が要るのは「モーダルフォームを開くと、そのフォーム以外にはアクセスできない」という一文です。正確には、ブロックされるのは同じ UI スレッドのウィンドウで、ほかのアプリや別の UI スレッドで作ったウィンドウは普通に使えます。

2026年の書き方:using、オーナー、二重起動の防止

今なら同じことをこう書きます。net10.0-windows で警告0件でコンパイルできたコードです。

void OpenModal()
{
    using var dlg = new Form2();
    DialogResult result = dlg.ShowDialog(this);   // ここで待つ。オーナーは無効化
    Text = $"closed with {result}";
}                                                 // ここで Dispose

Form2? _tool;
void OpenModeless()
{
    if (_tool is { IsDisposed: false }) { _tool.Activate(); return; }
    _tool = new Form2();
    _tool.FormClosed += (_, _) => _tool = null;
    _tool.Show(this);                              // オーナー付き:常に手前、一緒に最小化
}
  • using var なら例外が起きても破棄されます。2021年版は ShowDialog の中で例外が起きると Dispose を飛ばしてしまいます。
  • 2021年版のモードレスの例は f.Show() でオーナーなしに開いていました。これだとメインフォームをクリックした途端に子フォームが後ろに回り、ボタンを押すたびにウィンドウが1枚ずつ増えます。Show(this) でオーナーを指定し、既に開いていれば Activate() するだけ、というのが一般的なツールウィンドウの動きです。「フォームを1つだけ表示する」話はフォームを1つだけ表示する方法で詳しく扱います。
  • モードレスフォームの結果は ShowDialog のようには受け取れないので、FormClosed か独自のイベントで渡します。イベントの宣言はイベントの記事を参照してください。

DialogResult を OK・キャンセルボタンと結び付ける方法や、入力が正しくないときに閉じないようにする方法はOK・キャンセルボタンの記事に続きます。

.NET 10 のテンプレートと高DPIの既定値

2021年版は Visual Studio 2019 の「Windows フォーム アプリケーション (.NET Framework)」テンプレートから始めていました。.NET 10 では dotnet new winforms で作れます。生成された Program.cs は次のとおりです。

[STAThread]
static void Main()
{
    // To customize application configuration such as set high DPI settings or default font,
    // see https://aka.ms/applicationconfiguration.
    ApplicationConfiguration.Initialize();
    Application.Run(new Form1());
}

ApplicationConfiguration.Initialize() はソースジェネレーターがビルド時に作るメソッドです。EmitCompilerGeneratedFiles=true を付けてビルドし、生成ファイルを取り出すと中身はこうでした。

public static void Initialize()
{
    global::System.Windows.Forms.Application.EnableVisualStyles();
    global::System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
    global::System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.SystemAware);
}

既定値は SystemAware です。起動時のメインモニターの拡大率で描画し、拡大率の違うモニターにウィンドウを移すと Windows がビットマップとして引き伸ばすため、ぼやけることがあります。ノート PC の画面(150%)と外付けモニター(100%)を並べて使う環境ではよく目にする症状です。プロジェクトファイルに1行足して再ビルドすると、生成コードが変わります。

<PropertyGroup>
  <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
</PropertyGroup>
global::System.Windows.Forms.Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);

Microsoft Learn の「High DPI support in Windows Forms」は、モニターごとに拡大率が違う環境では PerMonitorV2 を勧めています。ダイアログがオーナーと別のモニターに出ることは珍しくないので、モーダル・モードレスを使うアプリなら一度確認しておく価値があります。また Application.SetHighDpiMode のページによると、この設定は最初のウィンドウを作る前に呼ぶ必要があります。Initialize() より先にフォームを new するコード(static フィールドの初期化子など)があってはいけません。

async と ShowDialog:2021年ごろの落とし穴

2021年ごろ、「重い処理を非同期で走らせて、終わったらダイアログを出す」でよく踏んだ落とし穴が3つあります。どれも今でもそのまま当てはまります。

  1. Task.Run の中で ShowDialog を呼ぶ。 フォームがスレッドプールのスレッド(MTA)で作られます。WinForms は Main に [STAThread] が付いた STA の UI スレッドが前提で、コントロールは作ったスレッドからしか触れません。UI スレッドのフォームをオーナーに渡すとクロススレッド呼び出しになり、クリップボードやドラッグ&ドロップのような OLE の機能は「STA ではない」という例外で失敗します。
  2. UI スレッドで .Result や .Wait() で待つ。 UI スレッドが止まるので、UI スレッドで再開したい await の続きが動けず、デッドロックになります。
  3. 正解は、イベントハンドラーを async にして await の後で ShowDialog を呼ぶこと。 WinForms の同期コンテキストのおかげで await の後は UI スレッドに戻っているので、そのままフォームを出せます。
async void OpenAfterLoadAsync()
{
    string data = await Task.Run(() => "loaded");  // await の後は UI スレッド
    using var dlg = new Form2 { Text = data };
    dlg.ShowDialog(this);
}

.NET 9 で Form.ShowDialogAsync が追加されました。参照パックの XML ドキュメントには「フォームが閉じるか破棄されると完了する Task<DialogResult> を返す」と書かれています。同じコードを2つのバージョンでビルドすると、結果が違いました。

async Task OpenModalAsync()
{
    using var dlg = new Form2();
    DialogResult r = await dlg.ShowDialogAsync(this);
    Text = r.ToString();
}
# net9.0-windows (Microsoft.WindowsDesktop.App.Ref 9.0.22)
Program.cs(69,32): error WFO5002: 'System.Windows.Forms.Form.ShowDialogAsync(System.Windows.Forms.IWin32Window)' は、評価の目的でのみ提供されています。将来の更新で変更または削除されることがあります。続行するには、この診断を非表示にします。

# net10.0-windows (Microsoft.WindowsDesktop.App.Ref 10.0.14)
Build succeeded. 0 Warning(s) 0 Error(s)

.NET 9 では実験的 API のため、警告ではなくエラーでビルドが止まりました。.NET 10 では診断なしでコンパイルできました。実行はしていないので、動作についてはドキュメントの説明以上のことは言えません。

まとめ

場面 使うもの
答えを受け取ってから次の処理 using var dlg = …; dlg.ShowDialog(this)
メインフォームと並べて使うツールウィンドウ Show(this)、開いていれば Activate()
モードレスフォームの結果 FormClosed か独自のイベント
非同期処理の後にダイアログ async ハンドラーで await の後に ShowDialog(.NET 10 なら ShowDialogAsync も可)
拡大率の違う複数モニター <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>

2021年版にあった Visual Studio 2019 の「新しいプロジェクトの作成」画面2枚は削除しました。赤枠の強調の付け方やファイル名が Microsoft のチュートリアルの画像と同じで、自分で撮ったものか確認できなかったこと(著作権のリスク)、そして .NET Framework のテンプレートを選ぶ画面で内容がもう合わないことが理由です。開発環境の準備はVisual Studio のインストールの記事で扱っています。2021年版の VB.NET の例は C# と同じ API を呼ぶだけなので、今回は C# のみにしました。

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

よくある質問

ShowDialog で開いたフォームは Dispose が必要?

必要です。Microsoft Learn の Form.ShowDialog のページによると、モーダルで開いたフォームは閉じるボタンや DialogResult の設定で閉じても Close は呼ばれず、非表示になるだけです。再表示できる代わりに自分で Dispose する必要があるので、using var dlg = new Form2(); と書くのが簡単です。Show で開いたモードレスフォームは閉じたときに自動で破棄されます。

Show(this) と Show() の違いは?

Show(this) は呼び出し元のフォームをオーナー(Owner)にします。オーナーを持つフォームは常にオーナーより手前に表示され、オーナーを最小化したり閉じたりすると一緒に最小化・クローズされます。Show() だけだと、メインフォームをクリックした瞬間に子フォームが後ろに隠れることがあります。

.NET 10 で ShowDialogAsync はそのまま使える?

net10.0-windows(参照パック 10.0.14)では Form.ShowDialogAsync は診断なしでコンパイルできました。同じコードを net9.0-windows でビルドすると error WFO5002(評価目的でのみ提供)で失敗します。.NET 9 を対象にするなら診断の抑制が必要で、.NET 10 以降なら通常の API として使えます。

  • #C#
  • #WinForms
  • #ShowDialog
  • #モーダル
  • #モードレス

t.laboratory

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

運営者情報 · アプリ

↑