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).
Once you've created your derived class you add message map members to your
derived class to handle messages coming from your child controls in your dialog
template and, if necessary, add data members to access your controls via DDX.
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:
void CImageImportDlg::OnInitDialog()
{
CFileDialog::OnInitDialog();
CWnd *pWnd = GetParent()->GetDlgItem(IDOK);
return TRUE;
}
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:
CWnd *pShellWnd = GetParent()->GetDlgItem(0x461);
CWnd *pListViewWnd = pShellWnd->GetDlgItem(IDOK);
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
.
class CHookWnd : public CWnd
{
public:
void SetOwner(CImageImportDlg *m_pOwner);
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
private:
CImageImportDlg *m_pOwner;
};
This is a very simple class.
SetOwner()
simply copies a
pointer to our dialog object into the
m_pOwner
variable.
OnNotify()
looks like this.
BOOL CHookWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)
{
LPNMLISTVIEW pLVHdr = reinterpret_cast<LPNMLISTVIEW>(lParam);
if (pLVHdr->hdr.code == LVN_ITEMCHANGED && (pLVHdr->uChanged & LVIF_STATE))
{
if (pLVHdr->iItem != -1)
{
CListCtrl ctl;
LPCITEMIDLIST pidl;
TCHAR tszBuffer[_MAX_PATH],
tszFileName[_MAX_PATH],
tszExtension[_MAX_EXT];
CString csTemp;
ctl.Attach(pLVHdr->hdr.hwndFrom);
pidl = (LPCITEMIDLIST) ctl.GetItemData(pLVHdr->iItem);
SHGetPathFromIDList(pidl, tszBuffer);
_tsplitpath(tszBuffer, NULL, NULL, tszFileName, tszExtension);
csTemp.Format(_T("%s%s"), tszFileName, tszExtension);
if (m_pOwner->m_nPrevSelection != pLVHdr->iItem)
{
m_pOwner->UpdatePreview(csTemp);
}
ctl.Detach();
}
}
*pResult = 0;
return FALSE;
}
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.
So let's look at the dialog class and see how this all fits together.
The CImageImportDlg class
class CImageImportDlg : public CFileDialog
{
class CHookWnd : public CWnd
{
public:
void SetOwner(CImageImportDlg *m_pOwner);
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
private:
CImageImportDlg *m_pOwner;
};
DECLARE_DYNAMIC(CImageImportDlg)
public:
CImageImportDlg(LPCTSTR lpszDefExt = NULL,
LPCTSTR lpszFileName = NULL,
DWORD dwFlags =
OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
CWnd* pParentWnd = NULL);
virtual ~CImageImportDlg();
virtual void DoDataExchange(CDataExchange *pDX);
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
void UpdatePreview(LPCTSTR szFilename);
protected:
CImagePreviewStatic m_preview;
CString m_csPreviewName;
int m_nPrevSelection;
CHookWnd m_wndHook;
virtual BOOL OnInitDialog();
};
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:
CImageImportDlg::CImageImportDlg(LPCTSTR lpszDefExt, LPCTSTR lpszFileName,
DWORD dwFlags, CWnd* pParentWnd)
: CFileDialog(TRUE, lpszDefExt, lpszFileName, dwFlags, szFilter, pParentWnd)
{
m_ofn.Flags |= OFN_ENABLETEMPLATE | OFN_ALLOWMULTISELECT | OFN_ENABLESIZING;
m_ofn.hInstance = AfxGetInstanceHandle();
m_ofn.lpTemplateName = MAKEINTRESOURCE(IDD_IMAGEPREVIEWDLG);
m_ofn.lpstrFile = new TCHAR[10000];
m_ofn.nMaxFile = 10000;
memset(m_ofn.lpstrFile, 0, countof(m_ofn.lpstrFile));
}
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.
OnNotify()
looks like this:
BOOL CImageImportDlg::OnNotify(WPARAM, LPARAM lp, LRESULT *pResult)
{
LPOFNOTIFY of = (LPOFNOTIFY) lp;
CString csTemp;
switch (of->hdr.code)
{
case CDN_FOLDERCHANGE:
if (m_wndHook.GetSafeHwnd() != HWND(NULL))
m_wndHook.UnsubclassWindow();
m_wndHook.SubclassWindow(GetParent()->GetDlgItem(lst2)->GetSafeHwnd());
break;
}
*pResult = 0;
return FALSE;
}
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.
#define countof(x) (sizeof(x) / sizeof(x[0]))
#include <GdiPlus.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
Add this code to your application initialisation
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
and this code to your application shutdown.
GdiplusShutdown(m_gdiplusToken);
and remember to add a variable somewhere (accessible to application
initialisation and shutdown) as a
unsigned long 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.
CFileDialog::CFileDialog(bunch of parameters)
{
if (dwSize == 0)
{
OSVERSIONINFO vi;
ZeroMemory(&vi, sizeof(OSVERSIONINFO));
vi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
::GetVersionEx(&vi);
if (vi.dwPlatformId == VER_PLATFORM_WIN32_NT && vi.dwMajorVersion >= 5)
dwSize = sizeof(OPENFILENAME);
else
dwSize = OPENFILENAME_SIZE_VERSION_400;
}
ASSERT(dwSize >= OPENFILENAME_SIZE_VERSION_400);
m_ofn.lStructSize = dwSize;
}
(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.
23 March 2004 - Added a wishy washy explanation of why the places bar sometimes doesn't appear.