自定义OpenFileDialog

来源:互联网 发布:淘宝权女朋友静雯 编辑:程序博客网 时间:2024/05/17 06:34

自定义“打开文件”对话框


本页内容

OpenFileDialog 
位置栏的系统设置 
RegOverridePredefKey 
重写位置栏项 
自定义的位置栏 
小结 

在 Microsoft .NET Framework 中,用 Windows 窗体确实可以很容易地显示“打开文件”对话框,但是结果窗口不如您通过 Win32 API 创建它时易于自定义。在 Windows 2000 中,Microsoft 添加了一个不错的功能 — 位置栏,它是一个出现在窗口左侧的垂直工具栏,它允许您选择经常访问的文件夹。如图 1 所示,位置栏包含可以使用户直接访问五个文件夹(历史、桌面、我的文档、我的电脑和网上邻居)的按钮。

Aa663661.mis03cuttingedgefig01(zh-cn,MSDN.10).gif

图 1 在位置栏中选择文件夹

在针对 Win32 API 中的“打开文件”通用对话框进行编码时,您可以设置用来隐藏位置栏的样式。但是,与 Win32 通用对话框的其他功能相同的是,在移植到 .NET Framework 时,该设置似乎已经丢失。创建通用对话框从来没有像在 Framework 中那样容易,但是这是以降低灵活性为代价的。另外,在托管代码中,无法在具有其他控件的情况下扩展该对话框的布局。

在本专栏中,我将着重介绍“打开文件”通用对话框的位置栏,并讨论如何自定义所显示文件夹的列表,以及如何使其特定于应用程序,甚至特定于调用。在讨论过程中,我将回顾 .NET Framework 中用来处理注册表项的一些注册表配置单元和节点以及 API。最后,我将显示一个类,该类用代表在工具栏中显示的位置的集合来扩展 OpenFileDialog 系统类。

OpenFileDialog


OpenFileDialog 类表示系统提供的通用对话框,该对话框允许用户选择和打开文件。该类从 CommonDialog 继承,但是其他类不可以从它继承。要显示对话框并选取文件,请使用以下非常简单的 C# 代码:

OpenFileDialog.InitialDirectory = @"c:\";OpenFileDialog.Filter = "Bitmap|*.bmp|All|*.*";OpenFileDialog.ShowDialog();

请注意 Filter 属性字符串需要的特殊格式。Filter 属性标识文件类型框中的项。属性的内容必须是字符串,而且该字符串必须由通过管道分隔的字符串对组成。在每个管道分隔的对中,第一个标记表示要选择的类型的描述性名称,而第二个标记代表文件的掩码。例如,如果您希望筛选文件夹视图以显示所有位图或所有文件,请使用以下字符串:

Bitmap|*.bmp|All|*.*

在 .NET Framework 中,必须将管道用作分隔符。而在 Win32 中,则使用 null(在 ASCII 中,使用 0)。但是,在 Win32 中,通用的编程做法是在字符串中使用管道,然后以编程方式将它们替换为 null。一个好消息就是,该服务现在自动由 .NET Framework 提供。

OpenFileDialog 类公开一组用来配置对话框的属性。例如,您可以选择初始目录、初始筛选器索引、窗口标题、是否可以选择多个文件,以及在关闭之前是否应当还原应用程序的当前目录。当用户单击“打开”按钮时,该类还引发事件(名为 FileOk)。正如我以前所提到的,OpenFileDialog 是密封类,因此您不能从它派生其他类。但是,如果您希望自定义文件对话框的行为,您应当尽可能地创建从抽象类 FileDialog 派生的崭新的类。在本例中,您可以访问几个功能强大但受保护的方法(如 HookProc 和 RunDialog)。HookProc 定义对话框挂钩进程,该进程向通用对话框添加特定功能。RunDialog 具有如下签名:

protected abstract bool RunDialog(   IntPtr hwndOwner);

该方法接收拥有对话框的窗口的 HWND 句柄。此方法的典型实现只是将 hwndOwner参数存储在 CommonDialog 的受保护成员 hwndOwner 中。RunDialog 方法非常重要,这是由于它允许您设置持久性对话框,并从不同的文件夹选择多个文件。要将其他模式对话框转换为无模式对话框,请替换所有者窗口,并使该对话框成为桌面窗口的子窗口。桌面窗口的句柄由 Win32 API 函数 GetDesktopWindow 返回。顺便提一句,使用一些 API 函数自定义“打开文件”对话框是绝对有必要的。

返回页首 

位置栏的系统设置


