C# OpenFileDialog をフォームのフィールドにする時の正しい書き方

C# OpenFileDialog をフォームのフィールドにする時の正しい書き方

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

C# WinForms でファイル取り込み機能を作る時、OpenFileDialog をどう持つか、毎回ちょっと迷うこと、ないっすか?

// 多くの人がこう書いてる(俺も最初これだった)
private void btnImport_Click(object sender, EventArgs e)
{
    using (var ofd = new OpenFileDialog())
    {
        ofd.Filter = "CSVファイル (*.csv)|*.csv|すべて (*.*)|*.*";
        if (ofd.ShowDialog(this) == DialogResult.OK)
        {
            ImportCsv(ofd.FileName);
        }
    }
}

これでも動くんだけど、業務系で 「マスタアップロード機能を1日に何度も使う」 とか 「前回開いたフォルダを覚えていてほしい」 みたいな要件が出ると、毎回 new してたら不便なことに気づく。フィルタも毎回書き直すし、InitialDirectory を覚えさせる場所もない。

この記事では VS2019・.NET Framework 4.7.2・C# 7.3 の業務系で実際に使ってる OpenFileDialog をフォームのフィールド化する書き方 を、ローカル using 版との対比+Designer 配置版+ハマり3点で整理する。

3行で結論:

  • 使い回しが2回以上ならフィールド化(前回フォルダ記憶・Filter プリセット保持・Designer プロパティ管理がラク)
  • 単発の取り込みならローカル using(軽くて Dispose 忘れの心配ゼロ)
  • フィールド化したら InitialDirectory を毎回上書きしないこと — 一番踏みやすい罠

💡 モーダル/モードレスの選択判断(ShowDialog vs Show)は別記事 C# WinForms の Form.ShowDialog と Form.Show の違いと使い分け完全ガイド で書いてる。OpenFileDialog の ShowDialog も同じモーダルの仕組みだ。

目次

ローカル using 版(基本パターン)

まず一番シンプルな書き方から。クリックハンドラの中で new して using で囲む。

private void btnImport_Click(object sender, EventArgs e)
{
    using (var ofd = new OpenFileDialog())
    {
        ofd.Title = "取り込みファイルを選択";
        ofd.Filter = "CSVファイル (*.csv)|*.csv|Excel (*.xlsx)|*.xlsx|すべて (*.*)|*.*";
        ofd.FilterIndex = 1;
        ofd.Multiselect = false;

        if (ofd.ShowDialog(this) == DialogResult.OK)
        {
            ImportCsv(ofd.FileName);
        }
    }   // ← ブロック抜けで自動 Dispose
}

このパターンの良いところは:

  • Dispose 忘れの心配がゼロusing で囲ってあれば確実に解放される
  • コードがそのボタンに閉じる — 影響範囲が読みやすい
  • 状態を持たない — テストしやすい・副作用が起きない

業務系のアプリで「この画面でしか使わない、しかも単発のファイル選択」ならこの書き方で十分だ。読み手にも意図が一発で伝わる。

ただし欠点として、ボタンを押すたびに OpenFileDialog インスタンスが新規作成される。ユーザーが前回開いたフォルダを覚えさせたい場合や、Filter を画面間で統一したい場合は 状態を持つ場所がない ので別の手段が要る。

フィールド化版(コンストラクタ初期化 + Form.Dispose 連動)

「同じ画面で何度も開く」「前回フォルダを記憶させたい」要件が出てきたら、OpenFileDialogクラスフィールド にして使い回す。

public partial class ImportForm : Form
{
    private readonly OpenFileDialog _csvOfd;

    public ImportForm()
    {
        InitializeComponent();

        _csvOfd = new OpenFileDialog
        {
            Title = "取り込みファイルを選択",
            Filter = "CSVファイル (*.csv)|*.csv|Excel (*.xlsx)|*.xlsx|すべて (*.*)|*.*",
            FilterIndex = 1,
            Multiselect = false
        };

        // フォーム破棄時に OpenFileDialog も Dispose する
        this.Disposed += (_, __) => _csvOfd.Dispose();
    }

