New information has been added to this article since publication.
Refer to the Editor's Update below.

C++ At Work

Counting MDI Children, Browsing for Folders

Paul DiLascia

Code download available at:CAtWork0506.exe(195 KB)

Q I'm writing a Multiple Document Interface (MDI) app using MFC. From the parent window, how can I check whether all the MDI child windows have been closed? If they are all closed, then I want to activate a panel in my main window.

Q I'm writing a Multiple Document Interface (MDI) app using MFC. From the parent window, how can I check whether all the MDI child windows have been closed? If they are all closed, then I want to activate a panel in my main window.

Ramesh

A Windows® and MFC don't provide any functions specifically to get the number of MDI child windows, but implementing what you want is easy enough. In fact, I can think of half a dozen ways to skin this cat. You could trap WM_CREATE/WM_DESTROY messages; you could install a Windows hook with SetWindowsHookEx; you could use EnumWindows to enumerate the child windows and count how many there are. But often the simplest solution is the one most easily overlooked.

A Windows® and MFC don't provide any functions specifically to get the number of MDI child windows, but implementing what you want is easy enough. In fact, I can think of half a dozen ways to skin this cat. You could trap WM_CREATE/WM_DESTROY messages; you could install a Windows hook with SetWindowsHookEx; you could use EnumWindows to enumerate the child windows and count how many there are. But often the simplest solution is the one most easily overlooked.

All you need to solve this problem—something that works whether you're using MDI or some other multiple-window user interface of your own design—is a lowly list. Figure 1 shows a class based on the Standard Template Library (STL) list that does the job. It's hardly even worth encapsulating, but I did just in case typing "push_back" is too weird for Windows-based programmers; CWinList lets you type "Add" instead. To use CWinList, just add a global instance somewhere, either as a global variable or data member in your main application class:

class CMyApp : public CWinApp { public: CWinList m_winlist; // list of open windows };

Figure 1 WinList.h

//////////////////////////////////////////////////////////////// // MSDN Magazine — June 2005 // If this code works, it was written by Paul DiLascia. // If not, I don't know who wrote it. // Compiles with Visual Studio .NET 2003 (V7.1) on Windows XP. Tab size=3. // #pragma once #include <list> using namespace std; ////////////////// // Simple list class to keep track of windows by adding them to a list. // Use this to keep track of MDI child or other windows. See view.cpp for // example of how to use. Based on STL list, CWinList is a list of CWnd // pointers: // list<CWnd*>, with methods Add and Remove. You can also call any other // STL list functions you want, and use the list iterator to enumerate // the items in the list. // class CWinList : public list<CWnd*> { public: CWinList() { } ~CWinList() { clear(); } // add window to list void Add(CWnd* pWnd) { push_back(pWnd); } // remove window from list void Remove(CWnd* pWnd) { remove(pWnd); } };

In order for your children to be tracked, all they have to do is add and remove themselves from the list as they're created and destroyed. The obvious place to do it is within the constructor and destructor for whichever window class you want to keep track of:

CMyView::CMyView() { theApp.m_winlist.Add(this); } CMyView::~CMyView() { theApp.m_winlist.Remove(this); }

Alternatively, you can call Add and Remove from your OnCreate and OnDestroy handlers to ensure your list contains only window objects with valid HWNDs. Since CWinList is derived from list<CWnd*>, you have the full power of STL at your disposal. For example, you can use the STL list iterator to enumerate the windows in your list:

CWinList& wl = theApp.m_winlist; for (CWinList::iterator it=wl.begin(); it!=wl.end(); it++) { CWnd* pWnd = *it; // do something }

I wrote a little test program called WinCount that uses CWinList to count MDI child windows. Figure 2 shows it running. WinCount has a status bar panel in the lower-right corner that shows the number of windows open, and the application's About dialog lists the windows' captions. The About dialog uses CWinList::iterator to generate its feedback message; the status bar panel is a standard MFC indicator panel with an ON_UPDATE_COMMAND_UI handler that displays the number of views:

void CMainFrame::OnUpdateWinIndicator(CCmdUI* pCmdUI) { CString s; s.Format(_T("Open:%d"), theApp.m_winlist.size()); pCmdUI->SetText(s); }

Figure 2 Counting MDI Children with WinCount

Figure 2** Counting MDI Children with WinCount **

List::size is an STL list method that returns the number of items in the list. Note that for WinCount, there's a one-to-one correspondence between views and MDI child windows, so counting the number of views is the same as counting the number of MDI children. If you have a more complex user interface with multiple views inside each MDI child frame, you'd have to override CMDIChildWnd to put the Add/Remove calls in your derived class, or you could use some other window class that appears exactly once in each child frame. You can use as many CWinLists as you want to keep track of different kinds of window classes.

I know we live in the heady age of MFC, .NET, and GUI Frameworks that do everything for you—but don't forget how to use basic data structures!

[Editor's Update - 5/9/2005: In the original implementation of CWinList shown in Figure 1, CWinList is derived from list<CWnd*>. Generally, however, it is considered bad practice to derive from STL containers as doing so can sometimes lead to unpredictable results. As such, the download for this article contains a new version where CWinList is now a typedef. This requires using push_back and remove instead of Add and Remove.]

Q I'm building an app in C++ using Visual Studio® .NET and MFC. In my program, the user has to pick a folder in which to copy some files. I can run the OpenFileDialog to let the user select a file, but how do I get the Open dialog to display only folders? Almost every installation program I've seen displays a dialog that shows only folders, but I can't seem to find the right flags.

Q I'm building an app in C++ using Visual Studio® .NET and MFC. In my program, the user has to pick a folder in which to copy some files. I can run the OpenFileDialog to let the user select a file, but how do I get the Open dialog to display only folders? Almost every installation program I've seen displays a dialog that shows only folders, but I can't seem to find the right flags.

Laine Chandler

A The reason you can't find the right flags is you're looking at the wrong function! The File Open dialog (the Win32® GetOpenFileName or MFC's CFileDialog) doesn't do folders. To display the folder-browser dialog, you have to call a special shell function, SHBrowseForFolder. To use it, you stuff a BROWSEINFO struct with a bunch of information, then call SHBrowseForFolder. Windows displays a dialog like the one in Figure 3. Users can navigate the folder hierarchy, expand and collapse items, and select the folder they want.

A The reason you can't find the right flags is you're looking at the wrong function! The File Open dialog (the Win32® GetOpenFileName or MFC's CFileDialog) doesn't do folders. To display the folder-browser dialog, you have to call a special shell function, SHBrowseForFolder. To use it, you stuff a BROWSEINFO struct with a bunch of information, then call SHBrowseForFolder. Windows displays a dialog like the one in Figure 3. Users can navigate the folder hierarchy, expand and collapse items, and select the folder they want.

Figure 3 Calling SHBrowseForFolder

Figure 3** Calling SHBrowseForFolder **

Alas, using SHBrowseForFolder isn't quite as easy as CFileDialog. It requires at least a minimal understanding of shellspeak, which makes heavy use of COM, IShellFolder, and PIDLs. In case you don't already know, PIDL (pronounced "piddle") is short for pointer-to-item-ID-list. The actual C type is LPITEMIDLIST, or LPCITEMIDLIST for the const variety. A PIDL is a byte string the shell uses to identify shell objects like files and folders, as well as pseudo-objects like My Computer or My Network Places. For ordinary files and folders, the bytes are the path name in Unicode, but for other objects the bytes are different so don't rely on that. The important thing is that when your user finally chooses a folder, SHBrowseForFolder returns it as a PIDL. To get the path name, you have to convert.

Since SHBrowseForFolder is so useful, and since MFC doesn't have a class to encapsulate it, I decided to write one for you. (I'm so nice, I know.) CFolderDialog hides the coding grungies and makes browsing for folders as easy as eating pie. All you have to do is instantiate and call BrowseForFolder:

CFolderDialog dlg(this); LPCITEMIDLIST pidl = dlg.BrowseForFolder( _T("Pick a folder, dude!"), BIF_RETURNONLYFSDIRS | BIF_STATUSTEXT); CString path = dlg.GetPathName(pidl);

CFolderDialog even has a handy function to convert the PIDL to a CString path name. SHBrowseForFolder can return the selected folder as a TCHAR string if you give it a buffer in BROWSEINFO::pszDisplayName, but the display name returned is only the final part of the full path—for example "Photos" if the folder is C:\MyStuff\Pub\Photos. If you want the full path name, you have to convert the PIDL using SHGetPathFromIDList or my own GetPathName. If the display name is what you want, call CFolderDialog::GetDisplayName.

If all you want to do is let the user pick a folder, then BrowseForFolder and GetPathName are all you need. But SHBrowseForFolder can do lots more. As with GetOpenFileName, it lets you supply a callback function to customize its behavior. If you provide a BrowseCallbackProc in BROWSEINFO::lpfn, Windows will call it when stuff happens. For example, Windows sends BFFM_INITIALIZED when the folder dialog has initialized itself, and BFFM_SELCHANGED when the user selects a new folder. Your callback procedure can process these notifications and do whatever it wants. For example, you can enable or disable the OK button by sending BFFM_ENABLEOK, or change the button text with BFFM_SETOKTEXT. CFolderDialog replaces the C-style callback with C++ virtual handler functions in typical MFC fashion, so instead of writing a callback proc you derive from CFolderDialog and override virtual functions like OnInitialized and OnSelChanged. Internally, CFolderDialog uses its own callback that calls these methods.

To exercise the more advanced features of CFolderDialog, I wrote a test app called FolderPick. It has two commands that run the folder dialog. One command displays the "old-style" dialog; the other, the new. The new style (BIF_NEWDIALOGSTYLE) creates a larger, sizeable dialog with a Make New Folder button and—if you specify BIF_EDITBOX—an edit box where the user can type the folder name. Other flags include BIF_BROWSEFORCOMPUTER to show computers and BIF_BROWSEFORPRINTER for printers. BIF_RETURNONLYFSDIRS tells Windows to return only file system directories, not pseudo-folders like My Network Places, and BIF_STATUSTEXT creates a status window whose text you can set. (BIF_STATUSTEXT is not supported for new-style dialogs.) For the full list of flags, see the documentation for BROWSEINFO.

FolderPick derives a new class, CMyFolderDialog, with overrides for OnInitialized and OnValidateFailed. When the dialog is initialized, FolderPick sets the status text and changes the name of the OK button to "Choose Me!"

void CMyFolderDialog::OnInitialized() { SetStatusText(_T("Nice day, isn't it?")); SetOKText(L"Choose Me!"); }

There are a couple of things to underscore here. First, CFolderDialog has wrappers like SetStatusText and SetOKText for folder dialog messages like BFFM_SETSTATUSTEXT and BFFM_SETOKTEXT. If you were programming in C, you'd call ::SendMessage; with CFolderDialog you just call the wrappers. The only caveat is that you can only call these wrappers from within your virtual notification handlers (OnInitialized, OnSelChanged, and the rest) because m_hWnd is valid only while the folder dialog is actually running, not before or after calling BrowseForFolder. Internally, CFolderDialog subclasses the folder dialog the first time its callback receives a notification. The second thing to notice is that some BFFM_ messages require Unicode strings, not LPCTSTRs. That's why "Choose Me!" in the snippet is a wide character string (prefixed with L).

The Microsoft documentation has a couple of minor errors I should point out in case you try to program SHBrowseForFolder in C. The documentation says to pass the string for BFFM_SETOKTEXT in WPARAM; actually, it's LPARAM. It also says that BFFM_SETSELECTION requires a Unicode string, but BFFM_SETSELECTION is available in both A and W flavors, so you can use LPCTSTR.

If you use BIF_EDITBOX with the new-style dialog, Windows displays an edit control where the user can type the name of the folder. If the user types something bad, Windows calls the browser proc with BFFM_VALIDATEFAILED. CFolderDialog processes this by calling OnValidateFailed. FolderPick overrides OnValidateFailed to display an error message like the one in Figure 4.

BOOL CMyFolderDialog::OnValidateFailed(LPCTSTR lpsz) { MessageBox(...); return TRUE; // don't dismiss dialog }

Figure 4 Error Message from FolderPick

Figure 4** Error Message from FolderPick **

Another cool feature that SHBrowseForFolder supports is custom filtering. This lets you control which items appear in the folder dialog on a per-item basis. As with callbacks, the mechanics are a bit tedious. You have to implement a COM interface, IFolderFilter, with two methods: GetEnumFlags and ShouldShow. When the folder dialog sends your callback BFFM_IUNKNOWN, you have to QueryInterface the IUnknown it passes for IFolderFilterSite, then call IFolderFilterSite::SetFilter with your IFolderFilter. Now the folder dialog calls your IFolderFilter::ShouldShow to filter each item. You can return S_OK to show the item or S_FALSE to hide it. Once you've installed your filter, IFolderFilterSite is no longer needed so you can Release it.

Naturally, I encapsulated all of this, too. To use custom filtering, all you have to do is call BrowseForFolder with bFilter=TRUE, and override two virtual functions: OnGetEnumFlags and OnShouldShow. No need to deal with COM, QueryInterface, or IFolderFilter. Figure 5 and Figure 6 show the code that performs this magic. CFolderDialog implements its own IFolderFilter internally, one that calls the corresponding virtual CFolderDialog functions. CFolderDialog::OnIUnknown uses the Active Template Library (ATL) CComQIPtr smart pointer class to make COM coding a breeze.

Figure 6 TraceWin

Figure 5 FolderDlg

FolderDlg.h

//////////////////////////////////////////////////////////////// // MSDN Magazine — June 2005 // If this code works, it was written by Paul DiLascia. // If not, I don't know who wrote it. // Compiles with Visual Studio .NET 2003 (V7.1) on Windows XP. Tab // size=3. // #pragma once #include "debug.h" // debugging tools ////////////////// // Class to encapsulate SHBrowseForFolder. To use, instantiate in your // app and call BrowseForFolder, which returns a PIDL. You can call // GetPathName to get the path name from the PIDL. // class CFolderDialog : public CWnd { public: CFolderDialog(CWnd* pWnd); ~CFolderDialog(); LPCITEMIDLIST BrowseForFolder(LPCTSTR title, UINT flags, LPCITEMIDLIST pidRoot=NULL, BOOL bFilter=FALSE); CString GetDisplayName() { return m_sDisplayName; } // helpers static CString GetPathName(LPCITEMIDLIST pidl); static CString GetDisplayNameOf( IShellFolder* psf, LPCITEMIDLIST pidl, DWORD uFlags); static void FreePIDL(LPCITEMIDLIST pidl); protected: BROWSEINFO m_brinfo; // used with SHBrowseForFolder CString m_sDisplayName; // display name of folder chosen BOOL m_bFilter; // do custom filtering? CComQIPtr<IShellFolder> m_shfRoot; // handy to have root folder static CALLBACK CallbackProc( HWND hwnd, UINT msg, LPARAM lp, LPARAM lpData); virtual int OnMessage(UINT msg, LPARAM lp); // internal catch-all // Virtual message handlers: override these instead of using callback virtual void OnInitialized() { } virtual void OnIUnknown(IUnknown* punk); virtual void OnSelChanged(LPCITEMIDLIST pidl) { } virtual BOOL OnValidateFailed(LPCTSTR lpsz) { return TRUE; } // wrappers void EnableOK(BOOL bEnable) { SendMessage(BFFM_ENABLEOK,0,bEnable); } void SetOKText(LPCWSTR lpText) { SendMessage(BFFM_SETOKTEXT,0,(LPARAM)lpText); } ... // etc (more wrappers—download for details) // Override for custom filtering. You must call BrowseForFolder // with bFilter=TRUE. virtual HRESULT OnGetEnumFlags(...) { return S_OK; } virtual HRESULT OnShouldShow(...) { return S_OK; } // IFolderFilter: used to do custom filtering. Download for details. DECLARE_INTERFACE_MAP() BEGIN_INTERFACE_PART(FolderFilter, IFolderFilter) STDMETHOD(GetEnumFlags)(IShellFolder* psf, ...); STDMETHOD(ShouldShow)(IShellFolder* psf, ...); END_INTERFACE_PART(FolderFilter) };

FolderDlg.cpp

#include "stdafx.h" #include "FolderDlg.h" #include <shlwapi.h> CFolderDialog::CFolderDialog(CWnd* pWnd) { memset(&m_brinfo,0,sizeof(m_brinfo)); m_brinfo.hwndOwner=pWnd->m_hWnd; // use parent window m_bFilter = FALSE; // default: no filtering SHGetDesktopFolder(&m_shfRoot); // get root IShellFolder } // Browse for folder. Args are same as for SHBrowseForFolder, // but with extra bFilter that tells whether to do custom filtering. LPCITEMIDLIST CFolderDialog::BrowseForFolder(LPCTSTR title, UINT flags, LPCITEMIDLIST root, BOOL bFilter) { TCHAR* buf = m_sDisplayName.GetBuffer(MAX_PATH); m_brinfo.pidlRoot = root; m_brinfo.pszDisplayName = buf; m_brinfo.lpszTitle = title; m_brinfo.ulFlags = flags; m_brinfo.lpfn = CallbackProc; m_brinfo.lParam = (LPARAM)this; // filtering only supported for new-style dialogs m_bFilter = bFilter; ASSERT(!bFilter||(m_brinfo.ulFlags & BIF_NEWDIALOGSTYLE)); LPCITEMIDLIST pidl = SHBrowseForFolder(&m_brinfo); // do it m_sDisplayName.ReleaseBuffer(); return pidl; } // Handy function to get the string pathname from pidl. CString CFolderDialog::GetPathName(LPCITEMIDLIST pidl) { CString path; TCHAR* buf = path.GetBuffer(MAX_PATH); SHGetPathFromIDList(pidl, buf); path.ReleaseBuffer(); return path; } // Handy function to get the display name from pidl. CString CFolderDialog::GetDisplayNameOf(IShellFolder* psf, LPCITEMIDLIST pidl, DWORD uFlags) { CString dn; STRRET strret; // special struct for GetDisplayNameOf strret.uType = STRRET_CSTR; // get as CSTR if (SUCCEEDED(psf->GetDisplayNameOf(pidl, uFlags, &strret))) { StrRetToBuf(&strret, pidl, dn.GetBuffer(MAX_PATH), MAX_PATH); dn.ReleaseBuffer(); } return dn; } ////////////////// // Free PIDL using shell's IMalloc // void CFolderDialog::FreePIDL(LPCITEMIDLIST pidl) { CComQIPtr<IMalloc> iMalloc; HRESULT hr = SHGetMalloc(&iMalloc); ASSERT(SUCCEEDED(hr)); iMalloc->Free((void*)pidl); } // Internal callback proc used for SHBrowseForFolder passes control to // appropriate virtual function after attaching browser window. int CFolderDialog::CallbackProc( HWND hwnd, UINT msg, LPARAM lp, LPARAM lpData) { CFolderDialog* pDlg = (CFolderDialog*)lpData; ASSERT(pDlg); if (pDlg->m_hWnd!=hwnd) { if (pDlg->m_hWnd) pDlg->UnsubclassWindow(); pDlg->SubclassWindow(hwnd); } return pDlg->OnMessage(msg, lp); } // Handle notification from browser window: pass to specific handler function. int CFolderDialog::OnMessage(UINT msg, LPARAM lp) { switch (msg) { case BFFM_INITIALIZED: OnInitialized(); return 0; case BFFM_IUNKNOWN: OnIUnknown((IUnknown*)lp); return 0; case BFFM_SELCHANGED: OnSelChanged((LPCITEMIDLIST)lp); return 0; case BFFM_VALIDATEFAILED: return OnValidateFailed((LPCTSTR)lp); default: TRACE( _T("***Warning: unknown message %d in CFolderDialog::OnMessage\n")); } return 0; } // Browser is notifying me with its IUnknown: use it to set filter if // requested. Note this can be called with punk=NULL when shutting down! void CFolderDialog::OnIUnknown(IUnknown* punk) { if (punk && m_bFilter) { CComQIPtr<IFolderFilterSite> iffs; VERIFY(SUCCEEDED(punk->QueryInterface( IID_IFolderFilterSite, (void**)&iffs))); iffs->SetFilter((IFolderFilter*)&m_xFolderFilter); } } ... // Standard MFC IUnknown not shown here — download source for details //////////////////////////////// IFolderFilter //////////////////////////// // Implementation passes control to parent class CFolderDialog (pThis) // BEGIN_INTERFACE_MAP(CFolderDialog, CCmdTarget) INTERFACE_PART(CFolderDialog, IID_IFolderFilter, FolderFilter) END_INTERFACE_MAP() STDMETHODIMP_(ULONG) CFolderDialog::XFolderFilter::AddRef() { METHOD_PROLOGUE(CFolderDialog, FolderFilter); return pThis->AddRef(); } STDMETHODIMP_(ULONG) CFolderDialog::XFolderFilter::Release() { METHOD_PROLOGUE(CFolderDialog, FolderFilter); return pThis->Release(); } STDMETHODIMP CFolderDialog::XFolderFilter::QueryInterface( REFIID iid, LPVOID* ppv) { METHOD_PROLOGUE(CFolderDialog, FolderFilter); return pThis->QueryInterface(iid, ppv); } // Note: pHwnd is always NULL here as far as I can tell. STDMETHODIMP CFolderDialog::XFolderFilter::GetEnumFlags(IShellFolder* psf, LPCITEMIDLIST pidlFolder, HWND *pHwnd, DWORD *pgrfFlags) { METHOD_PROLOGUE(CFolderDialog, FolderFilter); return pThis->OnGetEnumFlags(psf, pidlFolder, pgrfFlags); } STDMETHODIMP CFolderDialog::XFolderFilter::ShouldShow(IShellFolder* psf, LPCITEMIDLIST pidlFolder, LPCITEMIDLIST pidlItem) { METHOD_PROLOGUE(CFolderDialog, FolderFilter); return pThis->OnShouldShow(psf, pidlFolder, pidlItem); }

If you decide to use custom filtering, be careful because it overrides flags like BIF_RETURNONLYFSDIRS (return only file system dirs). Just for fun, I decided to implement BIF_RETURNONLYFSDIRS myself for FolderPick by manually disabling the OK button if the item selected is not a file system folder. To check for a file system object, you'd think the proper way would be to call IShellFolder::GetAttributesOf and look for SFGAO_FILESYSTEM. But when I tried this, "My Computer" had the SFGAO_FILESYSTEM attribute even though it's not really a file system folder! Go figure. The only reliable way I could discover to tell if a shell object is really a file or folder is to call GetPathName and check for an empty string. This is what I did in CMyFolderDialog to disable the OK button for non-folders. Download the source from the MSDN®Magazine Web site for details.

Finally, in order to help you understand what happens and when, I sprinkled CFolderDialog liberally with TRACE diagnostics. Figure 6 shows a sample run. You can turn the diagnostics on or off by setting a global variable CFolderDialog::bTRACE. Of course, the diagnostics appear only in debug builds. If you download the code, you also get a free copy of TraceWin to view the diagnostics without running under the debugger.

SHBrowseForFolder has lots of flags and features I haven't covered, but whichever features you decide to use, CFolderDialog alleviates the grunt work and lets you program SHBrowseForFolder the MFC way. Happy programming!

Send your questions and comments for Paul to  cppqa@microsoft.com.

Paul DiLascia is a freelance software consultant and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). In his spare time, Paul develops PixieLib, an MFC class library available from his Web site, www.dilascia.com.