Introduction

I wanted to import images in various formats into my
application. These images could have come from a digital camera or from a
scanner and don't necessarily have intelligible names. Look at the filename in
the sample picture above. It doesn't exactly drip with meaning.

So it was obvious that I needed to show a preview image in the file import
dialog. As the user clicks on files in the ListView the preview control updates
to show the image. Naturally I turned to CodeProject to see if anyone had
already implemented such a beast. If they have I couldn't find it, so I rolled
my own.

Basics

My starting point was the MFC CFileDialog class
which is a wrapper around the Windows File Open common dialog. It's relatively
easy to extend this class if all you need are a few extra controls. You do this
by deriving a new class from CFileDialog, creating a dialog
template containing your extra controls and working some voodoo on the
OPENFILENAME structure embedded in the CFileDialog
class. (See MSDN for full details). (As a side note, it's important to remember
that your dialog template becomes a child window of the CFileDialog
object).

This is all well and good. I created my extra dialog template resource,
created my derived class, wired up the image preview
control[^], compiled it and away we went. Up comes the dialog with my
extra controls and... nothing!

Which shouldn't come as any surprise at all. I hadn't added a handler to trap
selection changes in the ListView control so there was no way to tell the image
preview control to show the image most recently selected.

I needed to find a way of intercepting message traffic from the ListView
control, detecting selection changes, determining which file was involved and
telling the image preview control to display it. This is done by 'subclassing'
the child window, which simply means that messages sent to the child window are
first passed to code we wrote. We examine the messages, act on the ones we're
interested in, pass others on to the original message handler for that child
window and (probably) pass even the messages we are interested in on to the
original message handler.

A diversion

As previously noted, the custom dialog template is a child
of the File Open dialog. Thus, if you're looking for standard controls on the
File Dialog you have to go to your parent window. An obvious place to do this is
in your OnInitDialog() function. Suppose you're looking for the
"Open" button. You fire up Spy++ and use it to determine the control ID for the
button, which is, unsurprisingly, 1 (IDOK). You might do it like
this:

And, when you compile and run, it works. So you apply the same methodology
and discover that the control ID for the ListView is also 1! How can this be?

This is because the ListView control isn't a direct child of the File Open
dialog. Using Spy++ reveals that it's a child of another window, of class
SHELLDLL_DefView with a child ID of 0x461. The
SHELLDLL_DefView in turn is a child window of the File Open
dialog. Ok, so we write this code to access the ListView...

CWnd *pWnd = GetParent()->GetDlgItem(0x461)->GetDlgItem(IDOK);

and it crashes. Yet the line looks like it's correct. But if you break it
down into the individual steps involved:

you very quickly discover that the line fetching a pointer to the Shell
Window (child ID 0x461) returns a NULL pointer. But hang on, we know the window
exists with that child ID and as a child of the File Open dialog - Spy++ proves
it.

Yes, it existed at the time you used Spy++. But if you set a breakpoint in
the OnInitDialog() function, get the Window Handle for the parent
and use Spy++ to examine the child windows at that point in the dialogs
lifecycle you'll discover that the Shell window hasn't yet been created.

We'll discuss how to solve this problem a little later.

Back on track

So we've determined that not all dialog controls exist at
the time your OnInitDialog() is called (but all of yours as defined
in your dialog template will exist by then). Some very short time later, in
terms of human response times, they exist and we can then go using them.

Intercepting notifications from the ListView

I added a class derived
from CWnd and, once I could access the
SHELLDLL_DefView child window, subclassed it to the derived class.
It's important to note that we subclass the parent of the ListView,
not the ListView itself. The notification messages we're interested
in are sent to the parent.

Let's look at the class first before we come to how (and when) we subclass
the SHELLDLL_DefView.

