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 の中で何が起きてるか) を理解したい人は、関連記事側に進むと自然な流れだ。
関連記事
- C# WinForms の Form.ShowDialog と Form.Show の違いと使い分け完全ガイド —
OpenFileDialog.ShowDialog()も同じモーダルの仕組みなので、判断軸を固めたい時に効く
以上!


コメント