Extend the Common Dialog Boxes Using Windows Forms 1.x

 

Martin Parry
Microsoft Ltd.

March 2005

Applies to:
   Microsoft® .NET Framework 1.1
   Microsoft® Visual Studio .NET 2003

Summary: Describes some techniques for placing Windows Forms controls inside the standard File Open dialog box. You can use this ability to provide "preview" or "open as" behavior in your own applications. The same techniques can be applied to the other common dialog boxes. (8 printed pages)

Download the ExtensibleDialogsSource.msi sample file (297 KB).

Note   The techniques used in this article and the accompanying sample code are not supported by Microsoft. Use them at your own risk.

Contents

Introduction
Extensibility of Common Dialogs
How to Consume the Sample Open File Dialog
Summary
About the Author

Introduction

The Common Dialog Box Library has given application developers a standard way to let users browse the file system. You don't have to use this library, but it makes a lot of sense to do so: You get a prebuilt set of dialog boxes, and your users will feel right at home. Even so, many developers would like to offer a little extra functionality in these dialog boxes. Examples are:

  • The ability to preview a file, having clicked on it inside the File Open dialog box
  • The presence of Open As or Save As options inside the common dialog box

The common dialog boxes have always exposed an extensibility mechanism, but with the advent of "explorer style" common dialogs it became more difficult to make extended dialogs look just right. Managed code was out of the question too; examples of code that extends the common dialog boxes all seem to use Win32 code, with dialog resources.

This article will show how you can modify and extend the File Open dialog box from managed code. You can place Windows Forms controls inside the File Open dialog, interact with them from your code, receive events from them, and so on. The same ideas are applicable to the other common dialog boxes. Figure 1 shows the standard File Open dialog.

Figure 1. The standard File Open dialog box

The code sample associated with this article uses the File Open dialog box to search for picture files, and it provides a "preview" window inside the dialog box that shows the contents of each file selected. Figure 2 shows the extended File Open dialog. It works by allowing a client application to provide a control (a "Panel" seems most appropriate) that it then builds into the File Open dialog box. The application can place any child controls inside the panel, and hook event handlers onto them. The code sample simply uses a Panel control that contains a PictureBox. The File Open dialog box raises an event so that the client application knows when to change the contents of the PictureBox.

Figure 2. The File Open dialog box, extended to include a "preview" panel

Extensibility of Common Dialogs

The Common Dialog Box Library, which ships as ComDlg32.dll, includes an extensibility mechanism. For details of this mechanism, look at the following articles:

For the File Open dialog box, you must call the Win32 API GetOpenFileName, passing it an OPENFILENAME structure. One of the .NET Framework SDK samples shows how you can use this structure from managed code. See the OpenFileDlg sample.

Among other things, the OPENFILENAME structure tells the API about your "hook" procedure that will receive events generated within the dialog. It turns out that, for this hook procedure to receive all the messages you need, you must also supply a "child dialog" that will sit within the common dialog. We'll come back to the hook procedure later.

Note   You must supply the child dialog simply to get all the necessary messages sent to your hook procedure. The technique presented here uses a simple child dialog that contains a single (empty) static label control. The child dialog is not visible inside the common dialog.

This extensibility mechanism was designed before the days of managed code. It assumes that you're able to define a child dialog to place inside the common dialog, using an unmanaged Win32 dialog template resource. Achieving the same result from managed code requires you to build up a managed type that can be marshaled via p/invoke and presented to the Win32 API as a DLGTEMPLATE structure. Here's the managed class from the accompanying code sample:

[StructLayout(LayoutKind.Sequential)]
internal class DlgTemplate
{
   // DLGTEMPLATE
   public Int32 style = DlgStyle.Ds3dLook
             | DlgStyle.DsControl
             | DlgStyle.WsChild
             | DlgStyle.WsClipSiblings
             | DlgStyle.SsNotify;
   public Int32 extendedStyle =   ExStyle.WsExControlParent;
   public Int16 numItems = 1;
   public Int16 x = 0;
   public Int16 y = 0;
   public Int16 cx = 0;
   public Int16 cy = 0;
   public Int16 reservedMenu = 0;
   public Int16 reservedClass = 0;
   public Int16 reservedTitle = 0;

