はじめに
この記事では、Windows Forms の SaveFileDialog における拡張子とフィルタ周りのキモ、「Filter」「FilterIndex」「DefaultExt」「AddExtension」「SupportMultiDottedExtensions」をまとめて解説します。ユーザーにわかりやすい拡張子の選択肢を提示しつつ、入力ミス(拡張子なし/不一致)を最小化する実装パターンを確認します。
学べること:
・Filter 文字列の正しい書式と設計のコツ
・FilterIndex(1始まり)の扱いと選択結果の取得
・DefaultExt と AddExtension による拡張子の自動付与
・SupportMultiDottedExtensions で .tar.gz のような多段拡張子を扱う
説明
Filter: ダイアログの拡張子コンボに表示する選択肢を指定します。書式は「表示名|パターン|表示名|パターン|...」。パターンは *.txt のようにワイルドカードで記述し、複数ある場合は *.png;*.jpg のようにセミコロンで連結します。
FilterIndex: 現在選択されているフィルタ(1始まり)の番号です。表示前に既定値をセットできますし、ダイアログを閉じた後に「ユーザーがどのフィルタを選んだか」を取得できます。
DefaultExt: ユーザーが拡張子を付けずに保存名を入力した場合に、自動で補う拡張子(ドット無し、例:txt)。
AddExtension: true のとき、ファイル名に拡張子が無い場合に拡張子を自動付与します。確実に動作させるため、DefaultExt を併せて設定しておくのが基本です。
SupportMultiDottedExtensions: .tar.gz のような「ドットを複数含む拡張子」をひとかたまりとして扱います。多段拡張子を既定で付与したいときは、DefaultExt = "tar.gz"、AddExtension = true、SupportMultiDottedExtensions = true を組み合わせます。
サンプルコード
テキストを入力して「保存…」でダイアログを開く例です。.txt/.csv/.json/.tar.gz をフィルタで用意し、拡張子の自動付与や選択されたフィルタの取得を確認できます。
public class MainForm : Form
{
private readonly TextBox _txt = new TextBox();
private readonly Button _btnSave = new Button();
public MainForm()
{
Text = "SaveFileDialog 拡張子とフィルタの基本";
Width = 560;
Height = 380;
_txt.Multiline = true;
_txt.ScrollBars = ScrollBars.Both;
_txt.Dock = DockStyle.Fill;
_btnSave.Text = "保存...";
_btnSave.AutoSize = true;
_btnSave.Click += OnSaveClick;
var bottom = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
FlowDirection = FlowDirection.RightToLeft,
Height = 56,
Padding = new Padding(8)
};
bottom.Controls.Add(_btnSave);
Controls.Add(_txt);
Controls.Add(bottom);
}
private void OnSaveClick(object sender, EventArgs e)
{
using (var sfd = new SaveFileDialog())
{
// ユーザーに提示する拡張子の選択肢
sfd.Filter =
"テキスト (*.txt)|*.txt|" +
"CSV (*.csv)|*.csv|" +
"JSON (*.json)|*.json|" +
"Tar+GZip (*.tar.gz)|*.tar.gz|" +
"すべてのファイル (*.*)|*.*";
// 起動時に選択される既定のフィルタ(1始まり)
sfd.FilterIndex = 1;
// 拡張子自動付与の既定(ドット無し)
sfd.DefaultExt = "txt";
sfd.AddExtension = true;
// 多段拡張子(.tar.gz 等)を一つの拡張子として扱う
sfd.SupportMultiDottedExtensions = true;
sfd.Title = "ファイルを保存";
var dr = sfd.ShowDialog(this);
if (dr == DialogResult.OK)
{
// ユーザーが確定したフルパス
var path = sfd.FileName;
// 必要に応じて拡張子を整える(選択フィルタに合わせたい場合)
path = NormalizeExtensionByFilterIndex(path, sfd.FilterIndex);
// 実際の保存(UTF-8)
File.WriteAllText(path, _txt.Text, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
MessageBox.Show(
"保存しました:\n" + path + "\n選択フィルタ Index: " + sfd.FilterIndex,
"保存完了",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
}
}
// フィルタ選択に応じて拡張子を補正するオプションの関数
private static string NormalizeExtensionByFilterIndex(string path, int filterIndex)
{
// すべてのファイル(*.*) を選んだ場合などは何もしない。
switch (filterIndex)
{
case 1: // *.txt
return EnsureExtension(path, ".txt");
case 2: // *.csv
return EnsureExtension(path, ".csv");
case 3: // *.json
return EnsureExtension(path, ".json");
case 4: // *.tar.gz(多段拡張子)
return EnsureMultiDotExtension(path, ".tar.gz");
default:
return path;
}
}
private static string EnsureExtension(string path, string dotExt)
{
// 既に同じ拡張子ならそのまま
if (path.EndsWith(dotExt, StringComparison.OrdinalIgnoreCase))
return path;
// 何らかの拡張子が付いている場合は置き換える
var current = Path.GetExtension(path);
if (!string.IsNullOrEmpty(current))
return Path.ChangeExtension(path, dotExt);
// 拡張子が無いなら付与
return path + dotExt;
}
private static string EnsureMultiDotExtension(string path, string multiDotExt) // 例: ".tar.gz"
{
if (path.EndsWith(multiDotExt, StringComparison.OrdinalIgnoreCase))
return path;
// 末尾が ".gz" だけ等のケースを ".tar.gz" に揃える
// いったん既存の拡張子を除去してから連結
var baseName = Path.Combine(Path.GetDirectoryName(path) ?? string.Empty,
Path.GetFileNameWithoutExtension(path) ?? string.Empty);
return baseName + multiDotExt;
}
}
— サンプルコードの解説 —
Filter: "表示名|パターン" のペアを | で連結しています。*.tar.gz のように多段拡張子もそのまま書けます。
FilterIndex: 1 から始まります。起動時に 1(*.txt)を既定選択とし、保存後の sfd.FilterIndex でユーザーが選んだ番号を取得しています。
DefaultExt + AddExtension: ユーザーが「memo」のように拡張子無しで保存名を入力した場合でも、DefaultExt = "txt" と AddExtension = true により memo.txt が付与されます。
SupportMultiDottedExtensions: true にすると .tar.gz を一つの拡張子として扱いやすくなります。補助関数 EnsureMultiDotExtension では、ユーザー入力の末尾が .gz のみ等でも .tar.gz に整えています。
拡張子の整合: 現実のアプリでは「選んだフィルタ」と「ファイル名の拡張子」を一致させると親切です。サンプルの NormalizeExtensionByFilterIndex がその一例です。
つまづきポイント
Filter の書式ミス: 「表示名」と「パターン」は必ずペア。奇数要素になっていると例外の原因になります。パターンに余分な空白が入っていて一致しないケースにも注意。
FilterIndex は 1 始まり: 0 から数えない点に注意。フィルタの並び順を変えたら、対応する分岐やメッセージも忘れず更新します。
DefaultExt にドットは付けない: DefaultExt = "txt" のようにドット無しで指定します(".txt" は不可)。
AddExtension の期待値: ユーザーが既に拡張子を入力している場合は、それが優先されます。自動付与させたい前提なら、保存直前に自前で拡張子を正規化しておくと安心です。
多段拡張子の扱い: .tar.gz を既定で付けたいときは、SupportMultiDottedExtensions = true と DefaultExt = "tar.gz" と AddExtension = true をセットで。どれか一つが欠けると意図通りになりません。
「すべてのファイル(*.*)」の落とし穴: このフィルタを選ぶと拡張子の制約が無くなり、ユーザーが不一致な拡張子を付けることがあります。保存側で拡張子の検証を行うか、後続処理が拡張子に依存しないようにしておきましょう。
まとめ
SaveFileDialog の拡張子まわりは、Filter で選択肢を設計し、FilterIndex でユーザーの意図を読み取り、DefaultExt+AddExtension でミスを減らし、必要に応じて SupportMultiDottedExtensions で多段拡張子を正しく扱う——この型を押さえれば実用的で迷いにくい保存 UI が作れます。


コメント