文章来源: http://www.codeproject.com/KB/dialog/OpenFileDialogEx.aspx

Customizing OpenFileDialog in .NET By CastorTiu

Sample Image - OpenFileDialogEx.png

简   介

几天前,我开始用我的 IconLib library 来编写一个图标编辑器程序。

创建了主窗口,我想:“从哪开始呢?”随后我决定建个带有“打开”功能的菜单,并且在打开图标前要有预览功能。

如果你在阅读本文,大概是因为你知道.NET有一个OpenFileDialog类,但它不可定制。我写的这个控件,目的是允许你为.NET的OpenFileDialog类添加些功能。你无法自定义OpenFileDialog类主要是因为这个类被声明为sealed,这意味着你不能继承它。如果转到基类FileDialog,此类允许你从它继承,但它有一个internal abstract方法"RunFileDialog"。因为是“内部”“抽象”的,故此只允许从同一程序集内部继承。

多少次,你需要在OpenFileDialog控件上放置额外控件却无可奈何……

搜索.NET的代码,我找到一些用MFC的,却没有用.NET的。打开文件对话框根本就不是在.NET中实现的,而是利用一个Win32 API函数 "GetOpenFileName"

 

就此,我有三个选择:

  1. 从头开始重写自己的OpenFileDialog。
  2. 重新利用资源创建自己的OpenFileDialog (利用API “GetOpenFileName” 并建立自己的模版)。
  3. 破解(hack) .NET的OpenFileDialog ,在上面增加我所需要的功能。

 

选择(a)不适合我,因为这需要大量开发时间而我有大量工作要完成,写完以后可能还需要重新检查。后面一个选择需要建立模版使用Win32 API调用和资源。选择(c)是当前更有前途的选择,不要认为hack就是不好的,基本上hack就是当你想要让控件实现一些扩展功能时,你却必须从其他线程或进程来进行。

因为我喜欢挑战,所以我决定"hack" OpenFileDialog类来创建我的自定义控件。

它能帮你做什么?

我本就能破解控件使它做我想要的,而且也这么干过,但我在.NET 1.0中遇到这个问题很多次了,迄今为止没人对此给出个解决办法。因此我决定为这个控件编写一个接口,这样它可以用在不同的程序中。

还要创建某个东东,它不需要对当前项目改变或增加代码,并且无需知道它如何工作就能够增加多个控件。它应该是个单独的控件以便被添加,就像IDE中其他控件一样。

创建的这个控件,我叫它"OpenFileDialogEx"

我怎样用它呢?

想象一下若"OpenFileDialogEx"为抽象类——我不把这个类设为abstract只是因为VS IDE不能创建抽象类的实例,它拒绝绘制屏幕。

你可以原封不动地使用OpenFileDialogEx类,但这毫无意义,因为它不包含扩展功能,就是一个空的UserControl

所以必须继承OpenFileDialogEx以创建你自定义版的“打开文件对话框”。

在继承OpenFileDialogEx后,你已经建了一个自定义控件,在上面你可以加入任何控件,可以是按钮(Button)、面板(Panel)或者分组框(GroupBox)。基本上它就是个控件容器,随后这个容器就会被“挂”到.Net的OpenFileDialog对象上。

这个控件有与任何UserControl不同的额外3个属性、3个方法和2个事件。

属性:

DefaultViewMode:

这个属性让你选择OpenFileDialogEx的初始查看模式,默认使用“详细信息”(译者注:在我系统中默认为“列表”,应与系统设置有关)。这里你可以指定不同的查看模式,例如“图标”、“列表”、“缩略图”、“详细信息”等。

StartLocation:

这个属性指示控件应该被挂靠在标准OpenFileDialog的右面、下面还是里面。通常,它被用于水平扩展OpenFileDialog。此外,若你需要往OpenFileDialog里添加额外控件,那可以设为"None",OpenFileDialogEx中的控件会与原来的OpenFileDialog 共享同一个客户区(Client Area)。

OpenDialog:

这个属性表示控件中内嵌的OpenFileDialog。你可以在此设置打开文件对话框的标准属性,如InitialDirectoryAddExtensionFilters等等。

OpenFileDialog默认是可改变尺寸的,OpenFileDialogEx控件也是如此,可自动伸缩。当用户扩大或缩小窗口时,它根据StartLocation属性产生不同变化。

 StartLocation

  • Right:用户控件在垂直方向伸缩
  • Bottom:用户控件在水平方向伸缩
  • None:用户控件在两个方向自由伸缩

基本来说,当你添加诸如Button、Panel、Group Box之类的控件时,需要设置每个控件的Anchor属性,然后你就可以控制当用户改变OpenFileDialog窗口尺寸时各个控件的位置了。

举例来说,要获得图片预览,你可以设置StartLocationRight,在继承OpenFileDialogEx的控件中加入一个PictureBox,设PictureBoxAnchor属性为Left, Top, Right, Bottom。这样当用户改变尺寸时PictureBox也会随之动态伸缩。

方法:

这几个方法都是虚方法,你要重载它们来与原OpenFileDialog交互。

OnFileNameChanged()

当用户点击文件时被调用。

OnFolderNameChanged()

当用户改变文件夹时被调用。

OnClosing()

OpenFileDialog准备关闭时被调用,在释放被分配的资源时很有用。

事件:

