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

A class based on CFileDialog that provides easy image preview

0.00/5 (No votes)
22 Mar 2004 2  
Browse your images with a file open dialog and see what you've selected

Sample Image - imagepreviewdialog.jpg

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

    //  Do something with pWnd...

    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)
        {
            //  It's a valid listview index so we attach the

            //  the handle of the window that sent the message

            //  to a local CListCtrl object for easy access.

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

            //  Update our parent window

            if (m_pOwner->m_nPrevSelection != pLVHdr->iItem)
            {
                m_pOwner->UpdatePreview(csTemp);
            }

            //  Be certain we detach the handle before the CListCtrl

            //  object goes out of scope (else the underlying List View

            //  will be deleted, which is NOT what we want).

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

    //    Provide a big buffer for returned filenames

    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:
        //  Once we get this notification our old subclassing of

        //  the SHELL window is lost, so we have to

        //  subclass it again. (Changing the folder causes a 

        //  destroy and recreate of the SHELL window).

        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
// Initialize GDI+

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)
{
    // determine size of OPENFILENAME struct if dwSize is zero

    if (dwSize == 0)
    {
        OSVERSIONINFO vi;

        ZeroMemory(&vi, sizeof(OSVERSIONINFO));
        vi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
        ::GetVersionEx(&vi);

        // if running under NT and version is >= 5

        if (vi.dwPlatformId == VER_PLATFORM_WIN32_NT && vi.dwMajorVersion >= 5)
            dwSize = sizeof(OPENFILENAME);
        else
            dwSize = OPENFILENAME_SIZE_VERSION_400;
    }

    // size of OPENFILENAME must be at least version 4

    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.

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