Browse for a Folder the Non-COM Way

Brian Hart

In Windows, there are two ways to display the "Browse For Folder" dialog box, which provides a constant metaphor for selecting folders between My Computer, Windows Explorer, and other file system parts of the shell. You can do this either by calling SHBrowseForFolder() or by using a "toy box" known as SHDOCVW.DLL. SHBrowseForFolder() -- what Brian calls the non-COM way -- will get us started. In a later article, you can compare it to the COM way, with a guided tour of SHDOCVW.DLL and the powerful shell features it provides.

What should be a familiar Browse For Folder dialog box is shown in Figure 1. You can have these in your own applications quite simply. In fact, there are two ways to get this functionality for yourself. In this article, we'll explore the non-COM approach, with the Windows function SHBrowseForFolder(). We'll see how to fill in a data structure with information that initializes the Browse for Folder dialog box, then call SHBrowseForFolder() to display the dialog box. This is much simpler than writing it all yourself, and it gives your user a consistent environment from application to application. The sample application I've provided with this article (available in the Subscriber Downloads at www.pinpub.com/vcd) uses a single function to do all the work, putting it in an easy-to-access class and calling it whenever you need the pathname of a folder. We'll find out later that the shell does this in much the same way.

Before building the function, I'll show you SHBrowseForFolder() and its arguments, the structure you fill in to initialize the dialog box, and the information in the structure that doesn't need customizing. We can then decide what arguments we'd need to have for our general-purpose function that wraps this whole mess up.

First, let's look at what SHBrowseForFolder() needs. Here's the function's prototype:

WINSHELLAPI LPITEMIDLIST WINAPI 
     SHBrowseForFolder(LPBROWSEINFO lpbi);

This certainly looks straightforward. We pass in a pointer to the BROWSEINFO data structure to initialize and customize the Browse For Folder dialog box. We get back an item ID list -- we'll look at that structure later; it represents objects in the shell. Before calling the function, you need to set up a BROWSEINFO data structure. It's defined like this:

typedef struct _browseinfo { 
HWND hwndOwner;
LPCITEMIDLIST pidlRoot; 
LPSTR pszDisplayName; 
LPCSTR lpszTitle; 
UINT ulFlags; 
BFFCALLBACK lpfn; 
LPARAM lParam; 
int iImage; 
} BROWSEINFO, *PBROWSEINFO, *LPBROWSEINFO; 

These are fairly well-named elements, with one exception. hwndOwner is the HWND of the Browse For Folder dialog box's parent. pidlRoot is the folder at the top of the directory tree displayed in the Browse For Folder dialog box. If you set this member of the data structure to NULL, then the root of the directory tree is the Desktop. That's just what I did in the sample. (If you want to specify where the browsing starts, you can't just pass a string; you have to create an LPITEMIDLIST, and that's not a trivial task.) pszDisplayName points to a buffer that will receive the name of the folder the user selected (but not the full path). Set that buffer up before you call SHBrowseForFolder(), like this:

char szDisplayName[_MAX_PATH];
BROWSEINFO browseInfo;
...
// initialize structure members...
...
browseInfo.pszDisplayName = szDisplayName;
// initialize other structure members...
.

Now here's one that might fool you. lpszTitle doesn't contain text you want displayed on the title bar of the dialog box. The dialog box displays the contents of lpszTitle in a static text control above the treeview. The name of the member, more than anything, is what confuses most people; the documentation does, in fact, say that this member's text is displayed inside the dialog box, not on the title bar. The best text to pass here is a string to remind people what folder they're browsing for.

The most interesting (and fun) member of BROWSEINFO is the ulFlags member. You can specify zero or more of the flags shown in Table 1 in order to make the dialog box much more useful than just browsing for folders.

Table 1. Flags that can be specified to enhance the Browse For Folders dialog box.

Flag Meaning
BIF_BROWSEFORCOMPUTER Only returns computers. If the user selects anything other than a computer, the OK button is grayed.
BIF_BROWSEFORPRINTER Only returns printers. If the user selects anything other than a printer, the OK button is grayed.
BIF_DONTGOBELOWDOMAIN Doesn't include network folders below the domain level in the tree view control.
BIF_RETURNFSANCESTORS Only returns file system ancestors. If the user selects anything other than a file system ancestor, the OK button is grayed.
BIF_RETURNONLYFSDIRS Only returns file system directories. If the user selects folders that aren't part of the file system, the OK button is grayed.
BIF_STATUSTEXT Includes a status area in the dialog box. The callback function can set the status text by sending messages to the dialog box. Use of this flag is beyond the scope of this article.

These flags make the Browse For Folder dialog box very versatile. You can combine them to browse for printers, computers, and network folders and restrict the choices the user can select. The sample program available in the Subscriber Downloads provides an example of using this dialog box to select folders only, but you can change the flags used by the application, re-compile, and see what they look like. The last three parameters are more rarely used -- if you're interested, check the online Help, but for now, let's move along to saving some work.

I want to write a function that will create the structure, initialize it, and call SHBrowseForFolder() to display the dialog box. What's more, the function should extract the path to the selected folder from the LPITEMIDLIST and return it. If the function fails, the string it returns will be empty. We need to make this function a member function of a class that's easily accessible to the whole application. CMyApp and CMainFrame are generally the two classes in any MFC program that are globally accessible. My sample program is dialog-based, which means there isn't a CMainFrame class for us to use, so the choice is clear. For your application, you might want to make it a member of the CMainFrame class or the application class, whichever is right for you. The results are the same!