    private void btnImport_Click(object sender, EventArgs e)
    {
        if (_csvOfd.ShowDialog(this) == DialogResult.OK)
        {
            ImportCsv(_csvOfd.FileName);
            // _csvOfd.FileName / FileNames / InitialDirectory は次回まで保持される
        }
    }
}

ポイントは3つ:

  • コンストラクタで1回だけ初期化 — Filter や Title を毎回書かなくていい
  • Disposed イベントで Dispose — フォームが閉じたタイミングで連動破棄
  • OpenFileDialog の状態が保持されるFileName/前回ディレクトリが次回呼び出しまで残る

これだけで「前回開いたフォルダから始まる」挙動が標準で手に入る。OpenFileDialog の中で InitialDirectory を空のままにしておくと、Windows 標準の挙動として 前回 OpenFileDialog を閉じた時の場所 が次回開く時の出発点になる。

ただし欠点として、フォームのライフサイクルと OpenFileDialog の寿命が結合する。同一画面に5つも6つも OpenFileDialog フィールドを持つと、コンストラクタが膨らんで初期化処理が読みにくくなる。多用する画面は次の Designer 配置版を検討する方がラクだ。

フィールド化 + Designer 配置版(別解)

Visual Studio の Designer 上で OpenFileDialogコンポーネントとしてフォームに配置 する方法もある。ツールボックスから OpenFileDialog をフォームにドラッグすると、フォーム下部の「コンポーネント トレイ」に openFileDialog1 が現れる。

Designer 配置版の特徴:

  • Filter / Title / Multiselect を Visual Studio のプロパティウィンドウから設定 — コードを書かずに済む
  • Dispose は自動連動Form.Dispose で配置したコンポーネントも一緒に破棄される(Designer.cs で components.Add(...) されている)
  • コードビハインドからは openFileDialog1 というフィールドで触れる
// Designer 配置済み openFileDialog1 をクリックハンドラから使う
private void btnImport_Click(object sender, EventArgs e)
{
    if (this.openFileDialog1.ShowDialog(this) == DialogResult.OK)
    {
        ImportCsv(this.openFileDialog1.FileName);
    }
}

業務系のチームで複数の画面が同じ Filter を使うなら、ベースフォーム(Form を継承した親クラス)に OpenFileDialog を Designer 配置 しておいて、子フォームから継承する手も使える。Filter プリセットがチーム全体で揃う。

ただし欠点として、Designer 配置は InitializeComponent の自動生成コードに依存 するので、Designer.cs を手で編集すると壊れやすい。プロパティの動的な切り替え(実行時にユーザー権限で Filter を変える等)はコードから書く方がやりやすい。

フィールド化の具体的メリット2点

① 前回開いたフォルダの自動記憶

OpenFileDialog同じインスタンスで使い回す だけで、前回開いた場所が自動で記憶される。これは Windows の OpenFileDialog 標準挙動で、追加のコードは要らない。

// インスタンスを使い回せば、前回フォルダから自動で開く
if (_csvOfd.ShowDialog(this) == DialogResult.OK)
{
    // ... 取り込み処理 ...
}

// 次回ボタンクリック時、前回のフォルダから始まる

この挙動を 明示的にリセットしたい 場合だけ _csvOfd.InitialDirectory = "" を呼ぶ。それ以外は触らない。

② Filter プリセットを画面間で統一

業務系で「全画面で CSV / Excel / すべて の3択 Filter を統一したい」みたいな要件が出てきた時、フィールド化(or Designer 配置)してあれば 1ヶ所で定義できる。Common 系のクラスに OpenFileDialog ファクトリメソッドを置いておく書き方もよく使う。

public static class DialogFactory
{
    public static OpenFileDialog CreateCsvOpenDialog()
    {
        return new OpenFileDialog
        {
            Title = "取り込みファイルを選択",
            Filter = "CSVファイル (*.csv)|*.csv|Excel (*.xlsx)|*.xlsx|すべて (*.*)|*.*",
            FilterIndex = 1,
            Multiselect = false
        };
    }
}

// 各フォームで
_csvOfd = DialogFactory.CreateCsvOpenDialog();

