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.