Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Extend OpenFileDialog and SaveFileDialog the Easy Way

0.00/5 (No votes)
19 Jun 2015 26  
Customize OpenFileDialog and SaveFileDialog using a User Control

Screenshot - saveas.jpg

Table of Contents

Introduction

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.

What's New with this Control

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

Improvements at Design Time

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);
}

How It Works

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:

  1. Create the dialog using its constructor. At this point, there is still no UI, so no Windows messages to catch
  2. Set the properties you want to change at runtime using the virtual OnPrepareMSDialog() for the FileDialog and the Load event for the control itself
  3. 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)
  {//.... 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);
}

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;

    // 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);
}

How the Events are Hooked Up

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)
            {
                //.... 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);
}

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.

How to Add New Properties

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);
            //will work only for open dialog, save dialog will not update
            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);
    // 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
    }
}

How to Add the Places Bar to the OpenFileDialog and SaveFiledialog on Windows 2000 and XP

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;
        }
        //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 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.

Using the Control

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.

Display It by Calling the Regular Method ShowDialog

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;
    }
}

Display It by Calling the Extension Method ShowDialog of Extensions Class

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;
    }
}

History

  • This is version 1.0 if you disregard the work it's based on. For me, it works fine on 32 bit Windows XP SP3. I hope you'll enjoy it.
  • Version 1.1 fixes a bug associated with the destruction of the file list and keeps the handle and the view mode up to date. I've added a new design time property to enable/disable the OK button called 'FileDlgEnableOkBtn'.
    Below is an example of how this property can be used based on the conversion result:
    private void MySaveDialogControl_FilterChanged
                    (IWin32Window sender, int index)
    {
        FileDlgEnableOkBtn = GetFormatFromIndex(index);
    }
  • Version 1.2 added several fixes suggested by viewers who left feedback.
    Here are the most important changes as I see them:
    • John Horigan: set the 'AutoUpgradeEnabled' property for Vista/7 to false
    • LETRESTE Bruno: improved the autosize when showing the dialog
    • The solution has been converted to Visual Studio 2008. The old source code for VS 2005 is available in the customFileDialog_old.zip
    • If you still use VS 2005, just copy the new *.cs files over the old ones and open the solution/project. It should work.
    • I also used an automated tool to create a VB version for the same solution.
    • The VB source code is included too and it seems to work.
  • Version 1.3 has fixed more issues, thanks to viewers like beautyod
  • I’ve also added support for the places bar as a new feature targeted at windows 2000 and XP.
  • There are solution files for VS 2008 (.NET 3.5) and 2010 (.NET 4.0) for both VB and C#. The previous source code is available in the embedded CustomFileDialog_src_old.zip archive.
  • Version 1.4 is incorporating more fixes related to Windows 7, 64 bit, etc., thanks to the comments provided by Phil Atkin, John Simmons / outlaw programmer, neyerMat, kore_sar and others.
  • Version 1.5 is changing to Visual Studio 2013 project style and it has the latest source code on codeplex.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here