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


Comments

  • wheloltabotly PumeSonee Phobereurce 206399

    Posted by TizefaTaNaday on 06/03/2013 02:54am

    Rafseiste airjordanretro11breds2012.holidaygiving.org geskelelt jordan14candycaneforsale.holidaygiving.org Sabearneamono

    Reply
  • rLALD pzC LQvj

    Posted by BZfNRzIucc on 04/12/2013 11:52am

    buy tramadol cheap online safe buy tramadol online - tramadol extended release cost

    Reply
  • how do i remove the control panel item?

    Posted by Legacy on 12/11/2003 12:00am

    Originally posted by: jase jennings

    Hi

    Great class. Thanks.

    Hwo do i remove "Control Panel" from the list displayed?
    Selecting it is of no benefit when browsing for a folder.

    Thanks again

    Jase

    • Use BIF_RETURNONLYFSDIRS

      Posted by sp1ff on 05/07/2005 05:13pm

      Run the demo app & check `Use BIF_RETURNONLYFSDIRS'; you'll notice that the Control Pannel disappears.

      Reply
    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • The explosion in mobile devices and applications has generated a great deal of interest in APIs. Today's businesses are under increased pressure to make it easy to build apps, supply tools to help developers work more quickly, and deploy operational analytics so they can track users, developers, application performance, and more. Apigee Edge provides comprehensive API delivery tools and both operational and business-level analytics in an integrated platform. It is available as on-premise software or through …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds