Click to search the site Click to log in
Online articles
Download free tools
Support pages, per product
Services
Frequently asked questions, per product
How to customize the standard Win32 Browse for Folder dialog
Author: George Mihaescu
Published: May 12, 2006
Category: Implementation technique / Win32
Notes:
Description: This article describes how you can quickly get a standard "browse for folder" dialog in your Win32 application, then describes in detail how this standard dialog can be customized to virtually any degree of sophistication. We have used this technique in several projects over the years, with excellent results.
View count: 4,483
Comments: 3 Read comments or post your own

  Print viewOpens in new window
How to customize the standard Win32 Shell "browse for folder" dialog

How to customize the standard Windows Shell "browse for folder" dialog

 

By George Mihaescu

 

Summary: this article shows how to use the "browse for folder" dialog exposed through COM by the Windows Shell and how to customize it as much as you want. The sample code provided is in C++ / MFC dropping down at C Win32 API in key places; it is a Visual Studio 2005 project.

The problem

At the basic level, you'd like to have in your Windows application a dialog that allows the users to pick a folder. Ideally, you'd like this dialog to be customized (and maybe customized very extensively).

The solution

Let me solve the basic needs first. The Windows Shell offers a number of COM interfaces that allow you to exercise some of its functionality; among them, the ability to browse for folders. Use the code below to pop up a "browse for folder" dialog, not customized (except for a simple custom message at the top of the dialog):

 

#include "stdafx.h"

....

#include <shlobj.h>        //for Shell API

#include <Shlwapi.h> //for Shell API

 

....

 

void CMyDlg::OnBnClickedShowStandardBrowser()

{

       //standard use of the Shell API to browse for folders

       bool f_selected = false;

 

       char szDir [MAX_PATH];

       BROWSEINFO bi;        

       LPITEMIDLIST pidl;        

       LPMALLOC pMalloc;

       if (SUCCEEDED (::SHGetMalloc (&pMalloc)))

       {

              ::ZeroMemory (&bi,sizeof(bi)); 

 

              CString custom_msg;

              custom_msg = "Go ahead, select a directory:";

 

              bi.lpszTitle = custom_msg;

              bi.hwndOwner = this ->GetSafeHwnd ();

              bi.pszDisplayName = 0;           

              bi.pidlRoot = 0;

              bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT;

              bi.lpfn = NULL;      //no customization function

              bi.lParam = NULL;    //no parameters to the customization function

 

              pidl = ::SHBrowseForFolder(&bi);           

              if (pidl)

              {

                     if (::SHGetPathFromIDList (pidl, szDir))

                     {

                           f_selected = true;

                     }

                

                     pMalloc -> Free(pidl);

                     pMalloc -> Release();

              }     

       }

 

       if (f_selected)

       {

              MessageBox (szDir, "User selected path:");

       }

}

 

The code above produces the following output:

 

 

But how about customizing it? Well, the only question is "how custom to you want it"? How about this custom:

 

 

That's quite different from the first standard dialog shown above – yet it's the same dialog. The folder tree on the left is the original control of the dialog – in addition, we have extended the dialog size then added our own controls that respond to the events in the folder tree. The result is a perfectly smooth dialog that offers an excellent user experience.

 

This is not tremendously difficult (but not trivial either). The key to this are two fields in the BROWSEINFO structure: lpfn and lParam. The first is a pointer to a function that gets called on important events in the lifetime of the "browse for folder" dialog and the second is the parameter you want to pass to that function. The "important events" in the lifetime of the "browse for folder" dialog are the "dialog initialized" (so that you can change its layout and add your own controls) and the "selection changed" so that you can react when the user makes a selection in the folder tree control.

 

The sample class (CBrowsePackagesDialog) that I show below (and which is part of the sample offered for download) does exactly that. It simulates the case where you want to list the zip files in the folder selected by the user, and allow the user to select a subset of those zip files. It wraps the functionality offered by the standard Shell "browse for folder" dialog and does this customization internally, so that its interface is very simple. See how one would use it:

 

void CMyDlg::OnBnClickedShowCustomBrowser()

