17

この記事は最終更新日から1年以上が経過しています。

「まともな」フォルダ選択ダイアログ(Vista以降) - 簡単に FileOpenDialog を実装してみる

はじめに

image.png

ユーザーにフォルダパスを選択してもらうためのダイアログボックスとして、WinFormsのコモンダイアログクラスには、FolderBrowserDialog クラスが用意されていますが、パスを手入力できなかったりと結構不便です。
Microsoft Office の Application.FileDialog オブジェクトを使うと、ファイル選択ダイアログボックスに近い、割とリッチなUIでフォルダ選択ができます。

どうやら後者は Windows Vista 以降に用意された IFileOpenDialog インターフェイスをインプリメントした FileOpenDialog COMオブジェクトを利用したもののようです。
これを利用するには、"Windows API Code Pack" を利用する(有志が作成した NuGet パッケージあり)方法もありますが、exeモジュール単体に機能を同梱させることなどを想定して(NuGetパッケージは.dllで取り込まれる)、C#のコードで記述してみます。

※OpenFileDialogとは異なります。つづりが微妙に違うので注意

サンプルクラスのコード

FolderSelectDialog と命名してあります。詳細は、後日気が向いたときに...

FolderSelectDialog.cs
using System;
using System.Runtime.InteropServices;

namespace WindowsFormsApp1
{
    public class FolderSelectDialog
    {
        public string Path { get; set; }
        public string Title { get; set; }

        public System.Windows.Forms.DialogResult ShowDialog()
        {
            return ShowDialog(IntPtr.Zero);
        }

        public System.Windows.Forms.DialogResult ShowDialog(System.Windows.Forms.IWin32Window owner)
        {
            return ShowDialog(owner.Handle);
        }

        public System.Windows.Forms.DialogResult ShowDialog(IntPtr owner)
        {
            var dlg = new FileOpenDialogInternal() as IFileOpenDialog;
            try
            {
                dlg.SetOptions(FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM);

                IShellItem item;
                if (!string.IsNullOrEmpty(this.Path))
                {
                    IntPtr idl;
                    uint atts = 0;
                    if (NativeMethods.SHILCreateFromPath(this.Path, out idl, ref atts) == 0)
                    {
                        if (NativeMethods.SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item) == 0)
                        {
                            dlg.SetFolder(item);
                        }
                    }
                }

                if (!string.IsNullOrEmpty(this.Title))
                    dlg.SetTitle(this.Title);

                var hr = dlg.Show(owner);
                if (hr.Equals(NativeMethods.ERROR_CANCELLED))
                    return System.Windows.Forms.DialogResult.Cancel;
                if (!hr.Equals(0))
                    return System.Windows.Forms.DialogResult.Abort;

                dlg.GetResult(out item);
                string outputPath;
                item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out outputPath);
                this.Path = outputPath;

                return System.Windows.Forms.DialogResult.OK;
            }
            finally
            {
                Marshal.FinalReleaseComObject(dlg);
            }
        }

        [ComImport]
        [Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
        private class FileOpenDialogInternal
        {
        }

        // not fully defined と記載された宣言は、支障ない範囲で端折ってあります。
        [ComImport]
        [Guid("42f85136-db7e-439c-85f1-e4075d135fc8")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IFileOpenDialog
        {
            [PreserveSig]
            UInt32 Show([In] IntPtr hwndParent);
            void SetFileTypes();     // not fully defined
            void SetFileTypeIndex();     // not fully defined
            void GetFileTypeIndex();     // not fully defined
            void Advise(); // not fully defined
            void Unadvise();
            void SetOptions([In] FOS fos);
            void GetOptions(); // not fully defined
            void SetDefaultFolder(); // not fully defined
            void SetFolder(IShellItem psi);
            void GetFolder(); // not fully defined
            void GetCurrentSelection(); // not fully defined
            void SetFileName();  // not fully defined
            void GetFileName();  // not fully defined
            void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
            void SetOkButtonLabel(); // not fully defined
            void SetFileNameLabel(); // not fully defined
            void GetResult(out IShellItem ppsi);
            void AddPlace(); // not fully defined
            void SetDefaultExtension(); // not fully defined
            void Close(); // not fully defined
            void SetClientGuid();  // not fully defined
            void ClearClientData();
            void SetFilter(); // not fully defined
            void GetResults(); // not fully defined
            void GetSelectedItems(); // not fully defined
        }

        [ComImport]
        [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IShellItem
        {
            void BindToHandler(); // not fully defined
            void GetParent(); // not fully defined
            void GetDisplayName([In] SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
            void GetAttributes();  // not fully defined
            void Compare();  // not fully defined
        }

        private enum SIGDN : uint // not fully defined
        {
            SIGDN_FILESYSPATH = 0x80058000,
        }

        [Flags]
        private enum FOS // not fully defined
        {
            FOS_FORCEFILESYSTEM = 0x40,
            FOS_PICKFOLDERS = 0x20,
        }

        private class NativeMethods
        {
            [DllImport("shell32.dll")]
            public static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);

            [DllImport("shell32.dll")]
            public static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IShellItem ppsi);

            public const uint ERROR_CANCELLED = 0x800704C7;
        }
    }
}

サンプルの呼び出し方

image.png

FolderSelectDialog クラスのインスタンスを生成し、ShowDialog メソッドを呼び出すとダイアログボックスが表示されます。そして、ユーザーの応答結果がほかの一般的なコモンダイアログクラスのように DialogResult の値で返され、DialogResult.OK が返された場合は、Path プロパティに選択されたフォルダのパスが入ります。

SampleForm.cs
using System;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void buttonRef_Click(object sender, EventArgs e)
        {
            var dlg = new FolderSelectDialog();
            dlg.Path = textBox1.Text;
            if (dlg.ShowDialog() == DialogResult.OK)
                textBox1.Text = dlg.Path;
        }
    }
}

ご参考

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
otagaisama-1
自社の開発・運用で、皆様からの情報にとてもお世話になっています。ほぼ1人情SYSの身で、他のネット情報には少ないレアめなシステムを自社で使っている点でも、今めいたシステム情報入手面でも、Qiita のみなさまはとても貴重です。
この記事は以下の記事からリンクされています

コメント

これって使っていいんですか?

0

@Today_is_good_paint_too さん

Windows-API-Code-Pack-1.1のことでしょうか?

コードを見ていただければわかると思いますけれども、本稿は Windows-API-Code-Pack-1.1を使わない方法です。

0
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン