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はフォームが閉じるまで戻りません。その間、同じスレッドのほかのウィンドウ(メインフォームなど)は入力を受け付けません。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つあります。どれも今でもそのまま当てはまります。
Task.Runの中でShowDialogを呼ぶ。 フォームがスレッドプールのスレッド(MTA)で作られます。WinForms はMainに[STAThread]が付いた STA の UI スレッドが前提で、コントロールは作ったスレッドからしか触れません。UI スレッドのフォームをオーナーに渡すとクロススレッド呼び出しになり、クリップボードやドラッグ&ドロップのような OLE の機能は「STA ではない」という例外で失敗します。- UI スレッドで
.Resultや.Wait()で待つ。 UI スレッドが止まるので、UI スレッドで再開したいawaitの続きが動けず、デッドロックになります。 - 正解は、イベントハンドラーを
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 として使えます。