What should we pass to the function? We need the HWND of the window that's the parent of the Browse For Folder dialog box and the string that we want to put in the dialog box (the lpszTitle member of BROWSEINFO), as well as flags that we want to OR together to tailor the dialog box to our needs. Later, when doing things the COM way, we'll see how to pass in a value for which folder we want to be at the root. This is the BrowseForFolder function's prototype we'll use:

CString& BrowseForFolder(HWND hWnd, LPCSTR lpszTitle,
                        UINT nFlags);

This function returns a reference to a CString because the overloaded assignment operator ( = ) for CString works with references to CString objects. The return type CString& makes it possible to call the function like this:

CString strDir = pApp->BrowseForFolder(m_hWnd, 
                                "Hello, world!", 0);

Now for the fun part! Here's the code for the BrowseForFolder function, along with my comments:

CString& CBrowseForFolderApp::BrowseForFolder(
           HWND hWnd, LPCSTR lpszTitle, UINT nFlags)
{
  // We're going to use the shell to display a 
  // "Choose Directory" dialog box for the user.
  
  CString strResult = ";
  
  LPMALLOC lpMalloc;  // pointer to IMalloc

LpMalloc is a pointer to IMalloc, and IMalloc is the COM interface the Shell uses to allocate structures and pointers that it returns from most of its functions. To make the pointer point to something, we call SHGetMalloc():

  if (::SHGetMalloc(&lpMalloc) != NOERROR)
     return strResult; // failed to get allocator

  char szDisplayName[_MAX_PATH];
  char szBuffer[_MAX_PATH];

The two buffers allocated after the call to SHGetMalloc() hold information that the Browse For Folder dialog box returns. szDisplayName will be put into the pszDisplayName member of the BROWSEINFO structure. This member receives the name only of the selected folder, not its path.

  BROWSEINFO browseInfo;
  browseInfo.hwndOwner = hWnd;
  // set root at Desktop
  browseInfo.pidlRoot = NULL; 
  browseInfo.pszDisplayName = szDisplayName;
  browseInfo.lpszTitle = lpszTitle;   // passed in
  browseInfo.ulFlags = nFlags;   // also passed in
  browseInfo.lpfn = NULL;      // not used
  browseInfo.lParam = 0;      // not used   

  LPITEMIDLIST lpItemIDList;

Since we're not having a callback function (look up BrowseCallbackProc in the documentation for the details), we simply NULL- and zero-out the lpfn and lParam members of the structure. The preceding LPITEMIDLIST variable will receive the return value of the SHBrowseForFolder() function. If the return value is NULL, we know SHBrowseForFolder() failed. So call it, testing for a non-NULL return value:

if ((lpItemIDList = ::SHBrowseForFolder(&browseInfo)) 
       != NULL)
  {

To actually get something useful with the LPITEMIDLIST pointer returned, we can use SHGetPathFromIDList(), passing our lpItemIDList and szBuffer variables as arguments.

When the function returns, we should have a pathname to a folder in szBuffer. If szBuffer[0] is '\0', the NULL-terminator, then we don't have anything we can work with. Otherwise, we stuff szBuffer into strResult, using CString's very useful assignment operator, and return the pathname to the caller as a reference to a CString, something that another of CString's assignment operators accepts:

     // Get the path of the selected folder from the
     // item ID list.
if (::SHGetPathFromIDList(lpItemIDList, szBuffer))
     {
        // At this point, szBuffer contains the path 
        // the user chose.
        if (szBuffer[0] == '\0')
        {
           // SHGetPathFromIDList failed, or
           // SHBrowseForFolder failed.
           AfxMessageBox(IDP_FAILED_GET_DIRECTORY,
              MB_ICONSTOP|MB_OK);
           return strResult;
        }
     
        // We have a path in szBuffer!
        // Return it.
        strResult = szBuffer;
        return strResult;
     }
     else
     {
        // The thing referred to by lpItemIDList 
        // might not have been a file system object.
        // For whatever reason, SHGetPathFromIDList
        // didn't work!
        AfxMessageBox(IDP_FAILED_GET_DIRECTORY,
           MB_ICONSTOP|MB_OK);
        return strResult; // strResult is empty 
     }

Okay, now for the cleanup. The shell made use of its IMalloc interface (remember that LPMALLOC pointer we filled earlier) in order to allocate memory to hold our LPITEMIDLIST variable, which was filled by SHBrowseForFolder(). We need to clean up by freeing the memory through IMalloc::Free. Since IMalloc maintains a reference count to keep track of when it should do its own cleanup (as do all COM interfaces), we need to call the IUnknown::Release method. Memory could leak if we don't.

  lpMalloc->Free(lpItemIDList);
  lpMalloc->Release();      
}

// If we made it this far, SHBrowseForFolder failed.
return strResult;
}

And voila! We have our function, which we can call anytime we need a Browse For Folder dialog box displayed. Remember, the lpszTitle member of the BROWSEINFO structure is for the message you want displayed inside the dialog box -- not on the titlebar. Don't let the name confuse you!

The main dialog box of the sample program calls this function from the message handler function for the "Browse..." button displayed. It simply calls the function as the r-value for the CString assignment operator that takes a CString& as its argument. The assignment initializes a data member for the edit box in the dialog box. The information in the data member is then transferred to the edit box on the screen. Happy browsing!

Brian Hart has been programming since age 14 and is the founder of WnDBSoft Software International. His latest project is Internet Navigator 98, a multi-purpose Internet access tool. He's a full-time, first-year physics and mathematics double-major at Hamline University, located in Saint Paul, MN. http://www.wndbsoft.com/, brian@wndbsoft.com.