これでチーム全員が同じ Filter 文字列を書かずに済む。新しいファイル形式に対応したい時も DialogFactory の1ヶ所を直せば全画面に反映される。

ハマりポイント3つ — 俺が踏んだやつ

① InitialDirectory を毎回上書きしてて記憶効果ゼロ

これ、フィールド化のメリットを最も殺す罠だ。「前回フォルダから開きたい」と思ってフィールド化したのに、ボタンクリックハンドラで毎回 InitialDirectory を設定してたら 記憶効果はゼロ

// ❌ NG: 毎回 InitialDirectory を上書き → 記憶効果ゼロ
private void btnImport_Click(object sender, EventArgs e)
{
    _csvOfd.InitialDirectory = @"C:\Import";   // ← これが入ってると毎回ここから始まる
    if (_csvOfd.ShowDialog(this) == DialogResult.OK) { ... }
}

// ✅ OK: 初回だけ設定、それ以降は OpenFileDialog の自動記憶に任せる
private bool _initialDirSet = false;

private void btnImport_Click(object sender, EventArgs e)
{
    if (!_initialDirSet)
    {
        _csvOfd.InitialDirectory = @"C:\Import";
        _initialDirSet = true;
    }
    if (_csvOfd.ShowDialog(this) == DialogResult.OK) { ... }
}

これ俺、最初の正社員時代に 半日デバッガで追ってやっと気づいた やつ。ユーザーから「前回のフォルダから開かなくない?」と報告が来て、「フィールド化してるのに変だな」と思って原因を辿ったら、自分が InitialDirectory を毎回上書きしてた。流通系の基幹システムの取り込み画面で、テスト時にデフォルトフォルダを設定したコードがそのまま残ってた、というオチだった。

② Form.Dispose されない子フォームでメモリリーク

OpenFileDialog をフィールド化した子フォームを Show() で開きっぱなしにして、親のマネージャから参照を握ったまま、というパターンで踏む。フォームが GC されないので OpenFileDialog も道連れで残る。

// ❌ NG: ChildForm を Show したまま参照を残す → ChildForm.Dispose されず OpenFileDialog も残る
appWideManager.OpenChild(new ChildForm());   // 内部で List<Form> に保持

// ✅ OK: ChildForm の FormClosed で参照を切る
var child = new ChildForm();
child.FormClosed += (_, __) => appWideManager.RemoveChild(child);
child.Show();

長時間動かす業務アプリで「夕方になるとメモリ使用量がじわじわ上がる」症状が出たら、OpenFileDialog をフィールド化した子フォームの解放漏れを疑うのは定番。これは 数日経ってからプロファイラかけてやっと見えた 類の遅延バグだ。

③ Designer 配置時の Component 名衝突

Designer で OpenFileDialog を複数配置すると openFileDialog1 openFileDialog2 … と自動命名される。これを 意味のある名前にリネーム しないと、後でコードを読む時に「どっちが CSV 用?」「どっちが画像用?」が分からなくなる。

// ❌ NG: Designer のデフォルト名のまま
this.openFileDialog1.ShowDialog(this);   // CSV用?画像用?
this.openFileDialog2.ShowDialog(this);

// ✅ OK: Designer のプロパティウィンドウで Name を意味付け
this.csvImportDialog.ShowDialog(this);
this.imageUploadDialog.ShowDialog(this);

これは すぐに気づける罠 だけど、後で触る人が「openFileDialog1 って何のやつ?」と読み返す時間ロスを地味に積む。Designer 配置するなら配置直後にリネームを併せてやる習慣をつけておくと、半年後の自分が助かる。

著者の現場メモ — 流通系SIer時代の OpenFileDialog 運用

最初の正社員時代、流通系SIer の受託で2年間 C# WinForms ばっか書いてた。VS2019・.NET Framework 4.7.2・C# 7.3 の構成で、業務ロジックは DataAdapter + DataTable + 生SQL、画面は Designer で組む典型構成だった。

