In Windows, there are two ways to display the "Browse For Folder" dialog box, which provides a constant metaphor for selecting folders. You can call SHBrowseForFolder(), as Brian showed you back in the March issue (see "Browse for a Folder the Non-COM Way"), or you can use COM interfaces exposed in SHDOCVW.DLL. In this article, Brian walks you through the COM way and shows how much simpler it is than the non-COM way.
I like to think that variety isn't so much the spice of life as it is the spice of programming. There's always more than one way to do anything, and browsing for a folder is no exception. So why learn another way? Because this one is powerful and easy.
To COM or not to COM? That is the question . . .
Doing things the COM way saves you from having to provide functionality yourself that other programs already offer. Without COM, to present the Browse for Folder dialog box, you fill in the BROWSEINFO structure with settings, flags, and values, and then call the SHBrowseForFolder() function. Simple, right? Actually, since the Shell allocates the data structures that it returns, you have to mess around with the Shell's allocator too. I wrote a helper function that took the primary settings and flags and returned a string containing the path to the selected folder:
CString& BrowseForFolder(HWND hWnd,
LPCSTR lpszTitle,
UINT nFlags);
It's a straightforward function, but it's 70 lines long, so I won't reproduce it here again. It's full of testing for this and checking for that and don't forget to free something else.
As I mentioned in the March article, SHDOCVW.DLL is a type library packed with lots of COM interfaces. It's a veritable "toy box" full of functionality for you and me to play with! I should warn you, however, that these goodies (well, some of them, at least) are undocumented. This means that Microsoft might very well swoop in and change things on us; that's exactly what Microsoft did with the arrival of Windows 98 and Internet Explorer 5.
With Windows 95 and IE 4.01 (but not IE 3.x), SHDOCVW.DLL exposes all of the functionality I'm going to show you. With IE 5 and Windows 98, things change a little. There's still a SHDOCVW.DLL and also a new file, SHDOC401.DLL. As you can guess from the name, the new file is for the 4.01 functionality, and the new stuff is in the new SHDOCVW.DLL. For example, the IShellDispatch interface is exported by SHDOCVW.DLL if you use IE 4.01 and by SHDOC401.DLL if you use IE 5.
If you're willing to use COM to browse for your folder, you'll find a COM method that's essentially identical to my BrowseForFolder() in IShellDispatch. Here's the declaration:
0
LPDISPATCH BrowseForFolder(long Hwnd,
LPCTSTR Title,
long Options,
const VARIANT& RootFolder);
Except for an extra parameter-RootFolder-and different return type (which we'll deal with later), the two functions are almost identical.
So, you're going to add browsing for a folder to an app, and you're going to do it the COM way. What's the first step? Your code will be invoking methods from IShellDispatch, so you should add a wrapper class for it to your project. I'll use my crystal ball to predict that you'll also need the Folder interface, so I'll show you how to add both at once. ClassWizard makes it a snap.
Open your project (I'm using the March sample) and choose View, ClassWizard. Click the Add Class… button. From the drop-down menu that appears, click From A Type Library (see Figure 1). The Import from Type Library dialog box appears. Browse to C:\WINDOWS\SYSTEM or C:\WINNT\SYSTEM32 (or wherever you've installed Windows), and select SHDOCVW.DLL if you have IE 4.01 installed or SHDOC401.DLL if you have IE 5.
From the list of interfaces that appears in the Add Class From Type Library dialog box, select both "IShellDispatch" and "Folder" (see Figure 2), and then click OK. The wrapper classes are created for you. Alas, the names conflict with those in EXDISP.H, so they should be renamed. Open SHDOC401.H, and use Edit, Replace to changes all instances of IShellDispatch to CoIShellDispatch and all instances of Folder with CoFolder. Then change the class names in the SHDOC401.CPP file, too. You can use any names you like; I put "Co" in each name to remind me that I'm using COM.
To save executable-file-size space, delete every member of the IShellDispatch class except for the constructors and the BrowseForFolder() method. Figure 3 shows your class view after this work. Curious about those deleted member functions? Watch for a future article on some of them.
Next, make sure that MFC support for Automation is included in the STDAFX.H file. This gives the program access to COleDispatchDriver. Go to STDAFX.H, and add this line after #include <afxext.h>:
#include <afxdisp.h>
// MFC Support for automation
Next, protect the SHDOC401.H file from getting included more than once. ClassWizard usually handles this, but whenever you generate a COleDispatchDriver-derived class from a type library, you must do it yourself. Before the first line of code in the file, add these lines:
#ifndef __SHDOC401_H__
#define __SHDOC401_H__
<![if !supportEmptyParas]> <![endif]>
#pragma once
Don't forget to put an #endif statement at the end of the file.
With the housekeeping out of the way, you can start using IShellDispatch to browse for a folder. I added these classes to the sample from March that used my homemade folder-browsing function. I hope you remember that I added a function to the application class to simplify writing message handlers for many buttons that browse to a folder. I'm changing the prototype of that function now, because we don't need to pass it a window handle, and we can pass it the root folder-the place where the browsing should begin. This is a big advantage of the COM approach. The new prototype is as follows:
CString BrowseForFolder(LPCTSTR lpszTitle,
UINT nFlags,
LPCTSTR lpszRoot);
Remember that lpszTitle doesn't specify the dialog box's title bar text, but rather text that you want to display inside the dialog box-for example, "Please click a folder, and then click OK." The nFlags argument corresponds to the ulFlags member of the BROWSEINFO structure. Some new flags were introduced in version 4.71 of SHDOC401.DLL, and they work fine when you call SHBrowseForFolder(), but not when you use COM. This is because the Folder dispatch interface can't deal with anything that's not a folder. Using them can make your program can choke and crash; unpredictable things happen when the user is able to select anything that isn't a folder. Adding an edit box and displaying files in the tree view complicate matters.
For those of you who missed the March article, Table 1 shows the safe values of nFlags (these can be combined!).
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. |
Table 2 shows the new flags with SHDOC401.DLL version 4.71 that don't behave-don't use them!
Flag |
Meaning |
BIF_EDITBOX |
The browse dialog box includes an edit control in which the user can type the name of an item. |
BIF_BROWSEINCLUDEFILES |
The browse dialog box will display files as well as folders. |
BIF_VALIDATE |
If the user types an invalid name into the edit box, the browse dialog box will call the application's BrowseCallbackProc() function with the BFFM_VALIDATEFAILED message. This flag is ignored if BIF_EDITBOX isn't specified. |
The new argument is a string that represents the root. CoIShellDispatch::BrowseForFolder() takes a const VARIANT& RootFolder as its last argument. Personally, I hate working with VARIANTs. I use COleVariant to hide a lot of VARIANTs' ugliness, and it comes in especially handy when you have a VARIANT argument that can masquerade as a BSTR. The COleVariant::SetString() function is a star in these cases. In CBrowseForFolderApp::BrowseForFolder(), I send a COleVariant object as the last argument to CoIShellDispatch::BrowseForFolder(), with the help of COleVariant::SetString(). Here's the whole function:
CString CBrowseForFolderApp::BrowseForFolder(
LPCTSTR
lpszTitle, UINT nFlags,
LPCTSTR
lpszRoot)
{
CoIShellDispatch
disp;
IDispatch* lpDispatch;
HRESULT hResult;
// Initialize COM and
create an instance of the
// Shell COM Class.
Note: The CLSID CLSID_Shell
// is already defined
in EXDISP.H.
hResult =
::CoInitialize(NULL);
hResult =
::CoCreateInstance(CLSID_Shell,
NULL,
CLSCTX_SERVER,
IID_IDispatch,
(void**)&disp);
COleVariant var;
var.Clear();
var.SetString(lpszRoot,
VT_BSTR);
lpDispatch =
disp.BrowseForFolder(NULL, lpszTitle,
nFlags, var);
// Initialize the Folder object and get its Title.
CoFolder
folder(lpDispatch);
CString str =
folder.GetTitle();
folder.ReleaseDispatch();
disp.ReleaseDispatch();
// Return the selected folder's title
return str;
}
Now you see why you wrapped the Folder interface earlier on. The lpDispatch pointer returned by the function is a pointer to a Folder automation object. One of the constructors for CoFolder can instantiate a new object based on an IDispatch pointer. Once you have a Folder object, call GetTitle(), and you have the title of the folder. Unfortunately, it's only the title, not the entire path. What if the user has two folders with the same name on the same machine? And also, how can one tell from just a title what drive the folder is in or what folder it's a subfolder of? There isn't a property of the Folder object that holds information on the folder's path name, so if you need it, go back to the non-COM way until some new version of Folder has the method added.
Since the prototype of the application function changed, the message handler for the browse button will have to change, too. I added an edit box and member variable, m_strDefaultRoot, to the dialog box so that you can specify the root folder. Now let's look at how the message handler for the Browse button has changed (not much):
void CBrowseForFolderDlg::OnBrowse()
{
CBrowseForFolderApp*
pApp =
(CBrowseForFolderApp*)AfxGetApp();
// load root folder choice from edit box
if (!UpdateData(TRUE))
return;
m_strFolderName =
pApp->BrowseForFolder(
_T("Please select a folder below, and then click OK."),
BIF_RETURNFSANCESTORS|BIF_RETURNONLYFSDIRS,
m_strDefaultRoot);
UpdateData(FALSE);
}
![if !supportEmptyParas]> ![endif]
When you run the sample, it looks like Figure 4. The edit box above the Folder section lets you specify the path name of the folder you want to be the root of the Browse for Folder dialog box's directory tree. And voila! Now you have both the non-COM way and the COM way of doing things!
Notes from the rear
The BrowseForFolder() method isn't the only method of IShellDispatch-there's more. And there are lots of other interfaces, too, in SHDOC401.DLL (or SHDOCVW.DLL, if you're using IE 4) that give Windows programmers a lot of nice toys to play with. Stay tuned for more.
Check the sample code included in this month's Subscriber Downloads at www.pinpub.com/vcd. I strongly recommend that you compare it to the March code, which is included with this month's file, to see how the two versions work, because looking at both versions of the code can really give you the full flavor of how the COM way and the non-COM way are different.
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.