{

       //show our customized folder browser dialog

       CBrowsePackagesDialog dlg (this);

       if (dlg.Show () == IDOK)

       {

              MessageBox ("User is done");

       }

}

 

The header is very simple:

 

#ifndef _BROWSEPACKAGESDIALOG_H_

#define _BROWSEPACKAGESDIALOG_H_

 

class CBrowsePackagesDialog

{

public:

       //fake (because this dialog is not created from our resources) dialog ID

       enum { IDD = 0 };

 

       CBrowsePackagesDialog (CWnd* p_parent,

   const char* p_initial_folder = NULL);

       bool Show ();

 

       const char* Get_Initial_Folder () const;

       CWnd* Get_Parent () const;

       int Show_Zips_In_Dir (const char* path);

 

private:

       CWnd* m_p_parent;

       const char* m_p_initial_folder;

};

 

#endif //_BROWSEPACKAGESDIALOG_H_

 

The methods Get_Initial_Folder, Get_Parent and Show_Zips_In_Dir are doing the actual interfacing with the Shell "browse for folder" dialog; they are public only because they need to be called from the C-style dialog customization function. You can make them private and make the C customization function (Folder_Browse_Callback) a friend of the class; I did not want to do this so that I don't confuse anybody.

 

The heavy lifting is in the implementation; it all starts at the Show method, where you should notice the setting of the dialog customization function pointer and its parameter:

 

 

#include "stdafx.h"

#include "BrowsePackagesDialog.h" //own header

#include <shlobj.h>               //for Shell API

#include <Shlwapi.h>       //for Shell API

 

 

//This is the shell dialog customization callback

int CALLBACK Folder_Browse_Callback (HWND hwnd,UINT uMsg,

    LPARAM lp, LPARAM pData);

 

//This is the shell dialog event processing procedure

LRESULT CALLBACK Dialog_Message_Proc (HWND hwnd, UINT uMsg,

     WPARAM wParam, LPARAM lParam);

 

//helper functions

void Change_Dialog_Layout (HWND hwnd);

 

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

// GLOBALS

 

//wrappers over the controls (we attach some of the controls to MFC

//objects for ease of manipulation)

static CListCtrl     g_list_ctrl;

static CWnd          g_this_dialog;

static CButton       g_help_button;

static CButton       g_all_button;

static CButton       g_none_button;

 

WNDPROC g_previous_dlg_msg_proc = NULL;

 

 

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

// CONSTANTS

 

const char* PARENT_PROPERTY_NAME = "PARENT";

 

 

CBrowsePackagesDialog::CBrowsePackagesDialog (CWnd* p_parent,

                     const char* p_initial_folder /*=NULL*/)

{

       ASSERT (m_p_parent != NULL);

 

       m_p_parent = p_parent;

       m_p_initial_folder = p_initial_folder;

}

 

 

bool CBrowsePackagesDialog::Show ()

{

       //show the shell browse for folder dialog, which we customize through the

       //callback function Folder_Browse_Callback

       bool f_selected = false;

 

       char szDir [MAX_PATH];

       BROWSEINFO bi;        

       LPITEMIDLIST pidl;        

       LPMALLOC pMalloc;

       if (SUCCEEDED (::SHGetMalloc (&pMalloc)))

       {

              ::ZeroMemory (&bi,sizeof(bi)); 

 

              CString custom_msg;

              custom_msg = "Please select a folder containing .zip files:";

 

              bi.lpszTitle = custom_msg;

              bi.hwndOwner = m_p_parent ->GetSafeHwnd ();

              bi.pszDisplayName = 0;           

              bi.pidlRoot = 0;

              bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT;

              bi.lpfn = Folder_Browse_Callback; //the customization callback...

              bi.lParam = (LPARAM) this; //...to which pass 'this' as parameter

 

              pidl = ::SHBrowseForFolder(&bi);           

              if (pidl)

              {

                     if (::SHGetPathFromIDList (pidl, szDir))

                     {

                           f_selected = true;

                     }

                

                     pMalloc -> Free(pidl);

                     pMalloc -> Release();

              }     

       }

 

       return f_selected;

}

 

 