Once we've connected the SHELLDLL_DefView window handle
(subclassed it) to an instance of the CHookWnd class this function
will be called each time a child control of the SHELLDLL_DefView
window sends a WM_NOTIFY message to its parent. The
WM_NOTIFY message in turn has sub-messages which vary according to
the type of control. The sub-message we're interested in is the
LVN_ITEMCHANGED sub-message. As you can see we cast the
lParam message parameter to a pointer to an NMLISTVIEW
structure and examine the uChanged member. This contains a bunch of
flags which tell us which parts of the item were changed. We're interested in a
change in the items state. This means the item was selected where it wasn't
previously, or it's been deselected having been selected (and a few other things
like focus that I don't care about for the purpose of this dialog).

So if it's a notification we're interested in we attach the
ListView's window handle, conveniently passed to us in the
NMLISTVIEW structure, to a CListCtrl object, call
GetItemData() on that object and pass the item data through the
SHGetPathFromIDList() API to get the filename. When we've done that
we split the returned path into the filename and extension, recombine them and
tell our owner dialog object to update the preview window. Once that's done we
detach the ListView window handle from the CListCtrl object and
return FALSE. It's important that we return FALSE
because that ensures the WM_NOTIFY is passed on to the original
Windows Procedure of the SHELLDLL_DefView window.

A bug fixed

I used to get the filename directly from the List View text.
Works fine unless you run it on a default installation of Windows where file
extensions for known filetypes are hidden. Solving that little conundrum took
some headscratching and a search of MSDN. I found this[^] article by Paul DiLascia which describes an undocumented
trick to work around this problem. It hinges on the knowledge that the
itemdata for each entry in the ListView is actually a
PIDL containing the path and filename for this entry.

It's subclassing time

So how, and more importantly, when, do we subclass
the SHELLDLL_DefView? We've already established that we can't do it
during OnInitDialog(). You could use a timer but that's bad form.
The solution lies in studying the notifications the File Open common dialog
sends. This is probably a good time to acknowledge my debt to David Kotchan and
his excellent article Implementing
a Read-Only 'File Open' or 'File Save' Common Dialog[^].

During its lifecycle the File Open common dialog sends various notification
messages of its own to the dialog procedure. One of these notifications is the
CDN_INITDONE message, which tells us that the dialog has finished
it's initialisation. When you recieve this message you know that all the
controls, including the SHELLDLL_DefView window, have been created.
This seems like a good time to do our subclassing. And it will work, until the
user changes the directory! At which point the code will crash if all you've
done is handled the CDN_INITDONE message. This behaviour puzzled me
until I read David Kotchans article. It turns out that the
SHELLDLL_DefView window (and its child ListView
window) are destroyed and recreated each time the user navigates to a new
directory. So the proper place to perform the subclassing is when you recieve
the CDN_FOLDERCHANGE notification.

The first thing you'll notice is that I lied a little earlier. The
CHookWnd class is a private class nested within the
CImageImportDlg class. This makes sense because it's useless
outside the context of the dialog class and I don't want to instantantiate an
instance somewhere else due to faulty memory of it's purpose.

The CImageImportDlg constructor takes the same parameters as the
CFileDialog base class. Again, this makes sense because the class
is meant to be a drop-in replacement for CFileDialog. However,
since the class is also meant to allow the importing of images it makes sense
that it modify the behaviour of the base class to the extent that it's always a
multiple selection class. The constructor does this:

We modify the flags to allow for multiple selection of files (and as a
freebie we make the dialog resizable).

The voodoo I mentioned above for adding controls to the dialog is revealed
here. We set the OFN_ENABLETEMPLATE flag, set the
m_ofn.hInstance member variable to the instance handle of our
executable and set the m_ofn.lpTemplateName member variable to our
template identifier and let MFC take care of the rest.

We also need to provide a reasonably large buffer to contain the filenames
selected by the user. Fortunately the File Open common dialog assumes that it
need save only the filename and not include the path. You get the path when the
dialog's done by calling the GetPathName() function.

