定制 .NET 中的 OpenFileDialog
一个扩展控件,用于为 .NET 中的标准 OpenFileDialog 添加额外功能。
引言
几天前,我想开始创建一个图标编辑器应用程序,以利用我的 IconLib 库。
我创建了我的主窗体,然后我想:“我需要从哪里开始?”。然后,我决定创建一个带有“打开”功能的菜单。我认为“打开”功能应该有一个预览屏幕,以便在打开图标之前就能看到它。
如果你正在阅读此页面,很可能是因为你知道 .NET 有一个 OpenFileDialog
类,但它无法自定义。此控件的目标是允许你为 OpenFileDialog
.NET 类添加一些功能。你无法自定义 .NET 中的 OpenFileDialog
的主要原因是该类被声明为 sealed
,这意味着你无法继承它。如果你查看基类 FileDialog
,它允许你继承它,但它有一个 internal abstract
方法“RunFileDialog
”。由于它是 internal
和 abstract
,因此它只允许在同一程序集中继承它。
你是否曾多少次想在 OpenFileDialog
控件中添加一些额外的控件但却做不到……
在 .NET 中搜索代码时,我发现有几个地方使用了 MFC,但没有 .NET 的。OpenFileDialog 在 .NET 中不是原生实现,而是利用了 Win32 API “GetOpenFileName
”。
至此,我有三个选择:
- 从头开始创建自己的 OpenFileDialog。
- 创建自己的 OpenFileDialog,重用资源(使用 API “GetOpenFileName”并提供自己的模板)。
- 破解 .NET 的
OpenFileDialog
并添加我需要的功能。
选项 (a) 对我来说不是一个选项,因为它可能需要大量开发时间,而我还有很多其他事情要做。稍后,当产品完成后,我可以回顾它。下一个选项要求我使用 Win32 API 和资源调用提供自己的模板。选项 (c) 是当时更可行的选择;不要认为这是一个糟糕的破解,基本上,破解就是当你想要让控件执行一些额外的功能,并且必须从另一个线程或进程进行时。
所以,因为我喜欢挑战,我决定“破解”OpenFileDialog
类来创建我自己可自定义的控件。
它能为你做什么?
我可以破解该控件来做我需要的事情,仅此而已,但我从 .NET 1.0 开始就多次遇到这个问题,迄今为止还没有人提出解决方案,所以我决定为该控件创建一个接口,以便它可以在不同的应用程序中使用。
此外,我想创建一个不需要更改或添加当前项目代码,并且能够添加多个控件而无需了解它们工作细节的东西;它需要成为一个独立的控件,可以像 IDE 中的任何其他控件一样添加。
我创建了这个控件,并称之为“OpenFileDialogEx
”。
我怎么做到的?
想象一下 OpenFileDialogEx
是一个抽象类:我没有将此类设为抽象的唯一原因是因为 VS IDE 无法创建抽象类的实例,从而避免了在屏幕上的渲染。
你可以使用 OpenFileDialogEx
类,但没有意义,因为它不包含任何额外功能,只是一个空的 UserControl
。
所以你必须继承 OpenFileDialogEx 来创建你自己定制版本的 Open File Dialog。
继承 OpenFileDialogEx
后,你就创建了一个自定义控件,你可以在其中添加任何控件,可以添加额外的按钮、面板或组框。基本上,它是一个控件容器;稍后,这个容器将在运行时“附加”到 .NET OpenFileDialog
对象上。
这个控件有三个额外的属性、三个方法和两个事件,与任何 UserControl
都不同。
DefaultViewMode
此属性允许你选择 OpenFileDialog
应该以哪种视图打开;默认情况下,它以“详细信息视图”打开。你可以在此处指定不同的默认视图,如图标、列表、缩略图、详细信息等。
StartLocation
此属性指示创建的控件是堆叠在经典 OpenFileDialog
的右侧、底部还是后面。通常,此属性用于水平扩展 OpenFileDialog
。如果相反,你需要向当前的 OpenFileDialog
添加额外的控件,那么你可以指定“None
”,OpenFileDialogEx
中的控件将与原始 OpenFileDialog
共享相同的客户端区域。
OpenDialog
此属性是控件中嵌入的 OpenFileDialog
。你可以在此处设置标准属性,如 InitialDir、AddExtension、Filters 等。
默认情况下,OpenFileDialog
是可调整大小的,OpenFileDialogEx
会自动帮助你实现这一点;用户控件“OpenFileDialogEx
”会自动调整大小。当用户展开或收缩窗口时,它的行为将根据 StartLocation
属性而有所不同。
StartLocation
Right
:用户控件将垂直调整大小。Bottom
:用户控件将水平调整大小。None
:用户控件将水平和垂直调整大小。
基本上,当你添加控件作为按钮、面板、组框等时,你必须设置每个控件的 Anchor
属性,然后你就可以控制当用户调整 OpenFileDialog
窗口大小时你的控件的位置。
例如,要实现图片预览,你可以将起始位置设置为右侧,向你继承的 OpenFileDialogEx
添加一个 PictureBox
,并将 PictureBox
的 Anchor
属性设置为 Left, Top, Right, Bottom
;这将使 PictureBox 在用户调整 OpenFileDialog
大小时动态调整大小。
这些方法是虚拟方法,你将覆盖它们以与原始 OpenFileDialog
进行交互。
OnFileNameChanged()
每次用户在视图中单击任何文件时,都会调用此方法。
OnFolderNameChanged()
每次用户从 OpenFileDialog
中的任何控件更改文件夹时,都会调用此方法。
OnClosing()
当 OpenFileDialog
关闭时调用此方法,这对于释放任何分配的资源非常有用。
两个事件是 FileNameChanged
和 FolderNameChanged
,这些事件从它们各自的虚拟方法“OnFileNameChanged
”和“OnFolderNameChanged
”触发。我建议覆盖这些方法而不是使用事件,因为这样代码更简洁,而且没有额外的间接层次。
它是如何做到的?
第一个问题是 OpenFileDialog
是一个模态对话框。这意味着你基本上无法获取窗口的句柄,因为当你调用 ShowDialog()
时,只要 OpenFileDialog
处于打开状态,你就无法控制程序流程。
获取 OpenFileDialog
句柄的一种方法是覆盖窗体上的 WndProc
方法并监视消息。当创建 OpenFileDialog
时,所有者窗体将收到一些消息,如 WM_IDLE
、WM_ACTIVATE
、WM_NC_ACTIVATE
等。所有这些消息都将参数 lParam
设置为 OpenFileDialog
窗口的句柄。
如你所见,这需要覆盖 WndProc
方法。有些开发人员甚至不知道 WndProc
的存在,所以我希望避免这种情况。此外,我注意到 MDI 窗口打开 OpenFileDialog
时存在一些问题。
然后,我基本上所做的是,当调用 ShowDialog()
时,它会创建一个屏幕外的虚拟窗体并将其隐藏,这个窗体将负责打开 OpenFileDialog
并获取 OpenFileDialog
窗口句柄。
起初,它监听 WM_IDLE
消息,但问题是当发送消息时,已经太晚了,窗口已经创建并显示在屏幕上。但是,你仍然可以更改内容,但用户将在原始 OpenFileDialog
和自定义版本之间看到屏幕上的短暂闪烁。
相反,我们可以捕获发生在 OpenDialog
显示在屏幕之前 WM_ACTIVATE
消息。
到目前为止,它已获得句柄并准备显示,现在呢?
它将如何更改 OpenFileDialog
窗口的属性?
这就是方便的 .NET NativeWindow
发挥作用的地方。NativeWindow
是一个窗口包装器,它处理与它关联的句柄发送的消息。它创建一个 NativeWindow
并将 OpenFileWindow
句柄与其关联。从这一点开始,发送到 OpenFileWindow
的每条消息都将被重定向到我们自己的 NativeWindow
中的 WndProc
方法,我们可以取消、修改或让它们通过。
在我们的 WndProc
中,我们处理 WM_WINDOWPOSCHANGING
消息。如果打开对话框正在打开,我们将根据用户设置的 StartLocation
来更改原始的水平或垂直大小。它将增加要创建的窗口的大小。这只会在控件打开时发生一次。
此外,我们将处理 WM_SHOWWINDOW
消息。在这里,原始 OpenFileDialog
中的所有控件都已创建,我们将把我们的控件“附加”到打开的文件对话框。这通过调用 Win32 API “SetParent
”来完成。此 API 允许你更改父窗口。然后,基本上它所做的就是将我们的控件“附加”到原始 OpenFileDialog
中,其位置取决于 StartLocation
属性的值。
其优势在于,我们仍然可以完全控制附加到 OpenFileDialog
窗口的控件。这意味着我们可以接收事件、调用方法,并对这些控件做任何我们想做的事情。
此外,在初始化时,我们将获取原始 OpenFileDialog
中每个控件的窗口句柄。这再次允许创建 .NET NativeWindow
来处理每个控件中的消息。
现在一切都准备好了,我们如何监视当用户单击 ListView
时发送的消息?
起初,我尝试从 ListView
本身处理消息,为它创建一个 NativeWindow
,但问题是每次用户更改文件夹或单击不同的视图时,处理程序都会被销毁,我们必须重新创建处理程序。
使用 MS Spy 分析 FileOpenDialog
中的所有窗口,我们可以注意到 FileOpenDialog
中还有一个 FileDialog
窗口,它非常可能是 FileOpenDialog
的基窗口。检查 MSDN 文档,我们发现对 FileOpenDialog
的所有操作都会触发一个 WM_NOTIFY
消息,填充一个 OFNOTIFY
结构;该结构包含一个操作代码和一个 OFNOTIFY
结构。其中两个操作是 CDN_SELCHANGE
和 CDN_FOLDERCHANGE
。
当用户与文件夹组合框或列表视图交互时,会调用它们。然后,我首先获取到基 FileWindow
的句柄,并从此句柄创建 NativeWindow
。这允许处理 WM_NOTIFY
消息以分析 OFNOTIFY
结构并处理 CDN_SELCHANGE
和 CDN_FOLDERCHANGE
。当此窗口处理这些消息时,它们会被转发到 OpenFileDialogEx
控件的 OnFileNameChanged
和 OnFolderNameChanged
方法。
另一种方法是拦截 FileOpenDialog
窗口关闭的时间。起初,我使用了 WM_CLOSE
消息,它奏效了,但后来我发现当用户在列表视图中双击文件时,这个消息没有被调用。通过监视 FileOpenDialog
产生的消息,我发现我可以使用 WM_IME_NOTIFY
消息。当 FileOpenDialog
关闭时,此消息会以 IMN_CLOSESTATUSWINDOW
的 wParam
值发送;此时,我们将调用转发到 OnClosingDialog()
方法。
现在,当用户调整 FileOpenDialog
大小时如何调整 UserControl
的大小?这是通过处理 WM_WINDOWPOSCHANGING
消息来完成的;在这里,我们指定相对于 FileOpenDialog
大小来调整控件的大小。
作为一个重要的细节,当 OpenFileWindow
关闭时,它必须恢复到打开时的原始大小,这是可能的,因为 OpenFileWindow
会记住最后的位置/大小。如果我们不这样做,每次打开 OpenFileDialog
时,它都会增加大小,使其越来越大。
结论
我在 Windows XP 上测试了这一点,效果很好。我没有机会在 Windows 2000/2003 或 Vista 等不同操作系统上尝试,但它应该能正常工作。我不认为它会在 Windows 95/98 上工作,因为我只将结构大小设置为匹配 WinNT 操作系统。如果你有任何评论或发现错误,请告诉我,我将更新该控件。
历史
- 1.0.1 版本 (2006/11/14)
- 将
DialogResult
状态转发给调用者。 - 代码优化。
- 使用带有特殊标志的
SetWindowsPos
API 来调整控件大小,而不是直接赋值大小,以减少闪烁。 - 控件的调整大小现在在
WM_SIZING
中完成;这修复了一个错误,即在释放鼠标按钮之前,控件直到最后一次更新才被调整大小。
- 将
- 初始发布 1.0.0 (2006/07/14)