int CALLBACK Folder_Browse_Callback (HWND hwnd, UINT uMsg,

LPARAM lp, LPARAM pData)

{

       switch(uMsg)

       {

              case BFFM_INITIALIZED:

                     {

                     CBrowsePackagesDialog* p_dlg =

(CBrowsePackagesDialog*) pData;

 

                     //remove the context help button from the caption

                     CWnd::ModifyStyleEx (hwnd, WS_EX_CONTEXTHELP, 0, 0);

 

                     //set the special window property the holds the "parent" to

                     //the handle of the parent

                     ::SetProp (hwnd, PARENT_PROPERTY_NAME,

                           (HANDLE) p_dlg ->Get_Parent () ->GetSafeHwnd ());

 

                     //set window text

                     ::SetWindowText (hwnd, "Browse for .zip files to extract");

 

                     //extend the window and reposition the controls

                     ::Change_Dialog_Layout (hwnd);

 

                     //see if we need to make an initial directory selection

                     const char* p_initial_folder =

p_dlg ->Get_Initial_Folder ();

                     if (p_initial_folder != NULL)

                     {

                           ::SendMessage (hwnd, BFFM_SETSELECTION, 1,

(LPARAM) p_initial_folder);

                     }

 

                     //set an event processing function to be able to tell when

                     //the user checks items in the list control and when he

//clicks OK

                     g_previous_dlg_msg_proc = (WNDPROC) ::SetWindowLong (hwnd,

DWL_DLGPROC,

(LONG) Dialog_Message_Proc);

                     }

                     break;

 

              case BFFM_SELCHANGED:

                     // Show the the count of compatible zips available

//at the selected path

                     if (::IsWindow (g_list_ctrl.GetSafeHwnd ()))

                     {

                           g_list_ctrl.DeleteAllItems ();

 

                           char dir [MAX_PATH];

                           if (::SHGetPathFromIDList ((LPITEMIDLIST) lp, dir))

                           {

                                   TRACE ("User selected dir: %s\n", dir);

 

                                  //find the ZIP files in the selected dir

//and show them

                                  CBrowsePackagesDialog* p_dlg =

                                         (CBrowsePackagesDialog*) pData;

                                  int compatible_zip_count =

p_dlg ->Show_Zips_In_Dir (dir);

 

                                  //display the status message with the

//number of compatible ZIP files

                                  CString display;

                                  display.Format ("%d compatible zip files found at the selected location!", compatible_zip_count);

                                  ::SendMessage (hwnd, BFFM_SETSTATUSTEXT,

0, (LPARAM) (LPCTSTR) display);

                           }     

                          

                     }

                     break;

 

              default:

                 break;

        }

        return 0;

}

 

 

 

//IDs of the controls in the shell dialog

const int FOLDER_TREE = 0x3741;

const int STATIC1 = 0x3742;

const int STATIC2 = 0x3743;

const int ID_OK = 0x1;

const int ID_CANCEL = 0x2;

const int ID_NEW_LIST_CTRL = 1000;

const int ID_NEW_HELP_BUTTON = 1001;

const int ID_NEW_ALL_BUTTON = 1002;

const int ID_NEW_NONE_BUTTON = 1003;

 

//command ID sent by the shell dialog when F1 is pressed

const int ID_HELP_COMMAND = 0x365;

 

void Change_Dialog_Layout (HWND hwnd)

