扩展 FolderBrowserDialog 控件
前段时间写了一个 winform 的工具,根据需要,扩展了 FolderBrowserDialog 这个控件。
.Net 中 FolderBrowserDialog 控件封装了 SHBrowseForFolder 函数,提供了选择本机文件系统中的文件夹的功能。
它允许我们指定某一个目录作为选择目录树的根目录,但是只能指定有限的几个系统内置的目录;它也提供了不能选择虚拟文件夹的功能,但是并不允许我们自定义哪些文件夹能够被选择。
所以根据需要,对 FolderBrowserDialog 在下面 2 个方面做了功能扩展:
(1)允许指定任意有效的文件夹作为根目录;
(2)允许自定义指定文件夹是否能够选择的逻辑;
1、生成 FolderBrowserDialog 扩展类
.Net 中的 FolderBrowserDialog 类是密封的,不能子类化,而且通过 Reflector 查看 FolderBrowserDialog 的实现,基本上没有办法使用对象适配器。
权宜之下,只好选择了一个最丑陋的方式:先从 CommonDialog 类直接继承一个 FolderBrowserDialogEx 类,然后参考 FolderBrowserDialog 的逻辑使 FolderBrowserDialogEx 类实现 FolderBrowserDialog 类的功能。
2、允许指定任意有效的文件夹作为根目录
FolderBrowserDialog 已经提供了指定目录作为根目录的功能,只不过受限制很大,就是只能指定 Environment.SpecialFolder 枚举中定义的那些系统能够明确知道其路径的目录。原来还以为这个枚举里面有一个 Custome 之类的以允许我们指定其他的目录,没想到找了很长时间都没有找到。看来微软目前还是不想让我们指定其他目录的。
那么 FolderBrowserDialog 中是如何实现指定根目录的呢?
通过分析发现,FolderBrowserDialog 能够提供选择目录的功能的根本在于它的 RunDialog 方法内部调用了 Win32 的 SHBrowseForFolder 函数,这个函数接受 BROWSEINFO 结构的指针作为参数,而这个结构中就有一个成员变量 pidlRoot 存储了根目录的信息。
typedef struct _browseinfo { HWND hwndOwner; PCIDLIST_ABSOLUTE pidlRoot; LPTSTR pszDisplayName; LPCTSTR lpszTitle; UINT ulFlags; BFFCALLBACK lpfn; LPARAM lParam; int iImage; } BROWSEINFO
win32 中的 BROWSEINFO 中的 pidlRoot 是一个指针,对应到 .Net 中就应该是一个 IntPtr 类型的对象。如何获取到任意目录对应的这种指针呢?
先来看看 .Net 中如何根据 Environment.SpecialFolder 获取到这个指针的。
在 Reflector 反射出的代码中,发现了这么一行代码
UnsafeNativeMethods.Shell32.SHGetSpecialFolderLocation(hWndOwner, (int) this.rootFolder, ref root);
其实 windows 系统中的特殊文件夹早就有了,所以 win32 已经有了函数来提供根据特殊文件夹对应的 int32 值获取其对应的 PIDL(pointer to item identifier list,设计到 Windows Shell,个人了解不多,此处不作深入描述)。但是其他的文件夹在系统中又不好找到对应的 int32 值,看来这个函数不能为我们所用。
在查找了与 Windows Shell 相关的资料后,发现了一个函数
HRESULT SHParseDisplayName(
LPCWSTR pszName, IBindCtx *pbc, PIDLIST_ABSOLUTE *ppidl, SFGAOF sfgaoIn, SFGAOF *psfgaoOut );
这个函数刚好能够满足我们的要求,我们能够通过这个函数获取到指定的目录对应的 PIDL。
ok,下面就来开始扩展过程。
首先导入外部函数:
[DllImport("shell32.dll", CharSet=CharSet.Unicode)] private static extern int SHParseDisplayName(string pszName, IntPtr pbc, ref IntPtr ppidl, ulong sfGaoIn, IntPtr psfGaoOut);
再修改 RootFolder 属性以接收指定的目录作为根目录:
public string RootFolder { get { return _rootFolder; } set { if (null != value && 0 < value.Length && Directory.Exists(value)) { new FileIOPermission(FileIOPermissionAccess.PathDiscovery, value).Demand(); } _rootFolder = (null == value ? string.Empty : value); } }
然后修改 RunDialog 方法的实现,获取 RootFolder 的 PIDL:
IntPtr pidlRoot = IntPtr.Zero; bool success = false; SHGetSpecialFolderLocation(hwndOwner, (int)System.Environment.SpecialFolder.Desktop, ref pidlRoot); if (pidlRoot == IntPtr.Zero) { throw new Exception("No root folder"); } if (null != _rootFolder && 0 < _rootFolder.Length) { IntPtr pidlTmp = IntPtr.Zero; IntPtr psfGaoOut = IntPtr.Zero; if (S_OK == SHParseDisplayName(_rootFolder, IntPtr.Zero, ref pidlTmp, 0, psfGaoOut)) { pidlRoot = pidlTmp; } }
其中 S_OK 的定义为:
private static readonly int S_OK = 0;
好了,经过前面修改之后,把期望的根目录路径设置给 FolderBrowserDialogEx 的 RootFolder 属性,然后就得到我们期望的目录树了。
下图显示了指定 C:\windows\Microsoft.Net\Framework 为根目录的文件夹选择对话框界面效果:
下一篇继续扩展这个对话框,支持自定义哪些文件夹能够被选择。