C# WinFormsのForm.ShowDialogとForm.Showの違いと使い分け完全ガイド
みなさんこんにちは!ヒロポンです!
WinFormsの業務アプリ書いてると、サブフォーム出すたびにShowDialogにするかShowにするかで毎回ちょっと迷うこと、ないっすか?
俺も最初の頃、感覚でやって痛い目を見た。マスタ参照ダイアログをShowにしてユーザーに親フォームをガチャガチャ触られてデータの整合性が壊れたり、進捗フォームをShowDialogにして本処理が止まったり。動かないわけじゃないから気づきにくい、でも仕様としては事故ってる、ってやつだ。
この記事ではVS2019・.NET Framework 4.7.2の業務系現場でずっと使ってきたShowDialogとShowの使い分け基準を、コード3パターンとハマりポイント込みで整理する。
💡 Form間の値受け渡しやメインフォーム切り替えは別記事【C#】FormとFormの間で値の共有・受け渡しをすると【C#】Formから別フォームを表示しメインフォームを切り替えるで書いてる。今回はモーダル/モードレスの選択判断に絞った比較編だ。
ShowDialogはモーダル—呼び出し側が止まる
Form.ShowDialog()はいわゆるモーダル表示だ。サブフォームを出した瞬間から、サブフォームを閉じるまで呼び出し側のコードは1行も進まない。親フォーム自体も無効化されて、ユーザーは触れない。
//親フォーム側
private void btnSelectMaster_Click(object sender, EventArgs e)
{
using (var dlg = new MasterSelectForm())
{
//この行で止まる。dlgが閉じるまで次の行は実行されない
var result = dlg.ShowDialog(this);
if (result == DialogResult.OK)
{
// dlg.SelectedIdみたいなプロパティで結果を引き取る
txtMasterId.Text = dlg.SelectedId;
}
}
// usingブロックを抜けたタイミングでDisposeされる(後述のハマりポイント①参照)
}
挙動を整理すると3点:
- 戻り値
DialogResultでOK/Cancelを判定できる —モーダル前提のAPIなので分岐が綺麗に書ける - 呼び出し側がブロックされる —サブフォームを閉じるまで親側のコードは進まない
- 親フォームが無効化される —タイトルバー以外触れなくなるので、ユーザーが裏で操作する事故が起きない
ただし欠点もある。ShowDialogで開いたフォームはusingで囲むか自分でDispose()を呼ばないと残り続ける。Showと違って閉じても自動で破棄されないので、ここはお作法としてusingをクセにしておく方が楽だ。
Showはモードレス—呼び出し側は動き続ける
一方Form.Show()はモードレス表示。サブフォームを出した次の行から、呼び出し側のコードはすぐ実行される。親フォームも普通に触れる。
//親フォーム側
private ProgressForm _progressForm;
private void btnStartImport_Click(object sender, EventArgs e)
{
_progressForm = new ProgressForm();
_progressForm.Owner = this; //所有関係を設定(最前面維持と一括破棄のため)
_progressForm.Show(); //ノンブロッキングで表示
// ↑ Show()の次の行はすぐ走る。バックグラウンドで取り込み開始
StartImportAsync();
}
private void StartImportAsync()
{
// BackgroundWorkerやTask.Runで重い処理を回しつつ
// _progressForm.UpdateProgress(percent)で進捗を更新する
}
Showの特徴を整理すると:
- 戻り値はない(
void) —結果を受け取りたい時はサブフォーム側のイベントorプロパティ経由 - 呼び出し側は止まらない —バックグラウンド処理と並走させる用途に向く
- 親フォームも操作可能 —ユーザーが他のことを並行してできる
- 閉じたら自動で
Disposeされる —ShowDialogと違ってここは楽
ただし、呼び出し側が止まらないということは値の受け渡しが面倒になるってことだ。「サブフォームで選んだ値を親に返したい」みたいな同期的な要件があるなら、素直にShowDialogを使った方が早い。