{

       //change the layout of the dialog (extend it, then re-organize the

       //controls in the way we want, and finally create the list control)

 

       //constants

       const int DLG_WIDTH = 750;

       const int DLG_HEIGHT = 450;

 

       const int BUTTON_H = 22;

       const int BUTTON_W = 75;

 

       const int MARGIN = 10;

 

       //extend the window first

       ::SetWindowPos (hwnd, NULL, 0, 0, DLG_WIDTH, DLG_HEIGHT,

SWP_NOZORDER | SWP_NOMOVE);

 

       CRect rect;

       ::GetClientRect (hwnd, rect);

 

       //memorize the coordinates of the "grid" in which we place the controls

       int LEFT = rect.left + MARGIN;

       int RIGHT = rect.right - MARGIN;

       int TOP = rect.top + MARGIN;

       int BOTTOM = rect.bottom - MARGIN;

       int MIDDLE = 250;

       int TOT_WIDTH = RIGHT - LEFT;

       int TOT_HEIGHT = BOTTOM - TOP;

 

       const int STATIC1_H = 15;

       const int STATIC2_H = 30;

 

       HWND h_ctrl = GetDlgItem (hwnd, STATIC1);

       if (h_ctrl != NULL)

       {

              ::SetWindowPos (h_ctrl, NULL,

                           LEFT, TOP, TOT_WIDTH, STATIC1_H, SWP_NOZORDER);

       }

 

       h_ctrl = GetDlgItem (hwnd, FOLDER_TREE);

       if (h_ctrl != NULL)

       {

              LONG style = ::GetWindowLong (h_ctrl, GWL_STYLE);

              style |= TVS_SHOWSELALWAYS;

              ::SetWindowLong (h_ctrl, GWL_STYLE, style);

 

              ::SetWindowPos (h_ctrl, NULL,

                           LEFT, TOP + STATIC1_H + MARGIN / 2,

                           MIDDLE - LEFT, TOT_HEIGHT - (STATIC1_H + MARGIN / 2),

                           SWP_NOZORDER);

       }

 

       h_ctrl = GetDlgItem (hwnd, STATIC2);

       if (h_ctrl != NULL)

       {

              ::SetWindowPos (h_ctrl, NULL,

                           MIDDLE + MARGIN, TOP + STATIC1_H + MARGIN / 2,

                           RIGHT - MIDDLE - MARGIN, STATIC2_H, SWP_NOZORDER);

       }

 

       //OK and Cancel buttons

       h_ctrl = GetDlgItem (hwnd, ID_OK);

       HFONT h_button_font = NULL;

       if (h_ctrl != NULL)

       {

              ::SetWindowPos (h_ctrl, NULL,

                           RIGHT - 2 * BUTTON_W - MARGIN, BOTTOM - BUTTON_H,

                           BUTTON_W, BUTTON_H, SWP_NOZORDER);

              h_button_font = (HFONT) ::SendMessage (h_ctrl, WM_GETFONT, 0, 0);

       }

 

       h_ctrl = GetDlgItem (hwnd, ID_CANCEL);

       if (h_ctrl != NULL)

       {

              ::SetWindowPos (h_ctrl, NULL,

                           RIGHT - BUTTON_W, BOTTOM - BUTTON_H,

                           BUTTON_W, BUTTON_H, SWP_NOZORDER);

       }

 

       //subclass this dialog so that we have a CWnd wrapper for it to

//ease of use

       g_this_dialog.SubclassWindow (hwnd);

 

       //create and position the help button

       rect.SetRect (RIGHT - 3 * BUTTON_W - 2 * MARGIN, BOTTOM - BUTTON_H,

                             RIGHT - 2 * BUTTON_W - 2 * MARGIN, BOTTOM);

       if (g_help_button.Create ("Help", WS_CHILD | WS_VISIBLE, rect,

&g_this_dialog, ID_NEW_HELP_BUTTON))

       {

              //set the same font as for the other buttons (retrieved above)

              if (h_button_font != NULL)

              {

                     ::SendMessage (g_help_button.GetSafeHwnd (), WM_SETFONT,

                                            (WPARAM) h_button_font, 0);

              }

       }

 

       //create and position the All / None buttons

       rect.SetRect (MIDDLE + MARGIN,

                       TOP + STATIC1_H + MARGIN + STATIC2_H,

                       MIDDLE + MARGIN + BUTTON_W,

                       TOP + STATIC1_H + MARGIN + STATIC2_H + BUTTON_H);

       if (g_all_button.Create ("Check all", WS_CHILD | WS_VISIBLE, rect,

&g_this_dialog, ID_NEW_ALL_BUTTON))

       {

              //set the same font as for the other buttons (retrieved above)

              if (h_button_font != NULL)

              {

                     ::SendMessage (g_all_button.GetSafeHwnd (), WM_SETFONT,

                                            (WPARAM) h_button_font, 0);

              }

       }

 

       rect.SetRect (MIDDLE + MARGIN + BUTTON_W + MARGIN,

                       TOP + STATIC1_H + MARGIN + STATIC2_H,

                       MIDDLE + MARGIN + BUTTON_W + MARGIN + BUTTON_W,

                       TOP + STATIC1_H + MARGIN + STATIC2_H + BUTTON_H);

       if (g_none_button.Create ("Check none", WS_CHILD | WS_VISIBLE, rect,

&g_this_dialog, ID_NEW_NONE_BUTTON))

       {

              //set the same font as for the other buttons (retrieved above)

              if (h_button_font != NULL)

              {

                     ::SendMessage (g_none_button.GetSafeHwnd (), WM_SETFONT,

                                            (WPARAM) h_button_font, 0);

              }

       }

 

       //create and position the list control for the packages content:

       rect.SetRect (MIDDLE + MARGIN,

TOP + STATIC1_H + MARGIN + STATIC2_H + MARGIN + BUTTON_H,

                     RIGHT, BOTTOM - BUTTON_H - MARGIN / 2);

       if (g_list_ctrl.CreateEx (WS_EX_CLIENTEDGE,

                           WS_CHILD | WS_BORDER | WS_VISIBLE | LVS_REPORT,

                           rect, &g_this_dialog, ID_NEW_LIST_CTRL))

       {

              //force the control to have checkboxes

              DWORD style = g_list_ctrl.GetStyle ();

              g_list_ctrl.SetExtendedStyle (style | LVS_EX_CHECKBOXES |

                                                LVS_EX_FULLROWSELECT);

             

              //insert the columns

              g_list_ctrl.InsertColumn (0, "Zip name", LVCFMT_LEFT, 200);

              g_list_ctrl.InsertColumn (1, "Version", LVCFMT_LEFT, 50, 1);

              g_list_ctrl.InsertColumn (2, "Action if checked", LVCFMT_LEFT,

300, 2);

       }

}

 

 