As you'd guess, this is intercepting WM_NOTIFY messages
directed at the dialog. Don't confuse this with the OnNotify()
handler in the CHookWnd class. That class catches messages sent to
the SHELLDLL_DefView window. The dialog procedure never sees those
messages. What this message handler sees are the CDN_?? messages
sent by the File Open common dialog inside Windows. The only one we're
interested in is the CDN_FOLDERCHANGE message. As noted above,
seeing this message means that the user has navigated to a new directory and, as
a consequence, the SHELL window has been destroyed and recreated. So the code
checks if we've already subclassed the window and if we have we unsubclass our
hook window and subclass it to the newly created SHELL window.

Danger Will Robinson!

Here comes the obligatory warning about using
undocumented stuff. I mention above that the SHELLDLL_DefView
window has a child ID of 0x461 and hinted that I found it by using Spy++. Your
hackles should be rising about now. That's a magic constant that might change in
future versions of Windows. And yes, it might. But consider that Microsoft ship
a file called dlgs.h in the Platform SDK which contains a whole
bunch of constants identifying windows in all sorts of common dialogs. Some of
these dialogs have been around since Windows 95 and the constants haven't
changed, so it's a reasonable guess that these values are set in stone at
Microsoft. (Indeed, those with an eye for detail might have noticed that the
line above which subclasses the SHELL window uses a constant, lst2,
rather than 0x461 and might have wondered where it came from).

Using the code

Add the four source code files in the download to your
project. Add these lines to the end of your stdafx.h file.

and remember to add a variable somewhere (accessible to application
initialisation and shutdown) as a

unsignedlong m_gdiplusToken;

Merge the dialog template in user_dialog_template.rc into
your projects .rc file. Once you've merged the template you'll need
to make sure the template ID and the two static control ID's are added to your
projects resource.h file. Then, where you'd use a
CFileDialog substitute a CImageImportDlg, compile and
you should be ready to go.

The sample project illustrates how to do this.

A bug found and fixed

Actually it's not a bug in my code at all but
it'll be percieved as such so it's up to me to fix it. As originally presented a
debug build of the class will ASSERT if the user changes the view type using the
rightmost button of the toolbar. MFC asserts (within the
OnCommand() handler for CWnd) that either the high
word of wParam is 0 (command came from a menu) or the
lParam is a valid window handle. For whatever reason the
CFileDialog doesn't observe this rule and MFC asserts. It's a
harmless error but it's a trifle disconcerting to have debug builds assert so I
fixed it by adding an OnCommand() handler to CHookWnd
which substitutes 0 for the wParam parameter. The bug arises
because once we subclass the SHELL window using MFC code all of it's messages,
even the ones we're not interested in, pass through MFC handlers.

Getting the places bar

You'll see in the screen shot at the start of the article that the dialog shows the places bar. I've had a couple of people ask how to get it. The answer's
really trivial and the reason why it frequently doesn't appear is interesting as well. I'm going to expand a little on a reply I made to a message on this
articles message board.

The answer first. Don't set the m_ofn.lStructSize member. Just accept the default set in the CFileDialog constructor. Then, if your
platform supports the places bar you'll get it. If not, not. Now for the why.

Most of the samples I've seen discussing extending the CFileDialog class contain a line in the derived classes constructor that goes like this.

m_ofn.lStructSize = sizeof(OPENFILENAME);

which, on the face of it seems reasonable. I've done it myself often enough. But let's look at the code buried inside the CFileDialog constructor.

(I've deleted a few lines that aren't relevant). The constructor does some OS version checking and sets the structure size accordingly. Eventually your code is going
to call the Windows Common Dialogs code passing the OPENFILENAME structure. That code changes it's behaviour based on various parameters passed to it,
including the structure size (which can be considered as a kind of versioning). Pass it a structure with an 'old' length and it will assume it's dealing with an 'old'
application and react accordingly.

