453 lines
14 KiB
C#
453 lines
14 KiB
C#
using System;
|
||
using Microsoft.Win32;
|
||
using System.Runtime.InteropServices;
|
||
using System.Windows;
|
||
using System.Windows.Interop;
|
||
|
||
// ReSharper disable UnusedMember.Local
|
||
|
||
/// <summary>
|
||
/// Vista风格的文件夹选择对话框,支持多选、初始文件夹设置等功能。
|
||
/// </summary>
|
||
public sealed class VistaFolderBrowserDialog
|
||
{
|
||
/// <summary>
|
||
/// 创建原生的IFileOpenDialog实例。
|
||
/// </summary>
|
||
/// <returns>IFileOpenDialog实例。</returns>
|
||
private static IFileOpenDialog CreateNativeDialog() { return (IFileOpenDialog)new FileOpenDialog(); }
|
||
|
||
/// <summary>
|
||
/// 获取对话框的选项配置(Fos枚举)。
|
||
/// </summary>
|
||
/// <returns>Fos选项集合。</returns>
|
||
private Fos GetDialogOptions()
|
||
{
|
||
var options = Fos.Pickfolders;
|
||
if (Multiselect)
|
||
{
|
||
options |= Fos.Allowmultiselect;
|
||
}
|
||
|
||
if (!AllowNonStoragePlaces)
|
||
{
|
||
options |= Fos.Forcefilesystem;
|
||
}
|
||
|
||
return options;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定Shell项的路径和元素名称。
|
||
/// </summary>
|
||
/// <param name="item">Shell项。</param>
|
||
/// <param name="path">输出:文件系统路径。</param>
|
||
/// <param name="elementName">输出:元素名称。</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置对话框选择结果到属性(支持单选和多选)。
|
||
/// </summary>
|
||
/// <param name="dialog">IFileOpenDialog实例。</param>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置对话框的初始文件夹。
|
||
/// </summary>
|
||
/// <param name="dialog">IFileOpenDialog实例。</param>
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置对话框的选项。
|
||
/// </summary>
|
||
/// <param name="dialog">IFileOpenDialog实例。</param>
|
||
private void SetOptions(IFileOpenDialog dialog)
|
||
{
|
||
var options = GetDialogOptions();
|
||
if (!ShowNewFolderButton)
|
||
{
|
||
options |= Fos.Notestfilecreate;
|
||
}
|
||
dialog.SetOptions(options);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示对话框,无所有者窗口。
|
||
/// </summary>
|
||
/// <returns>用户是否选择了文件夹。</returns>
|
||
public bool ShowDialog() { return ShowDialog(IntPtr.Zero); }
|
||
|
||
/// <summary>
|
||
/// 显示对话框,指定WPF窗口作为所有者。
|
||
/// </summary>
|
||
/// <param name="owner">WPF窗口。</param>
|
||
/// <returns>用户是否选择了文件夹。</returns>
|
||
public bool ShowDialog(Window? owner)
|
||
{ return ShowDialog(owner == null ? IntPtr.Zero : new WindowInteropHelper(owner).Handle); }
|
||
|
||
/// <summary>
|
||
/// 显示对话框,指定Win32窗口作为所有者。
|
||
/// </summary>
|
||
/// <param name="owner">Win32窗口。</param>
|
||
/// <returns>用户是否选择了文件夹。</returns>
|
||
public bool ShowDialog(IWin32Window? owner) { return ShowDialog(owner?.Handle ?? IntPtr.Zero); }
|
||
|
||
/// <summary>
|
||
/// 显示对话框,指定窗口句柄作为所有者。
|
||
/// </summary>
|
||
/// <param name="owner">窗口句柄。</param>
|
||
/// <returns>用户是否选择了文件夹。</returns>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否允许选择非存储位置(如库、虚拟文件夹)。
|
||
/// </summary>
|
||
public bool AllowNonStoragePlaces { get; set; }
|
||
|
||
/// <summary>
|
||
/// 是否允许多选文件夹。
|
||
/// </summary>
|
||
public bool Multiselect { get; set; }
|
||
|
||
/// <summary>
|
||
/// 选中的元素名称(单选时)。
|
||
/// </summary>
|
||
public string? SelectedElementName { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 选中的所有元素名称(多选时)。
|
||
/// </summary>
|
||
public string[]? SelectedElementNames { get; private set; }
|
||
|
||
/// <summary>
|
||
/// 选中的文件夹路径(单选时)。
|
||
/// </summary>
|
||
public string? SelectedPath { get; set; }
|
||
|
||
/// <summary>
|
||
/// 选中的所有文件夹路径(多选时)。
|
||
/// </summary>
|
||
public string?[]? SelectedPaths { get; private set; }
|
||
|
||
// 属性声明
|
||
/// <summary>
|
||
/// 获取或设置对话框的标题。
|
||
/// </summary>
|
||
public string? Title { get; set; }
|
||
|
||
/// <summary>
|
||
/// 是否显示新建文件夹按钮。
|
||
/// </summary>
|
||
public bool ShowNewFolderButton { get; set; } = true;
|
||
|
||
// 以下为内部COM接口和枚举定义,通常无需注释
|
||
[ComImport]
|
||
[Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
|
||
private class FileOpenDialog
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提供了与Windows Shell交互所需的一些本地方法。这些方法主要用于实现文件夹浏览器对话框的功能,如获取当前活动窗口句柄、从给定路径创建项目标识符列表以及根据PIDL创建IShellItem接口实例。
|
||
/// </summary>
|
||
private class NativeMethods
|
||
{
|
||
/// <summary>
|
||
/// 获取当前活动窗口的句柄。
|
||
/// </summary>
|
||
/// <returns>当前活动窗口的句柄。</returns>
|
||
[DllImport("user32.dll")]
|
||
public static extern IntPtr GetActiveWindow();
|
||
|
||
/// <summary>
|
||
/// 从给定的PIDL创建一个IShellItem接口实例。
|
||
/// </summary>
|
||
/// <param name="pidlParent">父项的PIDL。如果为IntPtr.Zero,则表示桌面。</param>
|
||
/// <param name="psfParent">父项的IShellFolder接口指针。如果为IntPtr.Zero,则使用默认的桌面文件夹。</param>
|
||
/// <param name="pidl">要创建IShellItem接口实例的PIDL。</param>
|
||
/// <param name="ppsi">输出参数,返回创建的IShellItem接口实例。</param>
|
||
/// <returns>如果函数成功,则返回0;否则返回错误代码。</returns>
|
||
[DllImport("shell32.dll")]
|
||
public static extern int SHCreateShellItem(
|
||
IntPtr pidlParent,
|
||
IntPtr psfParent,
|
||
IntPtr pidl,
|
||
out IShellItem ppsi);
|
||
|
||
/// <summary>
|
||
/// 从给定的路径创建一个项目标识符列表。
|
||
/// </summary>
|
||
/// <param name="pszPath">要转换为项目标识符列表的路径字符串。</param>
|
||
/// <param name="ppIdl">输出参数,用于接收指向项目标识符列表的指针。</param>
|
||
/// <param name="rgflnOut">输出参数,返回关于解析路径的信息。</param>
|
||
/// <returns>如果函数成功,则返回0;否则返回错误代码。</returns>
|
||
[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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 代表一个Shell项,提供了获取项的名称、属性、父项以及与其他项比较等功能。
|
||
/// </summary>
|
||
[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
|
||
}
|
||
} |