   // DLGITEMTEMPLATE
   public Int32 itemStyle = DlgStyle.WsChild;
   public Int32 itemExtendedStyle = ExStyle.WsExNoParentNotify;
   public Int16 itemX = 0;
   public Int16 itemY = 0;
   public Int16 itemCx = 0;
   public Int16 itemCy = 0;
   public Int16 itemId = 0;
   public UInt16 itemClassHdr = 0xffff;
   public Int16 itemClass = 0x0082;
   public Int16 itemText = 0x0000;
   public Int16 itemData = 0x0000;
};

Note   This is a very specific implementation: the unmanaged DLGTEMPLATE structure actually defines the start of a data structure that contains multiple variable-length arrays. The type provided here is specifically for a dialog box that contains a single static label control. Notice too the combination of dialog and window styles that are applied. These styles are required to make the common dialog extension work properly.

The Hook Procedure

The unmanaged OPENFILENAME structure allows you to provide your own "hook" procedure. All window messages generated within the common dialog will be routed to this procedure, and it is this that allows you to lay out your own controls inside the dialog. Naturally enough, the OPENFILENAME structure expects you to specify your hook procedure using an unmanaged function pointer (see the documentation for OFNHookProc). Fortunately, the p/invoke technology allows you to provide a managed delegate that closely resembles the prototype of OFNHookProc, and use an implementation of that delegate in your call to GetOpenFileName.

Here's the delegate used in the accompanying sample code:

internal delegate IntPtr OfnHookProc( IntPtr hWnd,
                    UInt16 msg,
                    Int32 wParam,
                    Int32 lParam );

And here's how it is used:

_ofn.lpfnHook = new OfnHookProc(MyHookProc);

A minimal implementation of the hook procedure must handle the following window messages:

  • WM_INITDIALOG. This tells you that the common dialog has been created. At this point you can insert your own controls into the dialog. The accompanying code sample does this using the SetParent API.
  • WM_SIZE. This message is sent every time the user resizes the dialog, as well as when it's first shown. You can use this to resize any controls you've positioned inside the dialog, as well as any of the standard controls that are already present.
  • WM_NOTIFY. This message can carry a variety of different notification messages, including those specifically generated by the common dialogs, whose names are all of the form CDN_xxx. The accompanying code sample only responds to one such notification, CDN_SELCHANGE, which tells you that the user has selected an item (file or folder) inside the common dialog. When you receive this notification, you can ask the common dialog for the full path of the selected item, by sending it the CDM_GETFILEPATH message, like this:
StringBuilder pathBuffer = new StringBuilder(_MAX_PATH);
NativeMethods.SendMessage( hWndParent,
               CommonDlgMessage.GetFilePath,
               _MAX_PATH,
               pathBuffer );
string path = pathBuffer.ToString();

Marshalling Strings via P/Invoke

The OPENFILENAME structure passes two memory buffers into the GetOpenFileName API, one for the complete path selected in the dialog, and one for just the filename. Normally, if you want to call an API that needs to write a string into memory, you can pass a StringBuilder and p/invoke takes care of the magic that makes it work. The code sample uses this with the SendMessage API. However, when the string buffers are part of a larger structure, p/invoke can't work its magic with StringBuilders; you need to do something a little more involved . . .

You need to allocate these two string buffers in unmanaged memory. How can you do this from managed code? System.Runtime.InteropServices.Marshal is your friend. You allocate the memory as follows:

_fileNameBuffer = Marshal.AllocCoTaskMem( 2 * _MAX_PATH );
_fileTitleBuffer = Marshal.AllocCoTaskMem( 2 * _MAX_PATH );

Since AllocCoTaskMem deals in terms of bytes, and the code sample uses Unicode everywhere, we multiply by 2 to size the buffers correctly.

Before you use these buffers, it's wise to zero the memory they contain. The code sample uses an array of zero-valued bytes along with Marshal.Copy to write zeros into both buffers. All you need now is the ability to copy Unicode strings into and out of these buffers. Strings can be written into a buffer like so:

UnicodeEncoding ue = new UnicodeEncoding();
byte[] pathBytes = ue.GetBytes( path );
Marshal.Copy( pathBytes, 0, _fileNameBuffer, pathBytes.Length );

And you can read a Unicode string out of the buffer like this:

String fileName = Marshal.PtrToStringUni( _fileNameBuffer );