But hang on! If it's Windows 2000 the code in the CFileDialog structure duplicates what I've been doing, assigning the sizeof(OPENFILENAME) to
the m_ofn.lStructSize member! Uh huh. Yet if I do precisely that on my system with the latest Platform SDK and using VS .NET 2003 I don't get the places
bar. I did some single stepping through the constructor and discovered that the MFC libraries assign a size of 0x58 to the structure member. Then, single stepping
through my derived class constructor (which runs, of course, after the base class) the structure size gets reset to 0x4c.

It seems that my system is picking up an older header file even if I have the most recent Platform SDK installed. *shrug* The workaround is to not assign a value to
the structure size in my constructor and to let CFileDialog do it for me. It's not that serious a workaround anyway. It's not as if it was ever necessary
to set the structure size, given that it's set by the base class.

History

9 March 2004 - Initial version

10 March 2004 - Fixed an ASSERT bug.

12 March 2004 - Changed the way we get the filename from the ListView control.

Share

About the Author

I've been programming for 35 years - started in machine language on the National Semiconductor SC/MP chip, moved via the 8080 to the Z80 - graduated through HP Rocky Mountain Basic and HPL - then to C and C++ and now C#.

I used (30 or so years ago when I worked for Hewlett Packard) to repair HP Oscilloscopes and Spectrum Analysers - for a while there I was the one repairing DC to daylight SpecAns in the Asia Pacific area.

Afterward I was the fourth team member added to the Australia Post EPOS project at Unisys Australia. We grew to become an A$400 million project. I wrote a few device drivers for the project under Microsoft OS/2 v 1.3 - did hardware qualification and was part of the rollout team dealing directly with the customer.

Born and bred in Melbourne Australia, now living in Scottsdale Arizona USA, became a US Citizen on September 29th, 2006.

I work for a medical insurance broker, learning how to create ASP.NET websites in VB.Net and C#. It's all good.

Comments and Discussions

When I changed all control style to WS_EX_LAYOUTRTL for Arabic language, then the control "cmb13" which is a combo for "Drop-down combo box that displays the name of the current file, allows the user to type the name of a file to open, and select a file that has been opened or saved recently" does not look proper on Window XP machine.

Since m_ofn.hInstance is a handle to a memory object containing a dialog box template, you should use AfxGetResourceHandle() instead of AfxGetInstanceHandle() in dialog constructor. If you have resources in separate dll, AfxGetInstanceHandle will not work..

This killed several hours for me, so I'm posting to relevant articles here. The return value of CFileDialog::OnFileNameOK() is backwards: TRUE = keep dialog open, FALSE = close it (see MSDN Library). If you override CFileDialog::OnFileNameOK() to validate your controls, do something like the following:

Hi:
I want to further gain the data type that means are when select an recent item in 'File name' combobox, meanwhile, I need to get the selected item's data type so as to do something, I have try some 'SHELL Structures', How do you think?

I have no problem adding standard MFC controls to the CPreviewFileDlg class and accessing/using them. However, I'm not able to have it display/use ActiveX elements. They either don't show in the dialog (when the dialog is set to ignore contruction errors), or the dialog isn't constructed at all (otherwise).

The same ActiveX control works perfectly in classes derived from CDialog, but once I derive from CFileDialog, they are "lost".

Rob
Do you have any pointers on how to use your code in a VB.NET project?
I am porting a Windows app to ASP.NET. The users need to upload a lot of information from existing files on their own hard drive.

What I'm trying to do is add a custom area to the places bar and add a dialog underneath the folder view. To do this I have created a custom class that is derived from the CommonDialog class, I now need a couple of pointers on how to actually modify the design of the dialog. Do I create a form with the extra controls I need then sit the file dialog on that or do I have to get hold of the file dialog itself and resize and add stuff to it?

Hope you can help

Jason

Jason Brown
i2 Limited

There are 10 types of people in this world, those who understand binary and those who don't