using Microsoft.Win32; using System.Runtime.InteropServices; // ReSharper disable UnusedMember.Local namespace Melskin.Controls; /// /// Vista风格的文件夹选择对话框,支持多选、初始文件夹设置等功能。 /// public sealed class VistaFolderBrowserDialog { /// /// 创建原生的IFileOpenDialog实例。 /// /// IFileOpenDialog实例。 private static IFileOpenDialog CreateNativeDialog() { return (IFileOpenDialog)new FileOpenDialog(); } /// /// 获取对话框的选项配置(Fos枚举)。 /// /// Fos选项集合。 private Fos GetDialogOptions() { var options = Fos.Pickfolders; if (Multiselect) { options |= Fos.Allowmultiselect; } if (!AllowNonStoragePlaces) { options |= Fos.Forcefilesystem; } return options; } /// /// 获取指定Shell项的路径和元素名称。 /// /// Shell项。 /// 输出:文件系统路径。 /// 输出:元素名称。 private static void GetPathAndElementName(IShellItem item, out string? path, out string elementName) { item.GetDisplayName(Sigdn.Parentrelativeforaddressbar, out elementName); try { item.GetDisplayName(Sigdn.Filesyspath, out path); } catch (ArgumentException ex) when (ex.HResult == -2147024809) { path = null; } } /// /// 设置对话框选择结果到属性(支持单选和多选)。 /// /// IFileOpenDialog实例。 private void SetDialogResults(IFileOpenDialog dialog) { IShellItem item; if (!Multiselect) { dialog.GetResult(out item); GetPathAndElementName(item, out var path, out var value); SelectedPath = path; SelectedPaths = [path]; SelectedElementName = value; SelectedElementNames = [value]; } else { dialog.GetResults(out var items); items.GetCount(out var count); SelectedPaths = new string[count]; SelectedElementNames = new string[count]; for (uint i = 0; i < count; ++i) { items.GetItemAt(i, out item); GetPathAndElementName(item, out var path, out var value); SelectedPaths[i] = path; SelectedElementNames[i] = value; } SelectedPath = null; SelectedElementName = null; } } /// /// 设置对话框的初始文件夹。 /// /// IFileOpenDialog实例。 private void SetInitialFolder(IFileOpenDialog dialog) { if (!string.IsNullOrEmpty(SelectedPath)) { uint atts = 0; if (NativeMethods.SHILCreateFromPath(SelectedPath, out var idl, ref atts) == 0 && NativeMethods.SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out var item) == 0) { dialog.SetFolder(item); } } } /// /// 设置对话框的选项。 /// /// IFileOpenDialog实例。 private void SetOptions(IFileOpenDialog dialog) { var options = GetDialogOptions(); if (!ShowNewFolderButton) { options |= Fos.Notestfilecreate; } dialog.SetOptions(options); } /// /// 显示对话框,无所有者窗口。 /// /// 用户是否选择了文件夹。 public bool ShowDialog() { return ShowDialog(IntPtr.Zero); } /// /// 显示对话框,指定WPF窗口作为所有者。 /// /// WPF窗口。 /// 用户是否选择了文件夹。 public bool ShowDialog(Window? owner) { return ShowDialog(owner == null ? IntPtr.Zero : new WindowInteropHelper(owner).Handle); } /// /// 显示对话框,指定Win32窗口作为所有者。 /// /// Win32窗口。 /// 用户是否选择了文件夹。 public bool ShowDialog(IWin32Window? owner) { return ShowDialog(owner?.Handle ?? IntPtr.Zero); } /// /// 显示对话框,指定窗口句柄作为所有者。 /// /// 窗口句柄。 /// 用户是否选择了文件夹。 public bool ShowDialog(IntPtr owner) { if (Environment.OSVersion.Version.Major < 6) { throw new InvalidOperationException("对话框需要至少在Visia系统以上才能使用"); } var dialog = CreateNativeDialog(); try { SetInitialFolder(dialog); SetOptions(dialog); if (!string.IsNullOrEmpty(Title)) { dialog.SetTitle(Title); } if (dialog.Show(owner) != 0) { return false; } SetDialogResults(dialog); return true; } finally { Marshal.ReleaseComObject(dialog); } } /// /// 是否允许选择非存储位置(如库、虚拟文件夹)。 /// public bool AllowNonStoragePlaces { get; set; } /// /// 是否允许多选文件夹。 /// public bool Multiselect { get; set; } /// /// 选中的元素名称(单选时)。 /// public string? SelectedElementName { get; private set; } /// /// 选中的所有元素名称(多选时)。 /// public string[]? SelectedElementNames { get; private set; } /// /// 选中的文件夹路径(单选时)。 /// public string? SelectedPath { get; set; } /// /// 选中的所有文件夹路径(多选时)。 /// public string?[]? SelectedPaths { get; private set; } // 属性声明 /// /// 获取或设置对话框的标题。 /// public string? Title { get; set; } /// /// 是否显示新建文件夹按钮。 /// public bool ShowNewFolderButton { get; set; } = true; // 以下为内部COM接口和枚举定义,通常无需注释 [ComImport] [Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] private class FileOpenDialog { } /// /// 提供了与Windows Shell交互所需的一些本地方法。这些方法主要用于实现文件夹浏览器对话框的功能,如获取当前活动窗口句柄、从给定路径创建项目标识符列表以及根据PIDL创建IShellItem接口实例。 /// private class NativeMethods { /// /// 获取当前活动窗口的句柄。 /// /// 当前活动窗口的句柄。 [DllImport("user32.dll")] public static extern IntPtr GetActiveWindow(); /// /// 从给定的PIDL创建一个IShellItem接口实例。 /// /// 父项的PIDL。如果为IntPtr.Zero,则表示桌面。 /// 父项的IShellFolder接口指针。如果为IntPtr.Zero,则使用默认的桌面文件夹。 /// 要创建IShellItem接口实例的PIDL。 /// 输出参数,返回创建的IShellItem接口实例。 /// 如果函数成功,则返回0;否则返回错误代码。 [DllImport("shell32.dll")] public static extern int SHCreateShellItem( IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IShellItem ppsi); /// /// 从给定的路径创建一个项目标识符列表。 /// /// 要转换为项目标识符列表的路径字符串。 /// 输出参数,用于接收指向项目标识符列表的指针。 /// 输出参数,返回关于解析路径的信息。 /// 如果函数成功,则返回0;否则返回错误代码。 [DllImport("shell32.dll")] public static extern int SHILCreateFromPath( [MarshalAs(UnmanagedType.LPWStr)] string? pszPath, out IntPtr ppIdl, ref uint rgflnOut); } [Flags] private enum Fos { Allnonstorageitems = 0x80, Allowmultiselect = 0x200, Createprompt = 0x2000, Defaultnominimode = 0x20000000, Dontaddtorecent = 0x2000000, Filemustexist = 0x1000, Forcefilesystem = 0x40, Forceshowhidden = 0x10000000, Hidemruplaces = 0x20000, Hidepinnedplaces = 0x40000, Nochangedir = 8, Nodereferencelinks = 0x100000, Noreadonlyreturn = 0x8000, Notestfilecreate = 0x10000, Novalidate = 0x100, Overwriteprompt = 2, Pathmustexist = 0x800, Pickfolders = 0x20, Shareaware = 0x4000, Strictfiletypes = 4 } [ComImport] [Guid("d57c7288-d4ad-4768-be02-9d969532d960")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [CoClass(typeof(FileOpenDialog))] private interface IFileOpenDialog { [PreserveSig] int Show([In] IntPtr parent); void SetFileTypes([In] uint cFileTypes, [In][MarshalAs(UnmanagedType.Struct)] ref IntPtr rgFilterSpec); void SetFileTypeIndex([In] uint iFileType); void GetFileTypeIndex(out uint piFileType); void Advise([In][MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie); void Unadvise([In] uint dwCookie); void SetOptions([In] Fos fos); void GetOptions(out Fos pfos); void SetDefaultFolder([In][MarshalAs(UnmanagedType.Interface)] IShellItem psi); void SetFolder([In][MarshalAs(UnmanagedType.Interface)] IShellItem psi); void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); void SetFileName([In][MarshalAs(UnmanagedType.LPWStr)] string pszName); void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); void SetTitle([In][MarshalAs(UnmanagedType.LPWStr)] string? pszTitle); void SetOkButtonLabel([In][MarshalAs(UnmanagedType.LPWStr)] string pszText); void SetFileNameLabel([In][MarshalAs(UnmanagedType.LPWStr)] string pszLabel); void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); void AddPlace([In][MarshalAs(UnmanagedType.Interface)] IShellItem psi, FileDialogCustomPlace fdcp); void SetDefaultExtension([In][MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); void Close([MarshalAs(UnmanagedType.Error)] int hr); void SetClientGuid([In] ref Guid guid); void ClearClientData(); void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter); void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum); void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai); } /// /// 代表一个Shell项,提供了获取项的名称、属性、父项以及与其他项比较等功能。 /// [ComImport] [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellItem { void BindToHandler( [In][MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid bhid, [In] ref Guid riid, out IntPtr ppv); void GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); void GetDisplayName([In] Sigdn sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); void GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs); void Compare([In][MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); } [ComImport] [Guid("B63EA76D-1F85-456F-A19C-48159EFA858B")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellItemArray { void BindToHandler( [In][MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, out IntPtr ppvOut); void GetPropertyStore([In] int flags, [In] ref Guid riid, out IntPtr ppv); void GetPropertyDescriptionList( [In][MarshalAs(UnmanagedType.Struct)] ref IntPtr keyType, [In] ref Guid riid, out IntPtr ppv); void GetAttributes( [In][MarshalAs(UnmanagedType.I4)] IntPtr dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs); void GetCount(out uint pdwNumItems); void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems); } private enum Sigdn : uint { Desktopabsoluteediting = 0x8004c000, Desktopabsoluteparsing = 0x80028000, Filesyspath = 0x80058000, Normaldisplay = 0, Parentrelative = 0x80080001, Parentrelativeediting = 0x80031001, Parentrelativeforaddressbar = 0x8007c001, Parentrelativeparsing = 0x80018001, Url = 0x80068000 } }