All this means you have a class that holds references to unmanaged memory. What do we do when our class holds unmanaged resources? That's right: We implement IDisposable. Make sure your Dispose and finalize routines free the unmanaged memory buffers properly; once again, use the Marshal class for this.

Finding Control IDs

Each time the File Open dialog box is resized, you need to lay out the contents accordingly. The code sample makes the "content" window (which lists the files and folders) narrower, and places the user-supplied control to the right of it. The problem is: How can you get hold of that content window? How can you get hold of a child window that you didn't create?

Every child window (or control) on a dialog is identified by a numeric control ID. As part of the common dialogs' extensibility mechanism, all the control IDs used in the dialogs are published in a Platform SDK header file called dlgs.h. That's great for C++ developers, but it's not immediately useful to us from managed code. Importantly though, it gives us the confidence to use these control IDs knowing that they are published, not simply magic numbers that could change over time. Now, how can you find the control ID for a particular child window on a common dialog?

Since the early days of Visual C++, the Microsoft tool set has shipped with a very useful little application called Spy++. In these days of managed code and Windows Forms, you may wonder why we still ship this tool. Here's why.

First, run up any application that uses the File Open common dialog box and open that dialog on your screen, then start Spy++ and point it at the "Open" dialog. Spy++ can show you all the details of that dialog, and all of its child windows. Figure 3 shows how it looks when I run it against the accompanying code sample.

Figure 3. How Spy++ shows the Open File dialog

Next, home in on the child window that says "SHELLDLL_DefView", right-click it, and then select Properties. Figure 4 shows the result on my system.

Figure 4. Examining window properties in Spy++

Notice the value near the bottom, called "Control ID". That's where I found the number 0x0461. Using this number, with the GetDlgItem API, you can reliably refer to this child window from your code. If you need to modify any of the other controls in a common dialog box, you can use the same technique.

How to Consume the Sample Open File Dialog

So much for how it works; what if you just want to consume the Open File dialog class that's in the accompanying code sample? All you need to do is supply the control(s) that will be dropped into the dialog, and handle the "selection-changed" event. Let's look at that it in more detail.

Creating an Instance of the Open File Dialog

The constructor for OpenFileDialog takes all the parameters required to configure the dialog. Only two of these parameters need a detailed explanation here:

  • String filter. This parameter tells the common dialog which files are permissible to be displayed in the content window. It's actually a list of string pairs, separated by a null character, where the first string in each pair is descriptive text that appears in the Files of type drop-down list, and the second describes the format of acceptable file names, using the usual wildcards. The exact format of this parameter is described in the documentation for the OPENFILENAME structure.
  • Control userControl. Your client application passes in the control that you want placed inside the Open File dialog box. Any Windows Forms control will do, but it makes sense to use a Panel: that way you can easily place other controls inside. The sample code inserts a Panel that contains a PictureBox control.

After you've constructed an instance of OpenFileDialog, you simply call its Show method. The Boolean return value is true if the user clicked OK, and false if the dialog was closed for any other reason. Once the dialog has been closed, your code can get hold of the full path for the selected file by accessing the SelectedPath property.

Preparing Your Own Controls for Inclusion in the Dialog

You must instantiate all the controls you want to insert and set up their initial properties and relationships to each other, before you create the Open File dialog box. Here's how the code sample sets up the PictureBox control inside the dialog box:

// Create panel for the "preview" part of the dialog
Panel p = new Panel();
p.BorderStyle = BorderStyle.Fixed3D;

// Add a picture box to the "preview" panel
_picBox = new PictureBox();
p.Controls.Add( _picBox );
_picBox.Dock = DockStyle.Fill;
_picBox.SizeMode = PictureBoxSizeMode.StretchImage;
_picBox.Click += new EventHandler( picBox_Click );

Notice how you can respond to events raised on the controls you place inside the dialog. You simply attach an event handler to the required event(s) before instantiating the dialog.

Summary

This article and the accompanying code sample show that you can still take advantage of an extensibility mechanism that was designed before the advent of managed code. Alternatively, you could build your own File Open dialog box from scratch, but there is enough behavior inside the Common Dialog Box Library to make reuse very much worthwhile.

 

About the Author

Martin is an Application Development Consultant working for Microsoft in the UK, specializing in Windows Forms, smart clients, and Web services. When away from the computer, Martin is a keen amateur jazz pianist.