LRESULT CALLBACK Dialog_Message_Proc (HWND hwnd, UINT uMsg,

WPARAM wParam, LPARAM lParam)

{

       //We intercept the events in the dialog to check notifications

//from controls and react to user checking / unchecking things

//in the list control and pressing OK

       if (uMsg == WM_NOTIFY)

       {

              int idCtrl = (int) wParam;

              LPNMHDR pnmh = (LPNMHDR) lParam;

 

              if (idCtrl == ID_NEW_LIST_CTRL)

              {

                     //see how many checked items we have: if none, disable OK,

                     //otherwise enable it; same for All and None

                     const int item_count = g_list_ctrl.GetItemCount ();

                     int check_count = 0;

                     for (int i = 0; i < item_count; i++)

                     {

                           if (g_list_ctrl.GetCheck (i))

                           {

                                  check_count++;

                           }

                     }

 

                     CWnd* p_ok = g_this_dialog.GetDlgItem (ID_OK);

                     p_ok -> EnableWindow (check_count > 0);

 

                     g_all_button.EnableWindow (check_count != item_count);

                     g_none_button.EnableWindow (check_count > 0);

              }

       }

       else if (uMsg == WM_COMMAND)

       {

              //message from a button; we care about OK and Help

              WORD wNotifyCode = HIWORD (wParam);

              WORD idCtrl = LOWORD(wParam);  

             

              if (idCtrl == ID_OK && wNotifyCode == BN_CLICKED)

              {

                     //we should have at least one checked list item. Collect

                     //the data associated with the items that are checked and

//remember it in some variables (will have to be globals)

                     //...

              }

              else if (idCtrl == ID_NEW_ALL_BUTTON && wNotifyCode == BN_CLICKED)

              {

                     //check all items in the list

                     for (int i = 0; i < g_list_ctrl.GetItemCount (); i++)

                     {

                           g_list_ctrl.SetCheck (i, TRUE);

                     }

              }

              else if (idCtrl == ID_NEW_NONE_BUTTON &&

wNotifyCode == BN_CLICKED)

              {

                     //uncheck all items in the list

                     for (int i = 0; i < g_list_ctrl.GetItemCount (); i++)

                     {

                           g_list_ctrl.SetCheck (i, FALSE);

                     }

              }

              else if (idCtrl == ID_NEW_HELP_BUTTON &&

wNotifyCode == BN_CLICKED)

              {

                     //Help button clicked, show the help for this dialog

                     ::AfxGetApp () -> WinHelp (CBrowsePackagesDialog::IDD,

                                                       HH_HELP_CONTEXT);   

              }

       }

       else if (uMsg == ID_HELP_COMMAND)

       {

              //F1 pressed, show the help for this dialog

              ::AfxGetApp () -> WinHelp (CBrowsePackagesDialog::IDD,

                                                HH_HELP_CONTEXT);   

       }

       else if (uMsg == WM_DESTROY)

       {

              //remove the window property

              ::RemoveProp (hwnd, PARENT_PROPERTY_NAME);

       }

 

       //call the original dialog message procedure

       return ::CallWindowProc (g_previous_dlg_msg_proc, hwnd,

uMsg, wParam, lParam);

}

 

 