在 Win32 或 .NET Framework 中,位置栏的内容不能完全由应用程序配置。然而,在过去的几年中,一些知识库文章已经提到过,您只需在特定的注册表项中编写一些条目即可修改位置列表。换句话说,通常由对话框显示的位置列表只是默认列表。内部的 Win32 代码尝试从注册表中读取用户的位置列表。如果它失败(即,如果未指定列表),则使用默认列表。因此,您可以使用哪个注册表项?它位于 HKEY_CURRENT_USER 下,因此特定于登录用户。下面是它的路径:

Software  \Microsoft    \Windows      \CurrentVersion        \Policies          \ComDlg32            \PlacesBar

该注册表项在默认情况下不存在,必须显式创建。尝试使用注册表编辑器或 regedt32 创建注册表项。请记住,必须以管理员权限登录才能修改注册表。在创建注册表项之后,尝试启动任何使用 Windows“打开文件”通用对话框的 Win32 或托管应用程序。例如,尝试用 Microsoft 画图打开;结果显示在图 2 中。怎么啦?

Aa663661.mis03cuttingedgefig02(zh-cn,MSDN.10).gif

图 2 空位置栏

当 Win32 内部代码检测到刚创建的注册表子树时,它会丢弃默认列表。由于您尚未列出任何位置,所以位置栏是空的。(请注意,只要更改了这些设置的用户仍处于登录状态,此更改就会影响在系统上运行的所有应用程序。)

mis03cuttingedgefig03

图 3 自定义喜欢的位置

如何指定自己喜欢的位置?应当在上面提到的注册表项中创建条目。不能创建五个以上的条目,这是由于多出的条目将被忽略。每个条目的类型都应当是 REG_SZ(字符串值),且必须被命名为“PlaceX”,其中 X 是从零开始的索引。可通过完全限定路径来指定位置。或者,如果您希望将目标定为特殊文件夹(如我的文档或我的电脑),则使用文件夹 ID。在本例中,创建的是 REG_DWORD 条目并输入十六进制的文件夹号。图 3 中的两个位置条目可在图 4 中看到实际效果。

Aa663661.mis03cuttingedgefig04(zh-cn,MSDN.10).gif

图 4 两个位置

如果指定了五个以下的条目,则其余条目保留为空。没有比五个位置更适合地址栏的了。有一点很重要,那就是需要将条目命名为 Place0、Place1 等。为了恢复为默认值,只需删除已添加的两个注册表项 — 即 ComDlg32\PlacesBar。在删除注册表项时一定要格外小心。您应当确保只删除这两个注册表项,否则可能会损坏系统!

下面的 C# 代码显示了如何枚举当前用户的注册位置:

