轻松扩展 OpenFileDialog 和 SaveFileDialog






4.82/5 (80投票s)
使用用户控件自定义 OpenFileDialog 和 SaveFileDialog。
目录
- 引言
- 此控件的新增功能
- 设计时的改进
- 工作原理
- 事件如何连接
- 如何添加新属性
- 如何在 Windows 2000 和 XP 上为 OpenFileDialog 和 SaveFiledialog 添加“常用位置”栏
- 使用控件
- 历史
引言
如果您使用 WinForms,可能在某个时候您想要扩展 OpenFileDialog
或 SaveFileDialog
,但因为没有简单的方法而放弃了,尤其是当您想添加一些新的图形元素时。即使在 MFC 时代,这样的任务也不像 .NET 这样令人望而生畏,因为这些类是密封的,只暴露 16 个属性、5 个方法和 3 个事件用于自定义对话框。 Martin Parry 在 MSDN 上的文章可以帮助您了解如何使用 OFNHookProc
函数和 PInvoke
来自定义 OpenFileDialog
。这似乎意味着我们要回到 20 世纪 90 年代初的编程方式,并添加相当多的 PInvoke
和封送处理才能做任何事情。这足以让人放弃或寻找第三方库等替代方案。如果您正在为 WPF 而不是 Windows Forms 开发,我建议您直接跳转到 我关于同一主题的 WPF 文章。然而,CastorTiu 的文章让那些选择使用 Forms 自定义这些对话框的人的生活变得更轻松。利用他在这一主题上的出色工作,我尝试更进一步,使这两个对话框的自定义更加轻松。我将只关注他对原始辛勤工作的重构和改进,因此如果您需要详细信息,请查看 CastorTiu 的文章。虽然本文仅使用 C# 代码片段,但我已将等效的 VB.NET 代码包含在可下载的 zip 文件中,供 VB 用户使用。
此控件的新增功能
为了尽可能轻松地进行扩展,我在基类控件中添加了一些额外的属性和事件,以及一些设计特性。最受欢迎的属性可能是 FileDlgType
,它允许在设计时选择 OpenFileDialog
和 SaveFileDialog
。额外的属性和事件显示在下面
属性 | 事件 |
![]() | ![]() |
设计时的改进
最好能有一些视觉提示来表明控件的外观。我不想深入研究 设计时架构,所以我使用了一个简单的 OnPaint
重写来绘制一个红线或一个点,表示 FileDialog
接触到扩展。请注意,它仅在设计模式下绘制。
protected override void OnPaint(PaintEventArgs e)
{
if (DesignMode)
{
Graphics gr = e.Graphics;
{
HatchBrush hb = null;
Pen p = null;
try
{
switch (this.FileDlgStartLocation)
{
case AddonWindowLocation.Right:
hb = new System.Drawing.Drawing2D.HatchBrush
(HatchStyle.NarrowHorizontal, Color.Black, Color.Red);
p = new Pen(hb, 5);
gr.DrawLine(p, 0, 0, 0, this.Height);
break;
case AddonWindowLocation.Bottom:
hb = new System.Drawing.Drawing2D.HatchBrush
(HatchStyle.NarrowVertical, Color.Black, Color.Red);
p = new Pen(hb, 5);
gr.DrawLine(p, 0, 0, this.Width, 0);
break;
case AddonWindowLocation.BottomRight:
default:
hb = new System.Drawing.Drawing2D.HatchBrush
(HatchStyle.Sphere, Color.Black, Color.Red);
p = new Pen(hb, 5);
gr.DrawLine(p, 0, 0, 4, 4);
break;
}
}
finally
{
if (p != null)
p.Dispose();
if (hb != null)
hb.Dispose();
}
}
}
base.OnPaint(e);
}
工作原理
CastorTiu 的文章详细介绍了该控件的工作原理。我做了一些改进,但总体思路是一样的。
您需要在对话框变为模态之前获取其句柄,以便您可以将对话框与您自己的控件“粘合”在一起。
这是此复合对话框初始化的流程
- 使用其构造函数创建对话框。此时,仍然没有 UI,因此没有 Windows 消息可以
捕获
- 使用
FileDialog
的虚拟OnPrepareMSDialog()
和控件本身的Load
事件设置您想在运行时更改的属性 - 使用
DialogResult
返回值并处置控件
这是幕后发生的事情:您首先使用正确的 FileDialog
类型作为泛型参数创建 DialogWrapper
帮助器类
public DialogResult ShowDialog(IWin32Window owner)
{
DialogResult returnDialogResult = DialogResult.Cancel;
if (this.IsDisposed)
return returnDialogResult;
if (owner == null || owner.Handle == IntPtr.Zero)
{
WindowWrapper wr = new WindowWrapper
(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
owner = wr;
}
OriginalCtrlSize = this.Size;
_MSdialog = (FileDlgType ==
FileDialogType.OpenFileDlg)?new OpenFileDialog()as
FileDialog:new SaveFileDialog() as FileDialog;
_dlgWrapper = new WholeDialogWrapper(this);
OnPrepareMSDialog();
if (!_hasRunInitMSDialog)
InitMSDialog();
try
{
System.Reflection.PropertyInfo AutoUpgradeInfo =
MSDialog.GetType().GetProperty("AutoUpgradeEnabled");
if (AutoUpgradeInfo != null)
AutoUpgradeInfo.SetValue(MSDialog, false, null);
returnDialogResult = _MSdialog.ShowDialog(owner);
}
catch (ObjectDisposedException)
{
}
catch (Exception ex)
{
MessageBox.Show("unable to get the modal dialog handle", ex.Message);
}
return returnDialogResult;
}
当您调用控件的 ShowDialog public
方法时,消息只有在您调用 .NET 的 Open(Save)FileDialog.ShowDialog()
后才会开始流动。您必须监视 WM_ACTIVATE
,由于应用程序过滤器无法捕获它,因此您必须依赖父窗口的 WndProc
。我发现使用一个消息循环窗口比使用一个虚拟表单效果更好,开销也更小。我在 WholeDialogWrapper
的构造函数中调用 AssignDummyWindow()
来创建它。
private void AssignDummyWindow()
{
_hDummyWnd = NativeMethods.CreateWindowEx(0, "Message",
null, WS_VISIBLE, 0, 0, 0, 0,HWND_MESSAGE, NULL, NULL, NULL);
if (_hDummyWnd == NULL || !NativeMethods.IsWindow(_hDummyWnd))
throw new ApplicationException("Unable to create a dummy window");
AssignHandle(_hDummyWnd);
}
由于我们有这个窗口可以监听其子窗口,我们将如下捕获 WM_ACTIVATE
protected override void WndProc(ref Message m)
{
switch ((Msg)m.Msg)
{//.... code omitted
case Msg.WM_ACTIVATE:
if (_WatchForActivate && !mIsClosing && m.Msg == (int)Msg.WM_ACTIVATE)
//WM_NCACTIVATE works too
{ //Now the Open/Save Dialog is visible and about to enter the modal loop
_WatchForActivate = false;
//Now we save the real dialog window handle
_FileDialogHandle = m.LParam;
ReleaseHandle();//release the dummy window
AssignHandle(_FileDialogHandle);//assign the native open file handle
// to grab the messages
NativeMethods.GetWindowRect(_FileDialogHandle,
ref _CustomControl._DialogWindowRect);
_CustomControl._FileDialogHandle = _FileDialogHandle;
}
break;
//.... code omitted
}
base.WndProc(ref m);
}
一旦我们获得了真实的对话框句柄 _FileDialogHandle
,我们就可以忘记虚拟窗口并开始监听真正重要的事情。注意我如何释放虚拟窗口句柄并分配新的句柄。当同一个 WndProc
捕获 WM_SHOWWINDOW
消息时,我们终于可以排列我们的控件并设置父窗口了
private void InitControls()
{
mInitializated = true;
// Lets get information about the current open dialog
NativeMethods.GetClientRect(new HandleRef(this,_hFileDialogHandle),
ref _DialogClientRect);
NativeMethods.GetWindowRect(new HandleRef(this,_hFileDialogHandle),
ref _DialogWindowRect);
// Lets borrow the Handles from the open dialog control
PopulateWindowsHandlers();
switch (_CustomControl.FileDlgStartLocation)
{
case AddonWindowLocation.Right:
// Now we transfer the control to the open dialog
_CustomControl.Location = new Point((int)
(_DialogClientRect.Width - _CustomControl.Width), 0);
break;
case AddonWindowLocation.Bottom:
// Now we transfer the control to the open dialog
_CustomControl.Location = new Point(0,
(int)(_DialogClientRect.Height - _CustomControl.Height));
break;
case AddonWindowLocation.BottomRight:
// We don't have to do too much in this case, just the default thing
_CustomControl.Location =
new Point((int)(_DialogClientRect.Width - _CustomControl.Width),
(int)(_DialogClientRect.Height - _CustomControl.Height));
break;
}
// Everything is ready, now lets change the parent
NativeMethods.SetParent(new HandleRef(_CustomControl,_CustomControl.Handle),
new HandleRef(this,_hFileDialogHandle));
// Send the control to the back
// NativeMethods.SetWindowPos(_CustomControl.Handle,
(IntPtr)ZOrderPos.HWND_BOTTOM, 0, 0, 0, 0, UFLAGSZORDER);
_CustomControl.MSDialog.Disposed += new EventHandler(DialogWrappper_Disposed);
}
事件如何连接
您可能认为此代码在属性和事件方面是详尽的。嗯……不完全是!我在原始工作的基础上添加了几个属性和事件,但您可能仍然想添加更多。
在上面的代码片段中,您可以看到如何连接 Open(Save)FileDialog.Dispose
中的事件,但这很简单。我将基于我添加的示例,描述如何将新事件添加到 FileDialogControlBase
。
还有一个名为 MSFileDialogWrapper
的辅助类,它通过 WndProc
监视 Open(Save)FileDialog
对象,如下所示
protected override void WndProc(ref Message m)
{
switch ((Msg)m.Msg)
{
case Msg.WM_NOTIFY:
OFNOTIFY ofNotify = (OFNOTIFY)Marshal.PtrToStructure
(m.LParam, typeof(OFNOTIFY));
switch (ofNotify.hdr.code)
{
//.... code omitted
case (uint)DialogChangeStatus.CDN_TYPECHANGE:
{
OPENFILENAME ofn =
(OPENFILENAME)Marshal.PtrToStructure
(ofNotify.OpenFileName, typeof(OPENFILENAME));
int i = ofn.nFilterIndex;
if (_CustomCtrl != null && _filterIndex != i)
{
_filterIndex = i;
_CustomCtrl.OnFilterChanged
(this as IWin32Window, i);
}
}
break;
}
//.... code omitted
}
base.WndProc(ref m);
}
一旦我们将内部指针封送回正确的结构,我们就调用 FileDialogControBase
对象的 OnFilterChanged
方法,在本例中是 _CustomCtrl
。如果深入研究此方法,我们会发现以下内容
internal void OnFilterChanged(IWin32Window sender, int index)
{
if (EventFilterChanged != null)
EventFilterChanged(sender, index);
}
快速查看 EventFilterChanged
定义会发现它是一个事件。
public delegate void PathChangedEventHandler(IWin32Window sender,
string filePath);
public delegate void FilterChangedEventHandler(IWin32Window sender, int index);
public event PathChangedEventHandler EventFileNameChanged;
public event PathChangedEventHandler EventFolderNameChanged;
public event FilterChangedEventHandler EventFilterChanged;
public event CancelEventHandler EventClosingDialog;
这使得添加和删除它们非常容易,就像处理常规 WinForms 事件一样。
如何添加新属性
要设置 Open(Save)FileDialog
对象的属性,您必须重写 OnPrepareMSDialog()
,如果设计时设置不正确。但是,更改 Open(Save)FileDialog
的外观更复杂。例如,我将展示如何更改“确定”按钮上的文本 - 即“保存”或“打开”按钮。我们首先从 FileDialogControlBase
暴露该属性,然后使用 PInvoke
设置文本,如下所示
[DefaultValue("&Open")]
public string FileDlgOkCaption
{
get { return _OKCaption; }
set { _OKCaption = value; }
//........................
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!DesignMode)
{
if (MSDialog != null)
{
MSDialog.FileOk += new CancelEventHandler
(FileDialogControlBase_ClosingDialog);
MSDialog.Disposed += new EventHandler
(FileDialogControlBase_DialogDisposed);
MSDialog.HelpRequest += new EventHandler
(FileDialogControlBase_HelpRequest);
NativeMethods.SetWindowText(_dlgWrapper.Handle, _Caption);
//will work only for open dialog, save dialog will not update
NativeMethods.SetWindowText(_hOKButton, _OKCaption);
}
}
}
正如您在上面的代码注释中看到的,我们意识到我们正在处理一个黑匣子,有时我们无法知道更新的正确方法。这就是我们可以在 OpenFileDialog
上更改但不能在 SaveFileDialog
上更改的情况之一。如果您想知道我们如何获得 _hOKButton
或 _dlgWrapper.Handle
,您需要查看 FileDialogEnumWindowCallBack
,当 WM_SHOWWINDOW
在 DialogWrapper
的 WndProc
中捕获时,它会被调用。
private bool FileDialogEnumWindowCallBack(IntPtr hwnd, int lParam)
{
StringBuilder className = new StringBuilder(256);
NativeMethods.GetClassName(new HandleRef(this,hwnd), className, className.Capacity);
int controlID = NativeMethods.GetDlgCtrlID(hwnd);
WINDOWINFO windowInfo;
NativeMethods.GetWindowInfo(new HandleRef(this,hwnd), out windowInfo);
// Dialog Window
if (className.ToString().StartsWith("#32770"))
{
_BaseDialogNative = new MSFileDialogWrapper(_CustomControl);
_BaseDialogNative.AssignHandle(hwnd);
return true;
}
switch ((ControlsId)controlID)
{
//.....code omitted
case ControlsId.ButtonOk:
_OKButton = hwnd;
_OKButtonInfo = windowInfo;
_CustomControl._hOKButton = hwnd;
break;
//.....code omitted
}
}
如何在 Windows 2000 和 XP 上为 OpenFileDialog 和 SaveFiledialog 添加“常用位置”栏
在 .NET 3.5 为 Windows Vista 及更高版本引入 FileDialogCustomPlacesCollection 之前,据我所知,没有 API 允许您修改对话框上的“常用位置”栏。在 Windows 2000 和 XP 上有一个方法可以做到这一点,正如 Dino Esposito 在他的 MSDN 文章中所述。这需要修改注册表,并且只要用户登录,就会影响所有这些对话框的实例。由于这更像是一个本机对话框功能,我使用了应用于基类 FileDialog
的 扩展方法。下面显示了 static
类,它公开了两个 public
方法来设置常用位置和恢复注册表。
public static class FileDialogPlaces
{
private static readonly string TempKeyName =
"TempPredefKey_" + Guid.NewGuid().ToString();
private const string Key_PlacesBar =
@"Software\Microsoft\Windows\CurrentVersion\Policies\ComDlg32\PlacesBar";
private static RegistryKey _fakeKey;
private static IntPtr _overriddenKey;
private static object[] m_places;
public static void SetPlaces(this FileDialog fd, object[] places)
{
if (fd == null)
return;
if (m_places == null)
m_places = new object[5];
else
m_places.Initialize();
if (places != null)
{
for (int i = 0; i < m_places.GetLength(0); i++)
{
m_places[i] = places[i];
}
}
if (_fakeKey != null)
ResetPlaces(fd);
SetupFakeRegistryTree();
if (fd != null)
fd.Disposed += (object sender, EventArgs e) =>
{ if (m_places != null && fd != null) ResetPlaces(fd); };
}
static public void ResetPlaces(this FileDialog fd)
{
if (_overriddenKey != IntPtr.Zero)
{
ResetRegistry(_overriddenKey);
_overriddenKey = IntPtr.Zero;
}
if (_fakeKey != null)
{
_fakeKey.Close();
_fakeKey = null;
}
//delete the key tree
Registry.CurrentUser.DeleteSubKeyTree(TempKeyName);
m_places = null;
}
private static void SetupFakeRegistryTree()
{
_fakeKey = Registry.CurrentUser.CreateSubKey(TempKeyName);
_overriddenKey = InitializeRegistry();
// write dynamic places here reading from Places
RegistryKey reg = Registry.CurrentUser.CreateSubKey(Key_PlacesBar);
for (int i = 0; i < m_places.GetLength(0); i++)
{
if (m_places[i] != null)
{
reg.SetValue("Place" + i.ToString(), m_places[i]);
}
}
}
static readonly UIntPtr HKEY_CURRENT_USER = new UIntPtr(0x80000001u);
private static IntPtr InitializeRegistry()
{
IntPtr hkMyCU;
NativeMethods.RegCreateKeyW(HKEY_CURRENT_USER, TempKeyName, out hkMyCU);
NativeMethods.RegOverridePredefKey(HKEY_CURRENT_USER, hkMyCU);
return hkMyCU;
}
static void ResetRegistry(IntPtr hkMyCU)
{
NativeMethods.RegOverridePredefKey(HKEY_CURRENT_USER, IntPtr.Zero);
NativeMethods.RegCloseKey(hkMyCU);
return;
}
}
SetPlaces
接受一个最多包含五个对象的数组作为参数,这些对象可以是数字(特殊文件夹)或字符串(常规文件夹)。设置在 FileDialog
上的 Disposed
事件会自动调用 ResetPlaces
来恢复注册表。为了方便起见,我包含了 Places
辅助枚举,用于预定义的特殊文件夹。请注意,这在 Vista 或更高版本的 Windows OS 上可能会因 UAC 而失败。
如果您在 Vista 或更高版本上运行您的应用程序,请忽略此类,而是使用 Microsoft 的 FileDialogCustomPlacesCollection
类,或者更好的是,使用一些逻辑根据 操作系统版本 来选择方法。
使用控件
现在,让我们开始使用它。要开始使用,您可以将代码放入您的项目中,或者只是将对 FileDlgExtenders.dll 程序集的引用添加到 FileDlgExtenders
项目中。如果您选择后者,请先构建解决方案,因为在设计时需要基类。为了尽可能简单,选择“将用户控件添加到项目”,然后选择“继承的用户控件”,最后从列表中选择 FileDialogControlBase
。例如,我添加了一个名为 MySaveDialogControl
的控件,它只是将图像转换为所需尺寸、方向和文件格式的缩略图。接下来,您可能会在设计模式下设置属性和事件。有两种方法可以显示该控件。
通过调用常规方法 ShowDialog 显示
要在运行时设置 FileDialog
的数据,请在您的子类中重写 virtual
方法 OnPrepareMSDialog()
。您应该首先调用 base.OnPrepareMSDialog()
,这样您自己的更改就不会被清除。下面是一个示例,说明我如何在派生控件中更改运行时 FileDialog
的属性。
protected override void OnPrepareMSDialog()
{
base.FileDlgInitialDirectory = Environment.GetFolderPath
(Environment.SpecialFolder.MyPictures);
if (Environment.OSVersion.Version.Major < 6)
MSDialog.SetPlaces( new object[] { @"c:\",
(int)Places.MyComputer, (int)Places.Favorites, (int)Places.Printers,
(int)Places.Fonts, });
base.OnPrepareMSDialog();
}
如果您选择重写 OnLoad
,也需要采取类似的预防措施。始终调用 base.OnLoad
,否则 Load
事件将不会被调用。提示:如果 Visual Studio 无法呈现新控件,请清理解决方案并重新生成。如果仍然不起作用,您可能需要手动清除 bin 文件夹,并检查您在设计时初始化的对象。
最后,这是调用者显示的示例
using (MySaveDialogControl saveDialog = new MySaveDialogControl(lblFilePath.Text, this))
{
if (saveDialog.ShowDialog(this) == DialogResult.OK)
{
lblFilePath.Text = saveDialog.MSDialog.FileName;
}
}
通过调用 Extensions 类的扩展方法 ShowDialog 显示
有些人可能更喜欢扩展方法 ShowDialog
提供的语法糖
public static class Extensions
{
public static DialogResult ShowDialog
(this FileDialog fdlg, FileDialogControlBase ctrl, IWin32Window owner)
{
ctrl.FileDlgType =(fdlg is SaveFileDialog)?
FileDialogType.SaveFileDlg: FileDialogType.OpenFileDlg;
if (ctrl.ShowDialogExt(fdlg, owner) == DialogResult.OK)
return DialogResult.OK;
else
return DialogResult.Ignore;
}
}
您将不再需要关心重写 OnPrepareMSDialog()
,但您必须在调用者代码中以编程方式在运行时设置 FileDialog
成员。
using (MyOpenFileDialogControl openDialogCtrl = new MyOpenFileDialogControl())
{
openDialogCtrl.FileDlgInitialDirectory =
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
OpenFileDialog openDialog = new OpenFileDialog();
openDialog.InitialDirectory = Environment.GetFolderPath
(Environment.SpecialFolder.MyPictures);
openDialog.AddExtension = true;
openDialog.Filter = "Image Files(*.bmp)|*.bmp
|Image Files(*.JPG)|*.JPG|Image Files(*.jpeg)|*.jpeg
|Image Files(*.GIF)|*.GIF|Image Files(*.emf)|*emf.|
Image Files(*.ico)|*.ico|Image Files(*.png)|*.png|
Image Files(*.tif)|*.tif|Image Files(*.wmf)|*.wmf|Image Files(*.exif)|*.exif";
openDialog.FilterIndex = 2;
openDialog.CheckFileExists = true;
openDialog.DefaultExt = "jpg";
openDialog.FileName = "Select Picture";
openDialog.DereferenceLinks = true;
if (Environment.OSVersion.Version.Major < 6)
openDialog.SetPlaces(new object[] { @"c:\",
(int)Places.MyComputer, (int)Places.Favorites,
(int)Places.Printers, (int)Places.Fonts, });
if (openDialog.ShowDialog(openDialogCtrl, this) == DialogResult.OK)
{
lblFilePath.Text = openDialog.FileName;
}
}
历史
- 如果忽略 其所基于的工作,这是 1.0 版本。对我来说,它在 32 位 Windows XP SP3 上运行良好。希望您会喜欢。
- 版本 1.1 修复了与文件列表销毁相关的错误,并保持句柄和视图模式最新。我添加了一个新的设计时属性来启用/禁用 **OK** 按钮,称为 '
FileDlgEnableOkBtn
'。
下面是一个根据转换结果使用此属性的示例private void MySaveDialogControl_FilterChanged (IWin32Window sender, int index) { FileDlgEnableOkBtn = GetFormatFromIndex(index); }
- 版本 1.2 根据查看者的反馈添加了多项修复。
我认为最重要的更改如下- John Horigan:将 Vista/7 的 '
AutoUpgradeEnabled
' 属性设置为false
- LETRESTE Bruno:改进了显示对话框时的
autosize
- 解决方案已转换为 Visual Studio 2008。VS 2005 的旧源代码可在 customFileDialog_old.zip 中找到
- 如果您仍在使用 VS 2005,只需将新的 *.cs 文件复制到旧文件之上并打开解决方案/项目。应该可以工作。
- 我还使用了一个自动化工具为同一解决方案创建了 VB 版本。
- VB 源代码也包含在内,并且似乎可以正常工作。
- John Horigan:将 Vista/7 的 '
- 版本 1.3 修复了更多问题,感谢 beautyod 等观众
- 我还为 Windows 2000 和 XP 添加了对“常用位置”栏的支持,这是一项新功能。
- 为 VB 和 C# 提供了 VS 2008(.NET 3.5)和 2010(.NET 4.0)的解决方案文件。以前的源代码可在嵌入的 CustomFileDialog_src_old.zip 存档中找到。
- 版本 1.4 结合了 Phil Atkin、John Simmons / outlaw programmer、neyerMat、kore_sar 等人提供的评论,对 Windows 7、64 位等进行了更多修复。
- 版本 1.5 更改为 Visual Studio 2013 项目风格,并在 codeplex 上提供最新的源代码。