两个事件为FileNameChangedFolderNameChanged,由相应的虚方法"OnFileNameChanged()" 和 "OnFolderNameChaned()"来引发。建议不要直接使用事件,而应重写那些方法,因为事件数据在这层就被处理掉,没有传递给更多层。

它是如何编写的?

第一个问题是OpenFileDialog为有模式对话框,这就是说你基本上无法获得窗口句柄,因为当你调用ShowDialog()方法时,只要OpenFileDialog是打开的,你就没有程序流程的控制权。

想要获得OpenFileDialog的句柄,一种方法是重载窗体的WndProc方法并监视消息。当OpenFileDialog创建后,主窗体会收到一些消息,如WM_IDLE, WM_ACTIVATE, WM_NC_ACTIVATE等等。这些消息会设置参数lParam,此参数包含OpenFileDialog的窗口句柄。

如你所见,这需要重载WndProc方法。一些开发者甚至不知道此方法的存在,我可不想重蹈覆辙,而且我还会去注意一些MDI窗口打开OpenFileDialog时发生的问题。

接下来我主要做的是,当ShowDialog()方法被调用时,在屏幕上创建一个“影子”窗体(Dummy Form)并隐藏,此窗体会接管打开OpenFileDialog并获取其窗口句柄。

起初它监听的是WM_IDLE消息,但问题是当消息发送时已经太迟,窗口已经建立并显示在屏幕上,而你还是可以改变其内容,但用户会看到屏幕上原OpenFileDialog和自定义控件之间一个细微的闪烁。

于是,我们可以采用消息WM_ACTIVATE,它发生在OpenFileDialog显示在屏幕上之前。

至此,它获得所需的句柄,将要被显示出来,然后呢?

它如何改变OpenFileDialog窗口的属性呢?

这里涉及到.NET的NativeWindow类,此类是处理由相关句柄所发送消息的窗口过程的封装。那么可以创建一个NativeWindow,使其与OpenFileDialog的句柄相连。这样每个被发送给OpenFileDialog的消息都会被重定向给我们NativeWindow中的WndProc方法,我们可以对其屏蔽、修改或者原封不动。

WndProc里,我们处理消息WM_WINDOWPOSCHANGING。如果对话框正在打开,那就根据用户设定的StartLocation属性改变原始的水平或垂直尺寸,即增加将要创建窗口的尺寸。这只在控件打开时发生一次。

还要处理消息WM_SHOWWINDOW。这里,所有原来OpenFileDialog里的控件都已创建,我们要往上面“附加”自己的控件。这由一个Win32 API "SetParent" 来完成,此API可让你改变父窗口。下面主要做的是,根据StartLocation属性值“附加”我们的控件到原OpenFileDialog上所设定的位置。

这样做的好处是我们依然拥有对附加在OpenFileDialog上各控件的完整控制。这意味着我们可以接受事件,调用方法,对这些控件做任何我们所需要的。

同时在初始化时,我们还得到了原OpenFileDialog上每个控件的句柄,这样就允许再次建立.NET NativeWindow对象来处理每个控件的消息。

准备就绪,那当用户在ListView中点击时该如何监视消息呢?

原本,我试着通过建立一个NativeWindow来处理ListView本身所发出的消息,但问题是每次用户改变目录或者在不同的视图中点击,句柄都被销毁,必须重建句柄。

用MS Spy分析中OpenFileDialog所有的窗口,我们注意到OpenFileDialog里还有另一个FileDialog窗口,很可能就是OpenFileDialog的基类窗口。检查MSDN文档,我们看到每个OpenFileDialog发出的动作都会引发一个包含OFNOTIFY结构的WM_NOTIFY消息,此结构包含那个动作的代码。其中两个动作是CDN_SELCHANGECDN_FOLDERCHANGE

 

当用户与文件夹组合框或列表框交互时它们被调用。接下来,我首先获取基类对象FileWindow的句柄,并由这个句柄创建一个NativeWindow对象。这样就可以处理消息WM_NOTIFY来分析OFNOTIFY结构以及处理CDN_SELCHANGECDN_FOLDERCHANGE。当这个窗口处理消息时,消息被转送给OpenFileDialogEx控件的方法OnFileNameChangedOnFolderNameChanged

另一种方法是当OpenFileDialog窗口关闭时进行拦截。开始时我利用消息WM_CLOSE,而它也工作了,但后来我发现当用户在列表框里双击文件时这个消息没有被调用。查看OpenFileDialog产生的消息,我看到可以用WM_IME_NOTIFY消息,当OpenFileDialog关闭时这个消息会带着一个值为IMN_CLOSESTATUSWINDOWwParam参数被发送,这样就可以把调用传递给方法OnClosingDialog()

那么,当用户改变OpenFileDialog尺寸时如何伸缩UserControl呢?这通过处理消息WM_WINDOWPOSCHANGING来完成,在此我们设定控件随着OpenFileDialog的尺寸来改变大小。

作为一个重要细节,当OpenFileDialog关闭时,必须恢复我们打开时的原始尺寸。这是因为OpenFileDialog可能会记录上一次的位置和尺寸,如果没有这么做,每次打开OpenFileDialog,它都会增加尺寸,使得它变得越来越大。

结束语

我在Windows XP上测试运行正常,我还没机会在Windows 2000/2003或Vista等不同操作系统上试验,但应该没问题。我想在Windows 95/98上运行不了,因为我设置struct的size只是适合WinNT系统。如果你有任何建议或者发现了bug,请通知我,我会更新控件的。