その現場で マスタ取り込み画面(CSV/Excel)を画面ごとに作る のがチームの慣習になってて、最初は各画面でローカル using で書いてた。半年くらいして「同じ Filter 文字列を5画面で書き直してる」「ユーザーから前回フォルダ記憶のリクエストが来た」というタイミングで、ベースフォームに OpenFileDialog を Designer 配置 + Filter プリセット に切り替えた。

ルール化した時の決め方はシンプルで、「1画面で2回以上呼ぶか/チームで Filter を統一したいか」を Yes/No で振った。Yes ならフィールド化(or Designer 配置)、No ならローカル using。これで判断が9割揃った。残り1割の例外(権限ごとに Filter を切り替えたい等)はコードビハインドから動的に書く、という二段構えで運用した。

業務SE が WinForms 触る時、同じ書き方を画面間で揃える ことがコードの細かい書き方より効くタイプの工夫が多い。OpenFileDialog の持ち方の選択もそのひとつだ。

まとめ

ここまでで OpenFileDialog の3パターンとハマりはだいたい押さえた。要点をもう一度:

  • 使い回し2回以上 or 状態保持したい → フィールド化(コンストラクタ初期化 + Disposed で連動破棄)
  • チーム全画面で Filter を揃えたい → Designer 配置 + ベースフォーム継承 or DialogFactory パターン
  • 単発の取り込み → ローカル using で十分
  • InitialDirectory は毎回上書きしない — 自動記憶を活かすため、初回 or 必要時だけ設定

VS2019・.NET Framework 4.7.2・C# 7.3 の業務系でも、最新 .NET 8 + WinForms でも、OpenFileDialog の挙動はほぼ変わらない。WinForms の業務アプリを保守する限り長く使う知識 なので、判断軸を1回決めておくとラクだ。

よくある質問

Q1. SaveFileDialog も同じパターンでフィールド化していい?

A. 全く同じパターンで OK。SaveFileDialog も状態(FileName/前回フォルダ)を保持するし、Filter プリセットを使い回せる。OverwritePrompt プロパティだけ追加で意識する程度で、設計判断は一緒だ。

Q2. フィールド化した OpenFileDialog のテストは書きづらくない?

A. WinForms のダイアログ系は そのままテストするのは難しい。一般的には IFileSelector のような抽象を1段かませてテストする。

public interface IFileSelector
{
    string SelectFile(string filter, string initialDirectory);
}

public class OpenFileDialogSelector : IFileSelector
{
    private readonly OpenFileDialog _ofd = new OpenFileDialog();

    public string SelectFile(string filter, string initialDirectory)
    {
        _ofd.Filter = filter;
        if (!string.IsNullOrEmpty(initialDirectory))
            _ofd.InitialDirectory = initialDirectory;
        return _ofd.ShowDialog() == DialogResult.OK ? _ofd.FileName : null;
    }
}

ロジック側は IFileSelector を受け取り、テストでは固定パスを返すモック実装に差し替える、というやり方。業務系で WinForms をユニットテストする時の定番パターンだ。

Q3. WPF / .NET 8 の WinForms で何か変わる?

A. OpenFileDialog のクラス自体は System.Windows.Forms 名前空間にあって、.NET Framework 4.7.2 でも .NET 8 でも挙動は同じ。WPF は別物の Microsoft.Win32.OpenFileDialog を使うけど、フィールド化の判断基準は同じ(使い回しなら field、単発なら local using)。

Q4. Multiselect = true で複数選択したファイルはどう取れる?

A. OpenFileDialog.FileNames プロパティ(複数形)で string[] として取れる。

_csvOfd.Multiselect = true;
if (_csvOfd.ShowDialog(this) == DialogResult.OK)
{
    foreach (string path in _csvOfd.FileNames)
    {
        ImportCsv(path);
    }
}

Multiselect画面を作る時に決めて、後で動的に切り替えない のがチーム規約として安全。フィールド化してフォームごとに固定値で持つのが向く。

ここまでで OpenFileDialog の持ち方とハマりは押さえた。次は OpenFileDialog 自体のモーダル挙動(ShowDialog の中で何が起きてるか) を理解したい人は、関連記事側に進むと自然な流れだ。

関連記事

以上!

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

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

コメント

コメントする

CAPTCHA


目次