Table of Contents
If you used WinForms, chances are that at some point, you wanted to extend the OpenFileDialog
or SaveFileDialog
, but you gave up because there is no easy way to do it, especially if you wanted to add some new graphical elements. Even in the MFC days, such a task was not as daunting as it is for .NET because these classes are sealed, exposing only 16 properties, 5 methods and 3 events that can be used to customize the dialogs. Martin Parry's article on MSDN can give you an insight into how you can customize the OpenFileDialog
using the OFNHookProc
function and PInvoke
. It would look like we have to roll back the clock to early 1990's way of programming and add quite a bit of PInvoke
and marshalling to do anything. This is probably enough to give up or look to alternatives like third party libraries. If you are developing for WPF rather than Windows Forms, I suggest that you jump directly to my WPF article on the same topic. However, CastorTiu's article made life A LOT EASIER on those who chose to customize these dialogs using Forms. Using his great work on this topic, I tried to take it even further and make customizing of these two dialogs even more painless. I will focus only on my refactoring and improvements to his original hard work, so if you need details, please check CastorTiu's article. While this article uses only C# snippets, I’ve included the equivalent VB.NET code in the downloadable zip file for the VB folks.
To make extending as easy as possible, I've added some extra properties and events to the base control as well as some design features. Probably the most appreciated property will be the FileDlgType
that would allow selecting between OpenFileDialog
and SaveFileDialog
at design time. The extra properties and events are displayed below:
Properties | Events |
| |
It would be nice to have some visual clue about how the control will look like. I did not want to dig too much design time architecture so I used a simple OnPaint
override to draw a red line or a dot where the FileDialog
touches the extension. Notice that it is painting only in Design mode.
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's article describes in detail how the control works. I've made some improvements, but the general idea is the same.
You need to get the dialog handle before it goes modal, so you can 'glue together' the dialog with your own control.
Here is the flow of initializing of this composite dialog:
- Create the dialog using its constructor. At this point, there is still no UI, so no Windows messages to
catch
- Set the properties you want to change at runtime using the virtual
OnPrepareMSDialog()
for the FileDialog
and the Load
event for the control itself - Use the
DialogResult
return and dispose of the control
Here is what goes on behind the scenes: You start by creating the helper class DialogWrapper
with the right FileDialog
type as a generic parameter:
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;
}
When you call ShowDialog public
method of your control, the messages start flowing only after you called .NET's Open(Save)FileDialog.ShowDialog()
. You have to watch for WM_ACTIVATE
, and since an Application filter won't catch it, you have to rely on the parent's window WndProc
. Instead of using a dummy form, I found out that a message pump window will do just as fine with less overhead. I created it inside WholeDialogWrapper
's constructor calling 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);
}
Since we have this Window that listens to its Children, we will catch the WM_ACTIVATE
as below:
protected override void WndProc(ref Message m)
{
switch ((Msg)m.Msg)
{
case Msg.WM_ACTIVATE:
if (_WatchForActivate && !mIsClosing && m.Msg == (int)Msg.WM_ACTIVATE)
{
_WatchForActivate = false;
_FileDialogHandle = m.LParam;
ReleaseHandle();
AssignHandle(_FileDialogHandle);
NativeMethods.GetWindowRect(_FileDialogHandle,
ref _CustomControl._DialogWindowRect);
_CustomControl._FileDialogHandle = _FileDialogHandle;
}
break;
}
base.WndProc(ref m);
}
Once we got the real dialog handle as _FileDialogHandle
, we can forget about the dummy Window and start listening to what really matters. Notice how I released the dummy Window handle and assigned the new one. When the same WndProc
catches the WM_SHOWWINDOW
message, we can finally arrange our control and set the parent:
private void InitControls()
{
mInitializated = true;
NativeMethods.GetClientRect(new HandleRef(this,_hFileDialogHandle),
ref _DialogClientRect);
NativeMethods.GetWindowRect(new HandleRef(this,_hFileDialogHandle),
ref _DialogWindowRect);
PopulateWindowsHandlers();
switch (_CustomControl.FileDlgStartLocation)
{
case AddonWindowLocation.Right:
_CustomControl.Location = new Point((int)
(_DialogClientRect.Width - _CustomControl.Width), 0);
break;
case AddonWindowLocation.Bottom:
_CustomControl.Location = new Point(0,
(int)(_DialogClientRect.Height - _CustomControl.Height));
break;
case AddonWindowLocation.BottomRight:
_CustomControl.Location =
new Point((int)(_DialogClientRect.Width - _CustomControl.Width),
(int)(_DialogClientRect.Height - _CustomControl.Height));
break;
}
NativeMethods.SetParent(new HandleRef(_CustomControl,_CustomControl.Handle),
new HandleRef(this,_hFileDialogHandle));
(IntPtr)ZOrderPos.HWND_BOTTOM, 0, 0, 0, 0, UFLAGSZORDER);
_CustomControl.MSDialog.Disposed += new EventHandler(DialogWrappper_Disposed);
}
You might think that this code is exhaustive relating to the properties and events you can use. Well... not quite! I've added several properties and events to the original work, but you might still want to add more to it.
Just in the above code snippet, you see how the events from the Open(Save)FileDialog.Dispose
are hooked up, but this is an easy one. I'll describe how you can add new events to FileDialogControlBase
based on the one I've added as an example.
There is still another helper class called MSFileDialogWrapper
that monitors the Open(Save)FileDialog
object through the WndProc
as below:
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)
{
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;
}
}
base.WndProc(ref m);
}
Once we marshal the internal pointers into the right structures, we call the OnFilterChanged
method of FileDialogControBase
object, which is _CustomCtrl
in this case. If we dug deeper into this method, we find something as below:
internal void OnFilterChanged(IWin32Window sender, int index)
{
if (EventFilterChanged != null)
EventFilterChanged(sender, index);
}
A quick look at the EventFilterChanged
definitions reveals that it is an event.
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;
This makes it very easy to add and remove them the same way you deal with regular WinForms events.
To set the properties of the Open(Save)FileDialog
object, you have to override the OnPrepareMSDialog()
if the design time settings are not right. However, changing the appearance of the Open(Save)FileDialog
is more elaborate. As an example, I'll show how to change the Text on the Ok button - that's the Save or Open button. We start with exposing the property from the FileDialogControlBase
and then use PInvoke
to set the text as below:
[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);
NativeMethods.SetWindowText(_hOKButton, _OKCaption);
}
}
}
As you see the comment in the code above, realize that we are dealing with a black box and sometimes we can't know the right way to update. This is one of those cases when we can change it on OpenFileDialog
but not on SaveFileDialog
. If you wonder how we got the _hOKButton
or _dlgWrapper.Handle
, you would have to look at FileDialogEnumWindowCallBack
that is invoked when the WM_SHOWWINDOW
is captured in DialogWrapper
's 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);
if (className.ToString().StartsWith("#32770"))
{
_BaseDialogNative = new MSFileDialogWrapper(_CustomControl);
_BaseDialogNative.AssignHandle(hwnd);
return true;
}
switch ((ControlsId)controlID)
{
case ControlsId.ButtonOk:
_OKButton = hwnd;
_OKButtonInfo = windowInfo;
_CustomControl._hOKButton = hwnd;
break;
}
}
Until .NET 3.5 introduced FileDialogCustomPlacesCollection for Windows Vista and up, there was no API that I know of to allow you to modify the places bar on the dialog. There is a way to do it on Windows 2000 and XP as described by Dino Esposito in his MSDN article. That requires modifying the registry and it will affect all instances of these dialogs as long as the user is logged on. Since this looks more like a native dialog feature, I’ve used extension methods applied to the base class FileDialog
. The static
class is shown below and it exposes two public
methods to set the places and to restore the registry.
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;
}
Registry.CurrentUser.DeleteSubKeyTree(TempKeyName);
m_places = null;
}
private static void SetupFakeRegistryTree()
{
_fakeKey = Registry.CurrentUser.CreateSubKey(TempKeyName);
_overriddenKey = InitializeRegistry();
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
takes an argument as an array of up to five objects that can be numbers as special folders or strings as regular folders. The Disposed
event set on the FileDialog
makes an automatic call to the ResetPlaces
that restores the registry. As a convenience, I’ve included the Places
helper enumeration for predefined special folders. Be aware that this could fail on Vista or newer Windows OSes due to UAC.
If you are running your application on Vista or later, ignore this class and use Microsoft’s FileDialogCustomPlacesCollection
class instead, or better yet, use some logic to select the method based on the OS version.
Now, let's put it to work. To start using it, you can drop the code in your project or just add a reference to the FileDlgExtenders.dll assembly or to FileDlgExtenders
project. If you choose the latter, build the solution before you move forward, because you need the base class at design time. To make things as easy as possible, select 'Add User Control' to your project, than pick 'Inherited User Control' and finally select FileDialogControlBase
from the list. As an example, I've added a control called MySaveDialogControl
that is just converting images to thumbnails of the desired dimensions, orientation and file format. Next, you will probably set the properties and events in design mode. There are two ways to display the control.
To set up FileDialog
's data at runtime, override the virtual
method OnPrepareMSDialog()
in your subclass. You should call base.OnPrepareMSDialog()
firstly, so your own changes won't be wiped away. Below is an example of how I change FileDialog
's properties at runtime in my derived control.
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();
}
A similar precaution is needed if you choose to override OnLoad
. Always call base.OnLoad
, otherwise the Load
event won't get called. As a tip, if Visual Studio can't render the new control, clean the solution and rebuild it again. If it still does not work, you might have to manually purge the bin folders, and check what objects you initialize at Design time.
Finally, here is the caller displaying it:
using (MySaveDialogControl saveDialog = new MySaveDialogControl(lblFilePath.Text, this))
{
if (saveDialog.ShowDialog(this) == DialogResult.OK)
{
lblFilePath.Text = saveDialog.MSDialog.FileName;
}
}
Some of you might like more the syntactical sugar offered by the extension method 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;
}
}
You won’t have to care about overriding OnPrepareMSDialog()
anymore, but you will have to set the FileDialog
members programmatically at runtime in the caller code.
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;
}
}