機能比較表でまとめる
ここまでの差分を表で整理しておく。判断に迷った時はこの表を見るのが早い。
| 観点 | ShowDialog(モーダル) | Show(モードレス) |
|---|---|---|
| 呼び出し側のブロック | する(閉じるまで止まる) | しない(次の行に進む) |
| 戻り値 | DialogResult(OK/Cancel等) |
なし(void) |
| 親フォームの操作 | 不可(無効化される) | 可能 |
自動Dispose |
されない(明示が要る) | される |
| 値の受け取り方 | 戻り値後にサブフォームのプロパティ参照 | イベントor共有オブジェクト経由 |
| 多重起動の制御 | デフォルトで1つだけ(同フォーム) | 自分で制御しないと複数開く |
| 主な用途 | マスタ参照・確認ダイアログ・設定画面 | 進捗表示・ツールウィンドウ・ログ画面 |
業務系のWinFormsだと、8割はShowDialogで済むというのが俺の体感だ。ユーザーに何かを「選ばせる」「入力させる」「確認させる」操作のほとんどはモーダルで自然だからだ。Showを使うのは「進捗・ログ・常時表示のサブパネル」みたいな決まった用途に寄る。
業務SE目線の使い分け判断基準(コード3スニペット)
実装中に「どっちで開くか」で迷ったら、この3つの判断軸で振り分けると早い。
1.マスタ参照ダイアログ→ ShowDialog
「商品マスタから1件選ぶ」「取引先マスタから検索して選ぶ」みたいな選んで値を返してもらうタイプはShowDialog一択だ。
//取引先選択ダイアログを開いて、選ばれたコードを画面に反映する
using (var dlg = new TorihikiSelectForm())
{
if (dlg.ShowDialog(this)== DialogResult.OK)
{
txtTorihikiCode.Text = dlg.SelectedCode;
txtTorihikiName.Text = dlg.SelectedName;
}
}
理由は単純で、呼び出し側が結果を待ちたいから。Showだと「選ばれたかどうか」を判定するためにイベントハンドラ書いたり、コールバックで受けたりと余計な配線が増える。素直にShowDialogでDialogResultを受ければ1メソッドで完結する。
ただし欠点として、マスタ参照中はユーザーが他の操作を一切できない。明細グリッドを横目で見ながら絞り込みたい、みたいな要件があるならモードレス(Show)の検討も入る。
2.進捗・常時表示フォーム→ Show
「CSV取り込みの進捗バー」「ログを流しっぱなしのウィンドウ」「ツールバー的なサブパネル」はShowが向く。
//進捗フォームをShowして、本処理は呼び出し側で並行進行
var progress = new ProgressForm { Owner = this };
progress.Show();
try
{
foreach (var row in importRows)
{
ImportOneRow(row);
progress.UpdateBar(++current, importRows.Count);
Application.DoEvents(); // UIスレッドの再描画を促す(多用は注意)
}
}
finally
{
progress.Close(); // Closeした瞬間にDisposeもされる
}
進捗フォームをShowDialogで出すと、サブフォーム側でBackgroundWorkerを回さないと本処理が動かない。それ自体は可能だけど、業務系の単純なバッチ処理だとオーバーキル感がある。Show + Application.DoEventsか、async/await + IProgress<T>の組み合わせの方が現場では素直だ。
ただしApplication.DoEventsは再入が起きやすいので、本格的な非同期にするならTask.Run + IProgress<T>で組む方が安全。Showを選ぶ時点で「並行制御を自前で組む覚悟」も込みになる。
3.設定画面・確認ダイアログ→ ShowDialog
「環境設定画面」「削除確認ダイアログ」「印刷オプション」のような一時的に呼び出して使い終わる画面はモーダルが正解だ。
using (var dlg = new SettingsForm(currentSettings))
{
if (dlg.ShowDialog(this)== DialogResult.OK)
{
currentSettings = dlg.UpdatedSettings;
SaveSettings(currentSettings);
}
}
設定画面は「設定を変えるか、変えずに閉じるか」の二択で、その間にメイン業務を進められても意味がない。むしろモードレスで開くと、設定途中なのか確定済みなのかユーザーが分からなくなるのでShowは避けた方がいい。
ここで「Showでも問題ないだろ」と思って組むと、後でユーザーから「設定画面開けたまま伝票登録しちゃって、設定が反映されないまま処理が走った」みたいなバグ報告が来る。設定の適用タイミングが曖昧になるのがモードレス設定画面の罠だ。
ハマりポイント3つ—俺が踏んだやつ
判断基準を覚えるだけだと事故は減らない。実際にコードを書く時に踏みやすい地雷を3つ挙げておく。
① ShowDialog後のDispose忘れでメモリ食い続ける
ShowDialogで開いたフォームは閉じても自動で破棄されない。usingを付けない実装だと、長時間動かす業務アプリでメモリがじわじわ増える。
// ❌ NG: dlgがGC待ちで残り続ける
var dlg = new MasterSelectForm();
dlg.ShowDialog(this);
//この後dlgを参照していなくても、Disposeされないので残骸が残る
// ✅ OK: usingで確実にDispose
using (var dlg = new MasterSelectForm())
{
dlg.ShowDialog(this);
}
これ俺、最初の正社員時代にやらかした。流通系の基幹システムで、マスタ参照ダイアログを1日数十回開く画面のShowDialogをusingなしで書いてて、ユーザーから「夕方になると重い」って報告が来た。半日デバッガで追ってメモリ使用量を見て気づくまで結構かかった。
② Showで同じフォームを2回開いてNRE
モードレスは何も制御しないと何個でも開ける。同じフォームを2回開いて、片方を閉じた時にもう片方からアクセスしてNullReferenceExceptionを出すのが定番。
// ❌ NG:押すたびに新しいLogFormが開く
private void btnShowLog_Click(object sender, EventArgs e)
{
var log = new LogForm();
log.Show();
}
// ✅ OK:既に開いてたら前面に出すだけ
private LogForm _logForm;
private void btnShowLog_Click(object sender, EventArgs e)
{
if (_logForm == null || _logForm.IsDisposed)
{
_logForm = new LogForm { Owner = this };
_logForm.FormClosed += (_, __)=> _logForm = null;
_logForm.Show();
}
else
{
_logForm.BringToFront();
}
}
IsDisposedチェックとFormClosedイベントで参照クリアの両方が要る。片方だけだと「閉じた後にもう一回開けない」とか「閉じたフォームの参照を触って例外」とか踏む。
③ Owner未設定で最前面に出ない・タスクバーに別ウィンドウとして出る
Showで開いたサブフォームにOwnerを設定しないと、Alt+Tabでアプリを切り替えた時にサブフォームと親フォームがバラバラに扱われる。タスクバーにも別アイコンで出てくるので、業務アプリとしては違和感がある。
// ❌ NG: Ownerなし。タスクバーに別ウィンドウとして並ぶ
var sub = new SubForm();
sub.Show();
// ✅ OK: Owner設定で親子関係明示。タスクバーアイコンも統合される
var sub = new SubForm();
sub.Owner = this; // ←これ
sub.Show();
//もしくはsub.Show(this);と1行で書いてもいい
ShowDialogの場合はdlg.ShowDialog(this)のように引数でOwnerを渡すパターンが多いが、Showだと引数なしで呼ぶ書き方を見かける。基本的にはOwner付けておく方が業務UIとしては綺麗だ。
俺の現場メモ—流通系SIer時代の業務ダイアログ実装
最初の正社員時代、流通系SIerの受託で2年間C# WinFormsばっか書いてた。後半はVS2019 + .NET Framework 4.7.2 + SQL Serverの構成で、業務ロジックのほとんどがDataAdapter + DataTable +生SQLという、客先常駐でC#を回してる現場の人なら見覚えのあるスタックだ。
その現場でマスタ参照ダイアログを画面ごとに作るのがチームの慣習になってて、最初はShowDialog/Showをなんとなくで使い分けてた。半年くらいして「マスタ参照はShowDialog固定、進捗系はShow固定」のチーム規約を後輩と一緒に明文化した。理由は、判断基準が個人差で揺れると画面ごとの操作性がバラバラになるからだ。明細登録画面の取引先選択はShowDialogで動かないのに、伝票検索の取引先選択はShowで並行操作できる、みたいなのが混在するとユーザーから「画面ごとに使い方違うんだけど」と苦情が来る。
ルール化した時の決め方はシンプルで、「ユーザーが値を選び終わるまでメイン画面の操作を止めるべきか?」をYes/Noで振った。YesならShowDialog、NoならShow。これだけで判断は9割揃う。残り1割の「進捗だけど確認も挟みたい」みたいな例外パターンは、進捗フォーム自体をShowで出して、その中で確認ダイアログだけShowDialogにする、という二段構造でしのいだ。
業務SEがWinForms触る時、コードの細かい書き方より「画面の操作モデルを統一する」ことが効くタイプの工夫が多い。ShowDialogとShowの使い分けはまさにそれだ。地味な規約だけど、チームで揃えておくと後から触る人の事故が確実に減る。
まとめ
ここまででShowDialogとShowの判断軸はだいたい押さえたと思う。要点をもう一度:
- ShowDialog:呼び出し側を止めて結果を受け取りたい時。マスタ参照・設定・確認ダイアログ。
usingでDispose忘れを防ぐ - Show:並行操作させたい時。進捗・ログ・常時表示パネル。
Owner設定と多重起動制御は自前で - 判断基準:「メイン操作を止めるべきか?」のYes/Noで振り分ければ9割揃う
- 業務SE的には:画面ごとに揺らさず、モーダル/モードレスの基準をチーム内で先に決めておく方が事故が減る
VS2019 + .NET Framework 4.7.2の現場でも、最新.NET 8 + WinFormsでも、ShowDialog/Showの挙動は本質的に変わってない。WinFormsを扱う限り長く使う知識なので、判断軸を1回固めておくと長く効く。
よくある質問
Q1. ShowDialogで開いたフォームから値を返す一番素直な方法は?
A.サブフォーム側にpublicプロパティを生やしておいて、DialogResult.OKで閉じた後に親側で読み取るのが一番素直。
//サブフォーム側
public string SelectedCode { get; private set; }
private void btnOK_Click(object sender, EventArgs e)
{
SelectedCode = dgvList.CurrentRow.Cells["Code"].Value.ToString();
this.DialogResult = DialogResult.OK; // ←これでCloseされる
}
DialogResultを代入した瞬間にCloseまで走るので、btnOK_Click内で明示的にClose()を呼ぶコードを書いてる人がいたら、その1行は実は冗長だ。
Q2. Showで開いたフォームから親に値を渡したい時は?
A.一番素直なのはサブフォーム側にイベントを生やして、親で購読する。
//サブフォーム
public event EventHandler<string> CodeSelected;
//何か選んだ時
CodeSelected?.Invoke(this, selectedCode);
//親側
sub.CodeSelected += (s, code)=> txtCode.Text = code;
sub.Show(this);
共有オブジェクト(Singletonな状態クラス)を経由するパターンもあるが、状態管理が複雑になりがちなのでイベント経由の方が保守しやすい。
Q3. ShowDialogの中でもう一段ShowDialogを呼んでいい?
A.問題ない。ShowDialogのネストは普通に動く。設定画面を開いて、その中から「保存先フォルダ選択ダイアログ」をShowDialogで開く、みたいな使い方は業務アプリで頻繁に出てくるパターン。
ただしネストが深くなると、ユーザーから見て「今どの画面で何を編集してるか」分からなくなる。3段以上重ねるなら、画面構造そのものを見直した方がいい。
Q4. ShowDialogとShow、パフォーマンスの差はある?
A.体感できる差はない。どちらも内部的にはWin32のCreateWindow系を呼んでフォームを生成してる。
差が出るとしたらフォーム自体の初期化処理(コンストラクタやLoadイベント)の重さで、これはShowDialog/Showの選択とは独立した話だ。マスタの内容をLoadで全件取ってきてる重いダイアログは、ShowDialogにしてもShowにしてもモッサリする。
関連記事
- 【C#】FormとFormの間で値の共有・受け渡しをする — ShowDialog/Showを選んだ後、実際にフォーム間で値を渡す具体実装をやる時に効く
- 【C#】Formから別フォームを表示しメインフォームを切り替える —親子じゃなくメインを差し替えるパターン(MDI的な構造)が必要になった時に効く
以上!


コメント