RegistryKey placesBarRoot;placesBarRoot = Registry.CurrentUser.OpenSubKey(key);string[] valuesOfKey = placesBarRoot.GetValueNames();for(int i=0; i<valuesOfKey.Length; i++) {   Console.WriteLine(placesBarRoot.GetValue(valuesOfKey[i]);}placesBarRoot.Close();

首先在当前用户的配置单元上打开位置栏项。RegistryKey 类的 GetValueNames 方法用找到的所有条目的名称填充一个字符串数组。在我给出的示例中,valuesOfKey 数组将包含类似于 Place0、Place1 等的字符串。每个注册表项的值都是通过 GetValue 方法读取的。

这样,您无疑可以自定义出现在“打开文件”通用对话框中的位置,但是不要忘记这些更改会扩展到所有正在运行的应用程序。注册表设置基于每用户工作,Microsoft 不提供 API 以编程方式基于每应用程序或基于每调用来配置位置栏。是否可以在每次显示文件对话框时欺骗系统并自定义位置栏?答案是肯定的,但是实现并不是一件小事,它需要使用鲜为人知的 Win32 API 函数 — RegOverridePredefKey。


返回页首 

RegOverridePredefKey

RegOverridePredefKey 函数需要 Windows 2000 或更高版本;它不受运行 Windows 9x 系统的支持。该函数主要面向软件安装程序,它允许将一个预定义的注册表项映射到另一个注册表项。该函数的最终目标是允许安装程序检查可安装组件尝试对注册表进行的更改。实际上,在调用某些组件之前,安装程序将在别处映射关键的注册表子树,并使组件继续。

当组件完成之后,安装程序检查所做的更改,并确定它们是否安全。如果安全的话,安装程序会将这些更改映射到由 DLL 指向的初始位置。否则,在写入到目标位置之前,这些更改要么被拒绝,要么被修改。该函数的原型如下所示:

LONG RegOverridePredefKey(  HKEY hKey,       HKEY hNewHKey   );

第一个参数是要重新映射的注册表项,而第二个参数是重定向注册表项所在的位置。重定向的注册表项要么应当是预定义的项,要么应当是当前打开的注册表项。在 RegOverridePredefKey 返回之后,对 hKey 的任何调用都在 hNewHKey 项中的子树下解析。例如,请考虑以下调用:

   HKEY hkMyCU;   RegCreateKey(HKEY_CURRENT_USER, "Dino",        &hkMyCU);   RegOverridePredefKey(HKEY_CURRENT_USER,        hkMyCU);

实际上,在调用 hKey 之后,用来从 Software\Microsoft 读取的调用实际上将从 Dino\Software\Microsof 读取。这两项的配置单元必须相同。自动重定向仅适用于调用 RegOverridePredefKey 的过程。如果 hNewHKey 参数是 null,此函数将恢复该项的默认映射。重定向特定于进程,正是这一事实为您提供用来实现特定于应用程序的位置栏的工具。

返回页首 

重写位置栏项


创建或更新注册表条目是自定义“打开文件”对话框中位置列表的唯一方法。但是,要使所做的更改特定于应用程序,您需要修改每个调用的唯一系统项的内容。如果您真的在应用程序需要显示对话框时读取注册表并写入到其中,就会产生太多的通信量,而且,更重要的是,并发运行的应用程序将相互重写设置。通过使用 RegOverridePredefKey 项,实际上将创建指定注册表树的特定于进程的副本,并且可以在不损坏注册表的情况下更新它。由于映射只位于进程的上下文中,因此不会影响正在运行的其他应用程序。.NET Framework 不直接支持 RegOverridePredefKey 函数,这意味着您需要通过 P/Invoke 互操作性平台显式导入该函数。

在 .NET Framework 中,并非所有的 Win32 注册表函数都有与之匹配的托管类或静态方法。作为 Win32 中注册表核心的数据结构(即 HKEY 句柄)已在 .NET Framework 中完全隐藏,并且已被名为 RegistryKey 的类替代。RegistryKey 类确实在内部使用 HKEY 句柄,但是您无法从在 .NET Framework 上构建的应用程序内部读取该信息。实际上,hkey 数据成员被标记为私有成员。这对您会有何影响?(谁知道 Redmontonian 是否将在下一版本的 .NET Framework 中公开 HKEY 成员?)

不能通过导入和修改 RegOverridePredefKey 函数来使用 RegistryKey 类的实例,从而管理注册表项。您应当按如下方式导入该函数:

[DllImport("advapi32.dll")]private static extern long RegOverridePredefKey(    IntPtr hkey,    IntPtr hnewKey);

问题在于您不能调用我刚显示的包装方法,并向它传递使用 .NET Framework 类打开的注册表项。此时,我发现两种可能的方法:要么使用 Win32 函数(如 RegOpenKeyEx 和 RegOpenKey)打开要映射的注册表项及其重定向器,要么将所有的初始化代码放在新的 Win32 DLL 中,并从应用程序内部调用它的函数。在本专栏中,我将选择第二种方法。图 5 显示了用来完成该任务的简单 DLL 的 Win32 代码。(我必须承认,对我来说,在经过两年多时间只针对 .NET Framework 进行编码之后,返回到 Win32 DLL 有点困难。)

该库定义和导出两个名为 InitializeRegistry 和 ResetRegistry 的函数。InitializeRegistry 是无参数的函数,但是返回 Win32 HKEY 对象。ResetRegistry 不返回任何值,但是采用通过上一方法打开的同一个 HKEY。InitializeRegistry 函数打开 HKEY_CURRENT_USER 配置单元,并将其映射到新创建的名为 Dino 的节点。(当然,您可以将它随意重命名为对您来说更有意义的名称。)该函数创建注册表项(如果它尚且不存在的话),然后打开它。在调用 InitializeRegistry 之后,如果尝试在 HKCU 下读取和写入,都将导致在重新映射的注册表项下读取和写入,这只针对当前的进程发生。ResetRegistry 恢复事情的自然顺序,删除映射并关闭重定向器注册表项。当然,为了避免代码的其他部分出现严重问题,应当在非常短的时间间隔内调用 InitializeRegistry/ResetRegistry。在加载窗体时,永远不要调用 InitializeRegistry;在卸载时,永远不要调用 ResetRegistry。

在编译 Win32 DLL 之后,将文件放在与托管应用程序可执行文件相同的文件夹中。在编译 DLL I 时,由于习惯了 .NET Framework 编程的直接性,我总是忘记向 Visual Studio 6.0 项目中添加导出文件。我已经假设:如果声明 APIENTRY,这些函数已经够了。因此,在 .NET Framework 上构建的应用程序报告过几次错误,在 Win32 DLL 中找不到这样的函数。使用 .NET Framework 中的公共关键字是如此简单和方便!下面的代码片断说明了如何在托管应用程序中导入这两个函数。

[DllImport("myregutil.dll")]private static extern IntPtr InitializeRegistry();[DllImport("myregutil.dll")]private static extern void ResetRegistry(IntPtr hKey);
返回页首 

自定义的位置栏


现在,让我们看一看如何利用这个小型的 Win32 库,为针对“打开文件”通用对话框进行的每个调用创建自定义位置栏。在这里,我的目标是使用特殊类来设置位置栏并显示对话框。但是,如上所述,不能通过从 OpenFileDialog 类继承来创建新的对话框类。当不可能继承时,聚合始终是要考虑的替代选项。让我们创建一个 OpenDialogPlaces 类,以嵌入 OpenFileDialog 类的实例。下面的代码片断显示了这个特定类将如何工作:

   OpenDialogPlaces o = new OpenDialogPlaces();   o.Places.Add(@"c:\\");   o.Places.Add(@17);   o.Init();   o.OpenDialog.ShowDialog();   o.Reset();

该类公开了一个名为 Places 的可根据需要填充的数组。在完成后,调用名为 Init 的特定于类的方法并更新注册表。接着,显示对话框并调用 Reset,以便恢复注册表的初始状态。请注意,这不是实现该功能唯一可能的方法;它只为您提供了如何实现它的思路。

OpenDialogPlaces 类从 Object 继承,并在实例化时创建 OpenFileDialog 对象:

   public OpenDialogPlaces()      {          m_places = new ArrayList();          m_OpenFileDialog = new OpenFileDialog();          }

其他私有成员包括用来重写注册表项的句柄,这些成员另存为 IntPtr 对象。公共成员是指表示嵌入的通用对话框实例和 Places 集合的 OpenDialog 对象。

Init 方法设置要发出调用的注册表。在 Reset 被调用之前,该映射一直持续,如下所示:

m_overriddenKey = InitializeRegistry();RegistryKey reg;reg = Registry.CurrentUser.CreateSubKey(Key_PlacesBar);for(int i=0; i<Places.Count; i++){   if(Places[i] != null)      reg.SetValue("Place" + i.ToString(), Places[i]);  }

对 InitializeRegistry 的调用为如下操作奠定了基础:为要使用的进程创建虚设注册表树。上述代码在重定向器项下创建树,并定义已由用户添加到 Places 集合中的位置条目。

针对 Registry 对象使用 CreateSubKey 方法,可以创建子树。请注意,Win32 库重新映射 CurrentUser 项,因此,您必须在它的正下方创建子树。SetValue 方法允许您创建注册表条目,该方法是 RegSetValueEx Win32 API 函数的托管包装。在默认情况下,它创建 REG_SZ 条目。该方法的签名如下所示:

public void SetValue(string name,    object value) 

值参数的实际类型确定所创建的注册表项的类型。特别是,如果该参数的类型是 Int32,就会创建 REG_DWORD 条目。以上代码在内部执行以下代码片断中显示的类型检查:

if (value as Int32 != null){...}

如果文件夹的名称是绝对路径或相对路径,您将需要 REG_SZ 条目。如果您希望指向特殊文件夹,您需要使用文件夹特定的数字(有关数字列表,请参阅图 6)。在本例中,您需要 REG_DWORD 条目。

返回页首 

小结


如果您用以下代码中显示的值填充 Places 数组,然后显示文件对话框,将看到一个类似于图 7 所示的窗口:

Aa663661.mis03cuttingedgefig07(zh-cn,MSDN.10).gif

图 7 自定义的位置栏

   o.Places.Add(@"C:\");   o.Places.Add(17);   o.Places.Add(5);   o.Places.Add(@"C:\My Articles");   o.Places.Add(6);

如果您用自定义图标或注释指示文件夹,这将由位置栏中的项目反映出来。如图 8 所示,两个同时运行的应用程序可以拥有不同的位置栏。

Aa663661.mis03cuttingedgefig08(zh-cn,MSDN.10).gif

图 8 不同的位置栏

此处讨论的诀窍并不特定于 .NET Framework。它与 Windows 2000 操作系统的某些特征有关,因此,在 Win32 中同样可以很好地工作。


转载自:https://msdn.microsoft.com/zh-cn/library/aa663661.aspx

0 0