[C#][Windows Formsアプリ][SaveFileDialog] 実践:Filter/FilterIndex/DefaultExt/AddExtension/SupportMultiDottedExtensions を押さえる

スポンサーリンク

はじめに

この記事では、Windows Forms の SaveFileDialog における拡張子とフィルタ周りのキモ、「Filter」「FilterIndex」「DefaultExt」「AddExtension」「SupportMultiDottedExtensions」をまとめて解説します。ユーザーにわかりやすい拡張子の選択肢を提示しつつ、入力ミス(拡張子なし/不一致)を最小化する実装パターンを確認します。

学べること:
Filter 文字列の正しい書式と設計のコツ
FilterIndex(1始まり)の扱いと選択結果の取得
DefaultExtAddExtension による拡張子の自動付与
SupportMultiDottedExtensions.tar.gz のような多段拡張子を扱う

説明

Filter: ダイアログの拡張子コンボに表示する選択肢を指定します。書式は「表示名|パターン|表示名|パターン|...」。パターンは *.txt のようにワイルドカードで記述し、複数ある場合は *.png;*.jpg のようにセミコロンで連結します。

FilterIndex: 現在選択されているフィルタ(1始まり)の番号です。表示前に既定値をセットできますし、ダイアログを閉じた後に「ユーザーがどのフィルタを選んだか」を取得できます。

DefaultExt: ユーザーが拡張子を付けずに保存名を入力した場合に、自動で補う拡張子(ドット無し、例:txt)。

AddExtension: true のとき、ファイル名に拡張子が無い場合に拡張子を自動付与します。確実に動作させるため、DefaultExt を併せて設定しておくのが基本です。

SupportMultiDottedExtensions: .tar.gz のような「ドットを複数含む拡張子」をひとかたまりとして扱います。多段拡張子を既定で付与したいときは、DefaultExt = "tar.gz"AddExtension = trueSupportMultiDottedExtensions = 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 = trueDefaultExt = "tar.gz"AddExtension = true をセットで。どれか一つが欠けると意図通りになりません。

「すべてのファイル(*.*)」の落とし穴: このフィルタを選ぶと拡張子の制約が無くなり、ユーザーが不一致な拡張子を付けることがあります。保存側で拡張子の検証を行うか、後続処理が拡張子に依存しないようにしておきましょう。

まとめ

SaveFileDialog の拡張子まわりは、Filter で選択肢を設計し、FilterIndex でユーザーの意図を読み取り、DefaultExtAddExtension でミスを減らし、必要に応じて SupportMultiDottedExtensions で多段拡張子を正しく扱う——この型を押さえれば実用的で迷いにくい保存 UI が作れます。

Please follow and like us:

コメント

タイトルとURLをコピーしました