YABFFW: Yet Another BrowseForFolder Wrapper

Environment: VC6, VC7, Win2K

Quick Overview

YABFFW (Yet Another BrowseForFolder Wrapper) is a CWnd subclass wrapping the Shell API SHBrowseForFolder() in an MFC-friendly way. There are certainly others out there, but the approach taken here (in my opinion) integrates more cleanly into MFC apps, and makes it easier to customize the appearance and behavior of the dialog box.

Why Another BFF Wrapper?

Sooner or later, we all need to display a dialog allowing the user to select a directory. A search in MSDN turns up the function ::SHBrowseForFolder() (<shlobj.h>). The next step is usually to either write or steal a C++ class that wraps up the C-style function call in a more MFC-friendly way.

At least, that’s what happened to me. In my case, I needed to customize the dialog a bit, too. Specifically, I wanted to add a little checkbox labelled “Recurse” to it, and retrieve that checkbox’s value after the user dismissed the dialog. So, I started surfing the Web looking for examples. I found several (C++ wrappers as well as examples of customizing the “Browse for Folder” dialog), and happily started copying them. However, as I implemented the code, I began to see a cleaner and more extensible way to accomplish my task. CYABFFW is the product of that process.

So How Does It Work?

What makes CYABFFW different is that it is a CWnd that subclasses the “Browse for Folder” dialog right after creation. This gives the class access to things such as Message Maps, CObject diagnostics, DDX/DDV, and so on. It also gets us out of calling ::SetWindowLong, writing a wndproc, and so on. Of course, it commits you to MFC; if you’re not using MFC, CYABFFW won’t be much use to you. The source code appears below (though of course you can download it along with a sample project).

How Do I Use It?

In the simplest case, CYABFFW looks and acts just like most any MFC dialog class. You instantiate it on the stack, call DoModal(), and check the return value to see what the user did. Once the user clicks “OK,” you can retrieve the item they selected by calling either GetPath() or GetItemIdList(), depending on whether you want the selection as a path or an ITEMIDLIST.

For example:

CYABFFW dlg();
if (IDOK == dlg.DoModal())
{
    CString s = dlg.GetPath();
    // Do something with 's' ...

SHBrowseForFolder() allows quite a bit of customization of the dialog’s appearance and behavior. You do this by filling out a BROWSEINFO struct that is passed to the function. I’ve tried to expose all the functionality provided by SHBrowseForFolder(), just in a way more familiar to C++ programmers.

The first point of extension is the CYABFFW constructor. There are several constructors, each taking an array of parameters that control the resulting dialog. For a full list, refer to the source code, or the documentation below. Here’s an example, however:

CYABFFW dlg(_T("Please select a directory"),  // Hint to user
            BIF_USE_NEWUI,                    // Flags for the dlg
            this,                             // Parent window
            CSIDL_DRIVES);                    // Root of search
if (IDOK == dlg.DoModal())
{
    CString s = dlg.GetPath();
    // Do something with 's' ...

If you want to customize the dialog beyond what you can do with arguments to the constructor, you’ll need to subclass CYABFFW. CYABFFW defines three virtual functions that can be overridden: OnInitBFFDialog(), OnBFFSelChanged(), or OnBFFValidateFailed(). These correspond to the custom messages SHBrowseForFolder() sends to its optional callback functions BFFM_INITIALIZED, BFFM_SELCHANGED, and BFFM_VALIDATEFAILED. Of course, you can also Mesage Map entries to your subclass and process any Windows Messages you’re interested in. The demo project (see below) includes an example of doing this to add my “Recurse” checkbox.

The Code

YABFFW.h:

/**
 * \class CYABFFW
 *
 * \brief MFC Dialog wrapping \c ::SHBrowseForFolder
 *
 *
 * CYABFFW implements a different approach to wrapping \c
 * SHBrowseForFolder in an MFC-friendly way.  It actually
 * subclasses \c CWnd, allowing you to hook into the MFC Message
 * Mapping mechanism for customization.  Naturally, it also takes
 * care of some unpleasant chores associated with with \c
 * SHBrowseForFolder, such as translating from an \c LPITEMIDLIST
 * to a CString, and so on.
 *
 * Use it just like you would any other MFC-based Dialog:
 *
 \code
   void CMyClass::CMyHandler()
   {
     YABFFW dlg(_T("Choose a folder"), BIF_USENEWUI, this,
                CSIDL_DESKTOP);
     if (IDOK == dlg.DoModal())
     {
       CString s = dlg.GetPath();
       // Do something with 's' ...
     }
 \endcode
 *
 *
 */

class CYABFFW : public CWnd
{
// Construction
public:
/// Construct a CYABFFW dialog
CYABFFW(const CString &strHint = CString(), UINT nFlags = 0U,
CWnd *pParentWnd = NULL,
int nRootFolder = CSIDL_DESKTOP);
/// Load the “hint” from Resource
CYABFFW(int nHint, UINT nFlags = 0U, CWnd *pParentWnd = NULL,
int nRootFolder = CSIDL_DESKTOP);
/// Construct a CYABFFW dialog rooted at an arbitrary folder
CYABFFW(const CString &strHint, const CString &strRoot,
UINT nFlags = 0U, CWnd *pParentWnd = NULL);
/// Load the “hint” from Resource, but root the browse
/// operation at an arbitrary folder

CYABFFW(int nHint, const CString &strRoot, UINT nFlags = 0U,
CWnd *pParentWnd = NULL);

// Attributes
public:
/// Retrieve the display name of the selected item
CString GetDisplayName() const;
/// Retrieve the ITEMIDLIST of the selected item
LPCITEMIDLIST GetItemIdList() const;
/// Retrieve the path to the selected item
CString GetPath() const;

// Operations
public:
/// Display the Dialog – returns IDOK or IDCANCEL
int DoModal();
/// En/Dis-able the “OK” button
void EnableOk(BOOL bEnable);
/// Set the current selection in the tree control
void SetSelection(LPCITEMIDLIST pItemIdList);
/// Set the current selection in the tree control
void SetSelection(const CString &strPath);
/// Set the “hint” text
void SetText(const CString &strNewText);

// Overrides
public:
/// Called when the BFF Dialog has initialized
virtual void OnInitBFFDialog();
/// Called when the selection has changed in the tree control
virtual void OnBFFSelChanged(LPITEMIDLIST pNewSel);
/// Called when the user types an invalid name
virtual BOOL OnBFFValidateFailed(const CString &strBadName);

// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CYABFFW)
//}}AFX_VIRTUAL

// Implementation
public:
virtual ~CYABFFW();

// Generated message map functions
protected:
//{{AFX_MSG(CYABFFW) NOTE – the ClassWizard will add and remove
// member functions here.
//}}AFX_MSG

DECLARE_MESSAGE_MAP()

private:
/// Static method to be used for the SHBFF callback
static int CALLBACK BrowseCallbackProc(HWND hWnd, UINT nMsg,
LPARAM lParam,
LPARAM lpData);

private:
/// Free the memory referenced by m_pItemIdList
void FreeItemIdList(IMalloc *pMalloc = NULL);
/// CSIDL => ITEMIDLIST
LPITEMIDLIST ResolveCsidl(int nCsidl) const;
/// Textual filesystem path => ITEMIDLIST
LPITEMIDLIST ResolveFsPath(const CString &strPath) const;

private:
/// Display name of the selected item
CString m_strDisplayName;
/// Flags to be passed to the browse dialog
UINT m_nFlags;
/// “Hint” to be displayed above the tree control
CString m_strHint;
/// ITEMIDLIST identifying the selected Shell item
LPITEMIDLIST m_pItemIdList;
/// Parent CWnd (NULL => App main window)
CWnd *m_pParentWnd;
/// Selected path
CString m_strPath;
/// ITEMIDLIST identifying the root
LPITEMIDLIST m_pRoot;

};

/////////////////////////////////////////////////////////////////

#ifndef _DEBUG
inline CString CYABFFW::GetDisplayName() const
{ return m_strDisplayName; }

inline LPCITEMIDLIST CYABFFW::GetItemIdList() const
{ return m_pItemIdList; }

inline CString CYABFFW::GetPath() const
{ return m_strPath; }
#endif // not _DEBUG

YABFFW.cpp:

/**
 * \brief Construct a CYABFFW dialog
 *
 * \sa SetText
 *
 *
 * \param strHint Optional string to display in the dialog; can
 * be changed later by SetText
 *
 * \param nFlags Optional Bit vector made up of \c BIF_* values
 * controlling various aspects of the dialog's appearance &
 * behavior
 *
 * \param pParentWnd Points to the parent or owner window object
 * (of type CWnd) to which the dialog object belongs. If it is
 * NULL, the dialog objects parent window is set to the main
 * application window.
 *
 * \param nRootFolder Optional \c CSIDL value naming the root of
 * the tree that the user may browse
 *
 *
 */

CYABFFW::CYABFFW(const CString &strHint /*= CString()*/,
UINT nFlags /*= 0U*/,
CWnd *pParentWnd /*= NULL*/,
int nRootFolder /*= CSIDL_DESKTOP*/) :
m_nFlags(nFlags),
m_strHint(strHint),
m_pItemIdList(NULL),
m_pParentWnd(pParentWnd),
m_pRoot(NULL)
{
ASSERT_NULL_OR_VALID(pParentWnd); // Paranoia

m_pRoot = ResolveCsidl(nRootFolder);
}

/**
* \brief Load the “hint” from Resource
*
* \sa SetText
*
*
* \param nHint Resource Id of a string to be loaded and used in
* the dialog; can be changed later by SetText
*
* \param nFlags Optional Bit vector made up of \c BIF_* values
* controlling various aspects of the dialog’s appearance &
* behavior
*
* \param pParentWnd Points to the parent or owner window object
* (of type CWnd) to which the dialog object belongs. If it is
* NULL, the dialog objects parent window is set to the main
* application window.
*
* \param nRootFolder Optional \c CSIDL value naming the root of
* the tree that the user may browse
*
*
*/

CYABFFW::CYABFFW(int nHint,
UINT nFlags /*= 0U*/,
CWnd *pParentWnd /*= NULL*/,
int nRootFolder /*= CSIDL_DESKTOP*/) :
m_nFlags(nFlags),
m_pItemIdList(NULL),
m_pParentWnd(pParentWnd),
m_pRoot(NULL)
{
ASSERT_NULL_OR_VALID(pParentWnd); // Paranoia

if (!m_strHint.LoadString(nHint))
AfxThrowResourceException();

m_pRoot = ResolveCsidl(nRootFolder);
}

/**
* \brief Construct a CYABFFW dialog rooted at an arbitrary folder
*
* \sa SetText
*
*
* \param strHint String to display in the dialog; can be changed
* later by SetText.
*
* \param strRoot Path to a directory in the filesystem to root
* the browse operation at
*
* \param nFlags Optional Bit vector made up of \c BIF_* values
* controlling various aspects of the dialog’s appearance &
* behavior
*
* \param pParentWnd Points to the parent or owner window object
* (of type CWnd) to which the dialog object belongs. If it is
* NULL, the dialog objects parent window is set to the main
* application window.
*
*
*/

CYABFFW::CYABFFW(const CString &strHint,
const CString &strRoot,
UINT nFlags /*= 0U*/,
CWnd *pParentWnd /*= NULL*/) :
m_nFlags(nFlags),
m_strHint(strHint),
m_pItemIdList(NULL),
m_pParentWnd(pParentWnd),
m_pRoot(NULL)
{
ASSERT_NULL_OR_VALID(pParentWnd); // Paranoia

m_pRoot = ResolveFsPath(strRoot);
}

/**
* \brief Load the “hint” from Resource, but root the browse
* operation at an arbitrary folder
*
* \sa SetText
*
*
* \param nHint Resource Id of a string to be loaded and used in
* the dialog; can be changed later by SetText
*
* \param strRoot Path to a directory in the filesystem to root
* the browse operation at
*
* \param nFlags Optional Bit vector made up of \c BIF_* values
* controlling various aspects of the dialog’s appearance &
* behavior
*
* \param pParentWnd Points to the parent or owner window object
* (of type CWnd) to which the dialog object belongs. If it is
* NULL, the dialog objects parent window is set to the main
* application window.
*
*
*/

CYABFFW::CYABFFW(int nHint,
const CString &strRoot,
UINT nFlags /*= 0U*/,
CWnd *pParentWnd /*= NULL*/) :
m_nFlags(nFlags),
m_pItemIdList(NULL),
m_pParentWnd(pParentWnd),
m_pRoot(NULL)
{
ASSERT_NULL_OR_VALID(pParentWnd); // Paranoia

if (!m_strHint.LoadString(nHint))
AfxThrowResourceException();

m_pRoot = ResolveFsPath(strRoot);
}

#ifdef _DEBUG
CString CYABFFW::GetDisplayName() const
{
return m_strDisplayName;
}

/**
* \brief Retrieve the ITEMIDLIST of the selected item
*
*
* \return A constant pointer to an \c ITEMIDLIST naming the
* selected object.
*
*
* Note that the caller is \em not responsible for cleaning up
* this memory. The flip side is that this pointer’s validity is
* bounded by the lifetime of its owning CYABFFW instance.
*
*
*/

LPCITEMIDLIST CYABFFW::GetItemIdList() const
{
return m_pItemIdList;
}

CString CYABFFW::GetPath() const
{
return m_strPath;
}
#endif // _DEBUG

int CYABFFW::DoModal()
{
// We’ll need this eventually …
HRESULT hr;
IMalloc *pMalloc;
if (FAILED(hr = ::SHGetMalloc(&pMalloc)))
AfxThrowOleException(hr);

// Fill out a ‘BROWSEINFO’ structure to hand to ‘SHBrowseFor-
// Folder’:

BROWSEINFO browseInfo;
::ZeroMemory(&browseInfo, sizeof(BROWSEINFO));
browseInfo.hwndOwner = (NULL == m_pParentWnd ? NULL :
m_pParentWnd->m_hWnd);

browseInfo.pidlRoot = m_pRoot;

// Use a CString for memory management
browseInfo.pszDisplayName =
m_strDisplayName.GetBufferSetLength(MAX_PATH);

browseInfo.lpszTitle = m_strHint;
browseInfo.ulFlags = m_nFlags;
browseInfo.lpfn = BrowseCallbackProc;
browseInfo.lParam = (long)this;

if (NULL != m_pItemIdList)
FreeItemIdList(); // Probably never happen, but …

if (NULL == (m_pItemIdList = ::SHBrowseForFolder(&browseInfo)))
{
// User Cancelled out – clean up & bail.
m_strDisplayName.ReleaseBuffer();
pMalloc->Release();
return IDCANCEL;
}

// Right – if we’re here, the user actually selected an item.
// Try to get a full path. This will fail if the selected item
// is not part of the FileSystem.

::SHGetPathFromIDList(m_pItemIdList,
m_strPath.GetBufferSetLength(MAX_PATH));

// Cleanup time …
m_strPath.ReleaseBuffer();
m_strDisplayName.ReleaseBuffer();
pMalloc->Release();

// Note: m_pItemIdList has *not* been freed! We keep around in
// case the caller wants to retrieve it later. It will
// ultimately be freed in the destructor.

return IDOK;
}

void CYABFFW::EnableOk(BOOL bEnable)
{
ASSERT(NULL != m_hWnd);
SendMessage(BFFM_ENABLEOK, 0U, bEnable ? 1L : 0L);
}

void CYABFFW::SetSelection(LPCITEMIDLIST pItemIdList)
{
ASSERT(NULL != m_hWnd);
ASSERT_POINTER(pItemIdList, ITEMIDLIST);

SendMessage(BFFM_SETSELECTION, (WPARAM)FALSE,
(LPARAM)pItemIdList);
}

void CYABFFW::SetSelection(const CString &strPath)
{
ASSERT(NULL != m_hWnd);

SendMessage(BFFM_SETSELECTION, (WPARAM)TRUE,
(LPARAM)(LPCTSTR)strPath);
}

void CYABFFW::SetText(const CString &strNewText)
{
ASSERT(NULL != m_hWnd);

SendMessage(BFFM_SETSTATUSTEXT, 0U, reinterpret_cast<LPARAM>
(static_cast<LPCTSTR>(strNewText)));
}

void CYABFFW::OnInitBFFDialog()
{ /* No handling by default */ }

void CYABFFW::OnBFFSelChanged(LPITEMIDLIST /*pNewSel*/)
{ /* No handling by default */ }

/**
* \brief Called when the user types an invalid name
*
*
* \param strBadName Invalid item name provided by the user
*
* \return Return \c TRUE to indicate that the dialog may be
* dismissed, \c FALSE to force the dialog to remain displayed
*
*
*/

BOOL CYABFFW::OnBFFValidateFailed(const CString & /*strBadName*/)
{ return TRUE; /* No handling by default */ }

CYABFFW::~CYABFFW()
{
HRESULT hr;
IMalloc *pMalloc;
if (FAILED(hr = ::SHGetMalloc(&pMalloc)))
AfxThrowOleException(hr);

pMalloc->Free(m_pRoot);

FreeItemIdList(pMalloc);
}

BEGIN_MESSAGE_MAP(CYABFFW, CWnd)
//{{AFX_MSG_MAP(CYABFFW)
// NOTE – the ClassWizard will add and remove mapping macros
// here.
//}}AFX_MSG_MAP

END_MESSAGE_MAP()

int CYABFFW::BrowseCallbackProc(HWND hWnd,
UINT nMsg,
LPARAM lParam,
LPARAM lpData)
{
CYABFFW *pWnd = reinterpret_cast<CYABFFW*>(lpData);
ASSERT_VALID(pWnd);
ASSERT(NULL == pWnd->m_hWnd || hWnd == pWnd->m_hWnd);

if (NULL == pWnd->m_hWnd && !pWnd->SubclassWindow(hWnd))
AfxThrowOleException(HRESULT_FROM_WIN32(::GetLastError()));

switch (nMsg)
{
case BFFM_INITIALIZED:
// Indicates the browse dialog box has finished
// initializing. The lParam value is zero.

pWnd->OnInitBFFDialog();
return 0;
case BFFM_SELCHANGED:
// Indicates the selection has changed. The lParam parameter
// points to the item identifier list for the newly selected
// item.

{
LPITEMIDLIST p = reinterpret_cast<LPITEMIDLIST>(lParam);
ASSERT_POINTER(p, ITEMIDLIST);
pWnd->OnBFFSelChanged(p);
return 0;
}
case BFFM_VALIDATEFAILED:
// Indicates the user typed an invalid name into the edit box
// of the browse dialog box. The lParam parameter is the
// address of a character buffer that contains the invalid
// name. An application can use this message to inform the
// user that the name entered was not valid. Return zero to
// allow the dialog to be dismissed or nonzero to keep the
// dialog displayed.

{
LPTSTR p = reinterpret_cast<LPTSTR>(lParam);
ASSERT(!::IsBadStringPtr(p, UINT_MAX));
BOOL bDismissOk = pWnd->OnBFFValidateFailed(CString(p));
return bDismissOk ? 0 : 1;
}
default:
TRACE(_T(“WARNING: Unknown message 0x%08x (0x%08x) “)
_T(“passed to CYABFFW::BrowseCallbackProc!”),
nMsg, lParam);
return 0;
} // End switch on nMsg.
}

void CYABFFW::FreeItemIdList(IMalloc *pMalloc /*= NULL*/)
{
if (NULL == m_pItemIdList)
return;

bool bWeRelease = false;
if (NULL == pMalloc)
{
bWeRelease = true;
HRESULT hr;
IMalloc *pMalloc;
if (FAILED(hr = ::SHGetMalloc(&pMalloc)))
AfxThrowOleException(hr);
}

pMalloc->Free(m_pItemIdList);

if (bWeRelease)
pMalloc->Release();

m_pItemIdList = NULL;
}

LPITEMIDLIST CYABFFW::ResolveCsidl(int nCsidl) const
{
// Short-circuit special case …
if (CSIDL_DESKTOP == nCsidl)
return NULL;

LPITEMIDLIST pidlRoot;
HRESULT hr = ::SHGetFolderLocation(NULL, nCsidl, NULL, 0U,
>pidlRoot);
if (FAILED(hr))
{
ASSERT(NULL == pidlRoot);
AfxThrowOleException(hr);
}

return pidlRoot; // Caller assumes responsibility
}

LPITEMIDLIST CYABFFW::ResolveFsPath(const CString &strPath) const
{
USES_CONVERSION;

# ifdef _DEBUG
DWORD dwFileAttrs = ::GetFileAttributes(strPath);
ASSERT(0xffffffff != dwFileAttrs &&
FILE_ATTRIBUTE_DIRECTORY & dwFileAttrs);
# endif // _DEBUG

HRESULT hr;
IShellFolder *pDesktop;
if ( FAILED(hr = ::SHGetDesktopFolder(&pDesktop)) )
AfxThrowOleException(hr);

// Unfortunately, T2OLE expects a non-const string, so …
LPOLESTR p2 = T2OLE(const_cast<LPTSTR>(
static_cast<LPCTSTR>(strPath)));
LPITEMIDLIST pItemIdList;
if ( FAILED(hr = pDesktop->ParseDisplayName(NULL, NULL,
p2,
NULL,
&pItemIdList,
NULL)) )
{
pDesktop->Release();
AfxThrowOleException(hr);
}

pDesktop->Release();
return pItemIdList; // Caller assumes responsibility
}

Downloads


Download demo project – 45 Kb


Download doxygen documentation – 106 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read