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

#はじめに

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100206/81bfaa02-bafb-417c-8008-8dde0fc4e5bf.png)

ユーザーにフォルダパスを選択してもらうためのダイアログボックスとして、WinFormsのコモンダイアログクラスには、[FolderBrowserDialog](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.folderbrowserdialog) クラスが用意されていますが、パスを手入力できなかったりと結構不便です。
Microsoft Office の [Application.FileDialog](https://docs.microsoft.com/ja-jp/office/vba/api/office.filedialog) オブジェクトを使うと、ファイル選択ダイアログボックスに近い、割とリッチなUIでフォルダ選択ができます。

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

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

#サンプルクラスのコード

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

```C#: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](https://qiita-image-store.s3.amazonaws.com/0/100206/0a907855-81f6-a7bd-2c1b-28d4145139c7.png)

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

```C#: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;
        }
    }
}

```

#ご参考
- [Show detailed Folder Browser from a PropertyGrid](https://stackoverflow.com/questions/15368771/show-detailed-folder-browser-from-a-propertygrid) - Stack Overflow <br> Simon Mourierさんのご回答に大感謝！
- [Windows-API-Code-Pack-1.1](https://github.com/aybe/Windows-API-Code-Pack-1.1/)
    - [CommonFileDialog.cs](https://github.com/aybe/Windows-API-Code-Pack-1.1/blob/master/source/WindowsAPICodePack/Shell/CommonFileDialogs/CommonFileDialog.cs)