C# WinForms の Form.ShowDialog と Form.Show の違いと使い分け完全ガイド

C# WinForms の Form.ShowDialog と Form.Show の違いと使い分け完全ガイド

みなさんこんにちは!ヒロポンです!

WinForms の業務アプリ書いてると、サブフォーム出すたびに ShowDialog にするか Show にするかで毎回ちょっと迷うこと、ないっすか?

俺も最初の頃、感覚でやって痛い目を見た。マスタ参照ダイアログを Show にしてユーザーに親フォームをガチャガチャ触られてデータの整合性が壊れたり、進捗フォームを ShowDialog にして本処理が止まったり。動かないわけじゃないから気づきにくい、でも仕様としては事故ってる、ってやつだ。

この記事では VS2019・.NET Framework 4.7.2 の業務系現場でずっと使ってきた ShowDialogShow の使い分け基準 を、コード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日数十回開く画面の ShowDialogusing なしで書いてて、ユーザーから「夕方になると重い」って報告が来た。半日デバッガで追ってメモリ使用量を見て気づくまで結構かかった。

② 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 の使い分けはまさにそれだ。地味な規約だけど、チームで揃えておくと後から触る人の事故が確実に減る。

まとめ

ここまでで ShowDialogShow の判断軸はだいたい押さえたと思う。要点をもう一度:

  • 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 にしてもモッサリする。

関連記事

以上!

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次