65.9K
CodeProject 正在变化。 阅读更多。
Home

定制 .NET 中的 OpenFileDialog

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (109投票s)

2006 年 11 月 7 日

CPOL

10分钟阅读

viewsIcon

595655

downloadIcon

31806

一个扩展控件,用于为 .NET 中的标准 OpenFileDialog 添加额外功能。

Sample Image - OpenFileDialogEx.png

引言

几天前,我想开始创建一个图标编辑器应用程序,以利用我的 IconLib 库

我创建了我的主窗体,然后我想:“我需要从哪里开始?”。然后,我决定创建一个带有“打开”功能的菜单。我认为“打开”功能应该有一个预览屏幕,以便在打开图标之前就能看到它。

如果你正在阅读此页面,很可能是因为你知道 .NET 有一个 OpenFileDialog 类,但它无法自定义。此控件的目标是允许你为 OpenFileDialog .NET 类添加一些功能。你无法自定义 .NET 中的 OpenFileDialog 的主要原因是该类被声明为 sealed,这意味着你无法继承它。如果你查看基类 FileDialog,它允许你继承它,但它有一个 internal abstract 方法“RunFileDialog”。由于它是 internalabstract,因此它只允许在同一程序集中继承它。

你是否曾多少次想在 OpenFileDialog 控件中添加一些额外的控件但却做不到……

在 .NET 中搜索代码时,我发现有几个地方使用了 MFC,但没有 .NET 的。OpenFileDialog 在 .NET 中不是原生实现,而是利用了 Win32 API “GetOpenFileName”。

至此,我有三个选择:

  1. 从头开始创建自己的 OpenFileDialog。
  2. 创建自己的 OpenFileDialog,重用资源(使用 API “GetOpenFileName”并提供自己的模板)。
  3. 破解 .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,并将 PictureBoxAnchor 属性设置为 Left, Top, Right, Bottom;这将使 PictureBox 在用户调整 OpenFileDialog 大小时动态调整大小。

这些方法是虚拟方法,你将覆盖它们以与原始 OpenFileDialog 进行交互。

OnFileNameChanged()

每次用户在视图中单击任何文件时,都会调用此方法。

OnFolderNameChanged()

每次用户从 OpenFileDialog 中的任何控件更改文件夹时,都会调用此方法。

OnClosing()

OpenFileDialog 关闭时调用此方法,这对于释放任何分配的资源非常有用。

两个事件是 FileNameChangedFolderNameChanged,这些事件从它们各自的虚拟方法“OnFileNameChanged”和“OnFolderNameChanged”触发。我建议覆盖这些方法而不是使用事件,因为这样代码更简洁,而且没有额外的间接层次。

它是如何做到的?

第一个问题是 OpenFileDialog 是一个模态对话框。这意味着你基本上无法获取窗口的句柄,因为当你调用 ShowDialog() 时,只要 OpenFileDialog 处于打开状态,你就无法控制程序流程。

获取 OpenFileDialog 句柄的一种方法是覆盖窗体上的 WndProc 方法并监视消息。当创建 OpenFileDialog 时,所有者窗体将收到一些消息,如 WM_IDLEWM_ACTIVATEWM_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_SELCHANGECDN_FOLDERCHANGE

当用户与文件夹组合框或列表视图交互时,会调用它们。然后,我首先获取到基 FileWindow 的句柄,并从此句柄创建 NativeWindow。这允许处理 WM_NOTIFY 消息以分析 OFNOTIFY 结构并处理 CDN_SELCHANGECDN_FOLDERCHANGE。当此窗口处理这些消息时,它们会被转发到 OpenFileDialogEx 控件的 OnFileNameChangedOnFolderNameChanged 方法。

另一种方法是拦截 FileOpenDialog 窗口关闭的时间。起初,我使用了 WM_CLOSE 消息,它奏效了,但后来我发现当用户在列表视图中双击文件时,这个消息没有被调用。通过监视 FileOpenDialog 产生的消息,我发现我可以使用 WM_IME_NOTIFY 消息。当 FileOpenDialog 关闭时,此消息会以 IMN_CLOSESTATUSWINDOWwParam 值发送;此时,我们将调用转发到 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)
© . All rights reserved.