const char* CBrowsePackagesDialog::Get_Initial_Folder () const

{

       return m_p_initial_folder;

}

 

CWnd* CBrowsePackagesDialog::Get_Parent () const

{

       return m_p_parent;

}

 

int CBrowsePackagesDialog::Show_Zips_In_Dir (const char* dir)

{

       if (!::IsWindow (g_list_ctrl.GetSafeHwnd ())) return 0;

 

       int compatible_zips = 0;

       g_list_ctrl.DeleteAllItems ();

 

       char search_parms [MAX_PATH];

       ::strcpy_s (search_parms, MAX_PATH, dir);

       ::strcat_s (search_parms, MAX_PATH, "\\*.zip");

 

       WIN32_FIND_DATA find_data;

       HANDLE h_find = ::FindFirstFile (search_parms, &find_data);

       BOOL f_ok = TRUE;

       while (h_find != INVALID_HANDLE_VALUE && f_ok)

       {

              compatible_zips++;

 

              //add the found file in the list

              int idx = g_list_ctrl.InsertItem (0, find_data.cFileName);

              if (idx >= 0)

              {

                     g_list_ctrl.SetItemText (idx, 1, "1.0");

                     g_list_ctrl.SetItemText (idx, 2, "Will install");

              }

 

              //go for the next zip

              f_ok = ::FindNextFile (h_find, &find_data);

       }

 

       return compatible_zips;

}

 

As you can see, the trick is done in the C-style callback Folder_Browse_Callback which he dialog offers for customizations; from here, once we know the dialog has been initialized, we call Change_Dialog_Layout which does the extending of the dialog, re-positioning of its standard controls and adding our additional controls.

 

Then it sets a new Window Procedure (event handling callback) called Dialog_Message_Proc to the standard dialog – but saves the pointer to the original event handling function. This new event handler function first has a look at the events we care about (such as button clicks, F1 being pressed so that we can show our application's help, etc.) – and if it does not care about them, just passes them to the original event handling function (whose pointer was saved as described above).

 

These are the main points about this implementation; the rest is just application-specific logic, which you'll need to implement according to your own requirements.

 

 

 


Reader comments:
Name: (optional)
Verification text:    
(type as in image next to it)
Comment: max 2,000 characters; for security reasons no active content / no HTML formatting is supported.
Please stick to the subject of the article; comments are reviewed and unrelated / inappropriate ones will be deleted.

On Apr 17, 2007 at 17:12 EST George said:

Quick update: out of curiosity, I've run the sample on Vista and works beatifully, taking the Vista look and feel. I'm quite pleased with it.

On Aug 17, 2006 at 18:01 EST George said:

Indeed Brian, it is a lot of work to get it right, a lot of pixel counting... but you get spectacular results in a very reliable implementation. Besides, because this is the standard shell dialog, you'll get the look and feel of whatever flavor of Windows you run it on.

On Aug 17, 2006 at 16:59 EST brian said:

Very nice - could not tell that the second dialog is actually the same as the first (standard one) if you didn;t tell me. It's a lot of work though moving the controls around and all that.
Copyright 2308 registered users, 23 users online now