Customizing the Explorer Open Dialog
The secret is in OFN_EXPLORER
Al Williams
Al, a consultant specializing in software development, training, and documentation, is the author of Steal This Code! (Addison-Wesley, 1995). You can find Al on the Web at http://ourworld .compuserve .com/ homepages/Al_Williams.
One of the most radical changes Windows 95 makes to the traditional Windows user interface is the common file-open dialog. Instead of the old standard, Windows 95 uses a special open dialog that looks like a miniature version of the Explorer (see Figure 1). From this dialog, users can rename files, create directories, and (of course) open files.
Customizing the common dialogs has always been a hassle. Now that the open dialog looks different, the steps you take to customize it are very different, too. In this article I'll explain how to customize the new dialog, and show an example component for Borland's Delphi 2.0. Even if you don't use Delphi, you can still apply these ideas to any other 32-bit language.
Making the Dialog Appear
If you don't take any special steps in a Windows 95 program, you'll get the same old open box that Windows 3.1 and Windows NT use. The secret to making the new dialog box appear is to specify OFN_EXPLORER as a flag in the OPENFILENAME structure. If you are using Delphi, this occurs automatically (unless you have NewStyleControls set to False). If you want the new-style dialog, you don't need to do anything-unless, of course, you want to customize the dialog box.
Many applications need a customized dialog box. You might want to display information about the selected file, or a preview of the file. I like to show the entire path in the open dialog. It is peculiar that the new open dialog doesn't show the entire path to the file in an easy-to-read format. The sample program I present with this article (available electronically) opens BMP, WMF, or ICO files. While opening the file, you'll see the entire path you are browsing. You'll also be able to click a preview button to see a thumbnail of the graphic.
Using a Hook Function
The first step in customizing the open dialog is learning about important events that occur within the dialog. You can install a hook function that acts like an ordinary window procedure by setting the OFN_ENABLEHOOK flag and placing the address of the hook in the lpfnHook field of the OPENFILENAME structure. The hook function looks like an ordinary window procedure. It receives a window handle argument, a message, and the ubiquitous wParam and lParam parameters.
In a hook function, you'll most often want to process WM_NOTIFY messages. This message (which is new to Windows 95) is how many controls notify your program of events. In contrast, if a standard edit control runs out of memory, it sends your program a WM_COMMAND message. That doesn't make sense. Common controls and the newer common dialogs that need to send information that isn't really a command use WM_NOTIFY.
All WM_NOTIFY messages come with a pointer to a structure in lParam. However, this presents a classic chicken-and-egg dilemma. The type of the structure depends on the type of the notification. However, the control embeds the type of the notification in the structure. Confusing, isn't it? The trick is that all notification headers start with the same few bytes (a NMHDR structure; see Table 1). You can treat a pointer to any notification structure as a pointer to a NMHDR. Then you can retrieve the code that identifies the type of the structure and recast the pointer. Object-oriented programmers will recognize this as a crude form of polymorphism.
Table 2 lists the notification codes you may receive from the open dialog. You won't often need to cast the notification structure since the only information sent, in most cases, is that the event occurred. The complete OFNOTIFY structure appears in Table 3.
Windows uses confusing names for some of the notification codes. For example, you might reasonably think that CDN_FOLDERCHANGE occurs when the user changes the current folder. It does do that, but you'll also receive the same notification as the dialog opens to inform you of the initial folder. This is a good thing, of course, but the name is a bit misleading.
Another notification that may not be obvious is CDN_INITDONE. This notice indicates that the dialog has finished setting up its controls. This is a good time for you to initialize your custom controls since you can count on the contents of the original dialog controls.
Customizing the Resource
To add your own controls to the open dialog, you'll need to construct a dialog template. The best way to do this is with a design tool such as Borland's Resource Workshop (which doesn't come with Delphi), or any of Microsoft's C++ or SDK offerings. However, if you don't have access to these, you can construct one by hand using an ASCII text file (look in some of the older Windows programming books for details on how to manually construct RC files).
Your dialog template will contain any additional controls you want to surround the open dialog. You'll also add a control (it doesn't matter what kind) with a special ID of $45F. This control sets the location of the standard open dialog controls. The top left of the special control will be the top left of the open dialog. The system will keep the same distance from the bottom right of the special control to the other controls and the edge of the dialog.
This is easier to observe than to explain. Look at Figure 2, which is a dialog template that contains several controls. Note the relative position between the placeholder control (the empty rectangle) and the rest of the dialog. Figure 3 shows how the open dialog will use this template. Contrast this with Figures 4 and 5. Notice that the dialog template creates the placeholder control without the WS_VISIBLE style. This hides the control. If you don't hide it, it will show up on top of the standard controls.
Your custom dialog template should have the following styles set: DS_CONTROL, DS_3DLOOK, WS_CHILD, and WS_CLIPSIBLINGS. Since several of these styles are new to Windows 95, you may have to add them manually to your RC file (see the listings, available electronically, for an example).
Once you have a resource, you need to do four things to make the open dialog use it:
1. Set the OFN_ENABLETEMPLATE bit in the OPENFILENAME flags.
2. Place the resource name in the lpTemplateName field (also in OPENFILENAME).
3. Set your program's instance handle in the hInstance field.
4.Include the resource file in your project. How you do this depends on the language you are using.
To decorate the open box with static text or other nondynamic controls, just supply a new template. You'll only bother with a hook function if you need to interact with the dialog.
In the more usual case, you'll want to install a hook function and initialize your controls when the dialog sends the CDN_INITDONE notification. If you have any buttons or other controls that send messages, you'll need to process those commands in the hook function also.
Controlling the Dialog
It's easy to manipulate your custom controls in the hook function. The hook function receives a window handle as an argument, but that handle isn't the dialog itself. Instead, it is a child window that represents your customizations. This is true even if you don't provide a customized dialog template.
To interact with the existing controls, you'll need to send messages to the dialog box itself. Table 4 shows the messages you can send to the dialog box. Some of these messages return information; others control the appearance of the dialog.
Of course, you can't send any of these messages to the dialog before it exists or after it has closed. You'll often use these messages while processing a WM_COMMAND message or a notification event from the dialog itself.
A Delphi Component
If you've used the ordinary Delphi file-open component, you'll shudder to think of writing all the code to customize a bare file-open component. However, with a little effort, you can write the ugly mess once and create your own customizable open component, such as CustDlg.PAS (available electronically).
The TCustomOpenDialog component derives directly from TCommonDialog, so it must duplicate much of the code that already exists in TOpenDialog. Although it would have been nice to subclass TOpenDialog, Borland made much of that object private. Without protected members to use, TCustomOpenDialog would have to override all the important functions anyway.
All is not lost, however. Instead of subclassing the object, I simply copied and modified its source code. While this isn't very efficient, it does work. I removed some extraneous code, changed all the names, and added the custom code required.
Component Highlights
The new component differs from the standard component in three ways:
- The Execute function is different.
- The component adds several new properties and methods.
- The component provides several custom events.
If you aren't programming in Delphi, pay close attention to the DoExecute procedure and the ExplorerHook function. DoExecute is the procedure responsible for creating the dialog box. It sets up the OpenFilename structure with the required fields (mostly from design-time properties). The routine forces the dialog type to OFN_EXPLORER since only this type of dialog works with the customization techniques used. The procedure also sets ExplorerHook as the hook, and if you specify a dialog template ID, it stores that in lpTemplateName and sets the OFN_ENABLETEMPLATE flag. Notice that you don't have to supply a dialog template ID if you don't want to change the dialog's appearance.
The ExplorerHook function is a simple window procedure. It handles four messages: WM_INITDIALOG, WM_NOTIFY, WM_COMMAND, and WM_NCDESTROY. Because the function is a window procedure, it isn't part of any particular object. This poses a problem, since the code needs to examine properties and values from the object in use. If you only use one open dialog, that's not a problem-you could hard code the value or store it in a global variable. However, if you create multiple objects, this wouldn't work well.
To solve this, DoExecute stores a pointer to the current object (Self) in the dialog structure's lCustData field. This is a 32-bit integer that your program can use to store data. During WM_INITDIALOG, the lParam parameter points to the dialog structure. ExplorerHook learns the value of lCustData and attaches it to the dialog window with a window property. It also initializes the dialog object's Wnd property at the same time.
On every call to ExplorerHook, the code reads the property and converts it into a pointer to a TCustomOpenDialog object. When the code detects a WM_NCDESTROY (the last message a window receives), it removes the property.
The remaining code in ExplorerHook is responsible for routing WM_NOTIFY events and WM_COMMAND messages into user-defined events. The routine decodes the WM_NOTIFY code and passes information to the appropriate handler (see Table 5). Except for OnShareViolation, each event handler receives the sending object, the current window handle, the parent window handle, and a pointer to the dialog structure (not the notification structure). The OnShareViolation handler has all these parameters plus the name of the file that caused the violation.
When the default ExplorerHook function detects a notification of CDN_INITDONE, it centers the dialog. I changed that bit of code to respect a Boolean property named Center. This allows you to center the dialog if you like.
The user-defined command handler (OnCommand) gets control when you press a user-defined control. It gets the sending object, the current window handle, the parent window handle, and the control ID. You can use this function to respond to custom buttons or other controls.
The other interesting code in the object provides new properties and methods (see Table 6). Most of these are simple shortcuts for sending the dialog messages. For example, the GetSpec function supplies the Spec property. This simply retrieves the value from the dialog using CDM_ GETSPEC.
One final note on the procedures in the CustDlg unit: These routines rely on Delphi 2.0's ANSI string facility. These new strings (also known as "long strings") convert easily to the PChar type. This is handy because all of the Windows messages require PChars. If you were doing the same thing in an older dialect of Delphi, you'd need to make extensive use of StrPas and StrPCopy.
An Example
Available electronically is a sample program, Custopen, that uses the custom open dialog to view bitmaps, metafiles, and icons (see Figure 1). When you bring up the open dialog, it contains a preview button. Pressing that button shows a preview of the file. The dialog also shows the current directory path and contains some custom text near the top of the box.
You might consider putting the preview window directly on the dialog box. That is possible, but the component used for the preview window is a Delphi component and is difficult to store in a resource template. Instead, you would have to create a placeholder in the template and create the component dynamically at run time.
If you borrow the PREVIEW unit to use in your own code, you should be aware of how it handles exceptions. If the user selects a nongraphic file and clicks preview, an exception occurs. To prevent this from being a problem, I surrounded the offending code in NewPreview with a try block to catch exceptions. However, I don't distinguish between different types of exceptions-I ignore them all. If you were using the code where you might expect different types of exceptions, you'd want to spruce up the exception handling.
I used the standard MDI project to start the program in MAIN.PAS (available electronically). In the CreateMDIChild procedure I added a call to load an image component with the specified file name. The other change required is to FileNewItemClick. I changed this routine to use a TCustomOpenDialog object on the form.
To get the custom dialog template into the project, the MAIN.PAS file has the directive {$R CUSTDLG.RES}. I created CUSTDLG.RC using Borland's resource workshop and compiled it with BRC32 (the Borland resource compiler). The other functions I added to MAIN.PAS are CustomOpenDialog1Command (to handle the preview button), CustomOpenDialog1FolderChange (to update the current directory display), and CustomOpenDialog1InitDone (to change the Cancel button's text). These functions are straightforward.
The only thing to remember as you browse this code is that the dialog template is not a Delphi form; therefore, to set the directory text, the program uses SetDlgItemText. In a language like C, the resource compiler uses the same symbolic constants that the C compiler uses, so you use symbolic names for the control IDs. Although you can create const values for the control IDs, you would have to manually manage them. Rather than bother with that, I simply used constants (99 is the directory name, 100 is the preview button).
In a similar vein, I specified the dialog template by name, not by number. That way, you can simply supply a string to the dialog component. How you do this depends on which dialog editor you use. When you enter the dialog's name with Borland's Resource Workshop, the program will ask you if you want to create a symbol of that name. Just say no. Microsoft's tools will automatically create symbols unless you surround the name with double quotes. As a last resort, you could find the .H file that the RC file includes, edit it, and remove the #define symbol that defines the dialog name.
In the online listings, you'll also find SIMPLE.PAS. This program doesn't do anything except bring up a custom open dialog. Unlike MAIN, SIMPLE doesn't supply a custom dialog template.
Wrap-Up
Using a custom open dialog from Delphi requires some decidedly nonvisual programming. However, if you are careful, you can write this type of code once and encapsulate it in a visual way.
If you are thinking of using any of the messages or techniques in this article with the old-style file-open dialog, forget it. These things only work when OFN_ EXPLORER is set in the options. I haven't decided if I prefer using the new method to customize dialogs or the old method. One thing I have decided is that it is definitely different.
Table 1: NMHDR structure.
Name Type Description hwndFrom HWND Window handle of control or dialog idFrom Unsigned Integer ID of control or dialog code Unsigned Integer Type of notification
Table 2: Open dialog notification codes.
Name Description CDN_INITDONE Initialization complete CDN_SELCHANGE Selection changed CDN_FOLDERCHANGE Folder changed (see text) CDN_SHAREVIOLATION Sharing violation occurred CDN_HELP Help requested CDN_FILEOK User pressed OK CDN_TYPECHANGE User changed file type
Table 3: OFNOTIFY structure.
Field Type Description hdr NMHDR (see Table 1) Standard header lpOFN Pointer to OPENFILENAME Dialog structure pszFile Pointer to Char Name of file if hdr.code = CDN_SHAREVIOLATION
Table 4: Open dialog messages.
Message wParam lParam Description CDM_GETSPEC Size of Pointer to Get current file spec character buffer character buffer CDM_GETFILEPATH Size of Pointer to Get current file and path character buffer character buffer CDM_GETFOLDERPATH Size of Pointer to Get current folder name character buffer character buffer CDM_GETFOLDERIDLIST Size of Pointer to Shell ID list of items character buffer character buffer CDM_SETCONTROLTEXT ID of control Pointer to Sets control text character buffer CDM_HIDECONTROL ID of control Not used Hides specified control CDM_SETDEFEXT Not used Pointer to Sets default character buffer extension (no '.')
Table 5: Component event handlers.
Handler Corresponding Message or notification OnInitDone CDN_INITDONE OnFileOK CDN_FILEOK OnFolderChange CDN_FOLDERCHANGE OnHelp CDN_HELP OnSelChange CDN_SELCHANGE OnTypeChange CDN_TYPECHANGE OnShareViolation CDN_SHAREVIOLATION OnCommand WM_COMMAND
Figure 1: The example program.
Figure 2: Typical resource template.
Figure 3: Dialog generated using the template in Figure 2.
Figure 4: Resource template with a small placeholder.
Figure 5: Open dialog generated from the resource template in Figure 4.
Table 6: New component methods and properties.
Name Type Description
Wnd Property Contains window handle of user dialog
FolderPath Property Corresponds to CDM_GETFOLDERPATH (read-only)
FilePath Property Corresponds to CDM_GETFILEPATH (read-only)
Spec Property Corresponds to CDM_GETSPEC (read-only)
DefExt Property Corresponds to CDM_SETDEFEXT (write-only)
HideControl Procedure Corresponds to CDM_HIDECONTROL
SetControlText Procedure Corresponds to CDM_SETCONTROLTEXT
TemplateName Property Name of custom template
Center Property Determines if dialog auto-centers