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()も同じモーダルの仕組みなので、判断軸を固めたい時に効く
以上!
執筆者
バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


コメント