October 1996
Wicked Code
Jeff Prosise Jeff Prosise writes extensively about programming in Windows and is a contributing editor of several computer magazines. He is the author of Programming Windows 95 with MFC (Microsoft Press). Love it or hate it, some things about MFC just can't be beat. One of the greatest benefits of writing Win32¨ apps with MFC is that you can use MFC classes as base classes for more ambitious classes of your own. Ever since I laid eyes on the MFC 4.0 CTreeView class, I've wanted to build a drive-view class that displays a hierarchical listing of drives and folders-something resembling the left pane of an Explorer window, but without printer folders and other non-file-system objects. I thought it would be easy. It turns out the devil is in the details, as is so often the case in programming. Among other things, you have to worry what happens when the user creates, deletes, or renames a folder in an Explorer or shell folder window while the drive view is displayed. A mediocre drive-view class would probably ignore latent changes to a drive's subdirectory structure. A robust one would sense the changes and update itself accordingly. For improved performance, a drive-view class should mimic Explorer by adding folder items to the tree view on the fly rather than scanning every folder on every drive the moment the class is instantiated. In short, writing an industrial-strength drive-view class is not a trivial undertaking. The class I wrote, CDriveView, will save you loads of development time. It's completely self-contained, and it uses low-overhead threads to monitor activity in the file system. You can use it as is, or you can use it as the starting point for more powerful classes of your own. If you've never before written a multithreaded MFC class or used Win32 file change notification objects, CDriveView offers a working tutorial on both. The source code for CDriveView appears in Figure 1. CDriveView is derived from the MFC CTreeView class, which takes the functionality of a TreeView control (in MFC, CTreeCtrl) and places it in a CView-like wrapper. You can call CTreeCtrl functions on a CDriveView by first calling CTreeView::GetTreeCtrl to obtain a reference to the underlying CTreeCtrl. You can call CView functions on a CDriveView directly and also take advantage of key CView overridables such as OnInitialUpdate. CDriveView overrides CTreeView::OnInitialUpdate and initializes the tree view with items representing the drives present in the system. Most of the work is done by the helper functions CDriveView::InitTree and CDriveView::AddDriveNode. The former uses the GetLogicalDrives API function to enumerate logical drives, and the latter is called once per logical drive to add a "node" (TreeView item) to the tree view. For its part, AddDriveNode uses the handy Win32 GetDriveType API to determine what kind of drive-floppy drive, local hard disk, network drive, CD-ROM drive, or RAM drive-it's dealing with. The drive type determines which image will represent the drive node in the tree view. The image is obtained from the image list assigned to the tree view in CDriveView::OnInitialUpdate. The bitmaps that make up the image list are contained in the file Drives.bmp. Initially, none of the drive items added to the CDriveView are expanded to show subitems, but each drive that contains at least one subdirectory is assigned a dummy child item that causes a button containing a small plus sign to appear to the left of the drive icon. Clicking the button expands the tree to show the folders in the drive's root directory. Just before the expansion occurs, the tree view sends a TVN_ITEMEXPANDING notification to its parent via a WM_NOTIFY message. An ON_NOTIFY_REFLECT macro in the class's message map reflects the notification back to the view and activates CDriveView::OnItemExpanding, which deletes the dummy item and replaces it with folders. The helper function DriveView::AddDirectoryNodes uses the Win32 FindFirstFile and FindNextFile APIs to enumerate subdirectories and CTreeCtrl::InsertItem to add items to the TreeView. OnItemExpanding is also activated when the user collapses a branch of the tree. When a branch collapses, OnItemExpanding does essentially the opposite of what it did earlier by deleting the subfolder items and adding a dummy item so the branch can be expanded again later. CDriveView's just-in-time approach to populating the tree view prevents it from having to delay the application to enumerate every folder on every drive each time a CDriveView is created. It also ensures that, if the user expands a branch in which a folder was added, renamed, or deleted since the CDriveView was initialized, the change will be reflected in the TreeView. Unfortunately, it doesn't help if a folder is created, renamed, or deleted in a branch that's already expanded. This is where multiple threads and file-change notifications come in. Win32-based applications can use the FindFirstChangeNotification API to get a handle to a file-change notification object. When a thread blocks on a file-change notification object with WaitForSingleObject or WaitForMultipleObjects, it is suspended in an efficient wait state until an event in the file system causes the object to become "signaled." Each time CDriveView::AddDriveNode adds a drive node to the TreeView, it calls CreateMonitoringThread, another CDriveView function, to create a worker thread whose relative thread priority is THREAD_PRIORITY_IDLE. That thread creates a file-change notification object that becomes signaled when a directory is created, renamed, or deleted anywhere on the specified drive. The thread then blocks on the notification object. When a file-change notification wakes it up, the thread posts a WM_USER message to the drive-view window with wParam identifying the drive on which the change occurred. CDriveView::OnDriveContentsChanged responds to the message by calling CDriveView::RefreshDrive to refresh the branches of the directory structure displayed in the TreeView. Thus, if you create, rename, or delete a folder outside of a CDriveView, the change is immediately reflected in the CDriveView-provided, of course, the affected folder is visible at the time. You can see for yourself how the threads work by examining the source code for CDriveView's CreateMonitoringThread, RefreshDrive, and ThreadFunc functions. The latter is a static member function that serves as the thread function for the threads launched by CreateMonitoringThread. Thread functions must be declared static if they're members of a class. Otherwise, the compiler will unwittingly add a this pointer to the function's parameter list and you won't like what happens. In the source code for ThreadFunc, notice that each worker thread blocks on two objects. One is the file-change notification object created by FindFirstChangeNotification; the other is a CEvent object named m_event that's part of CDriveView. m_event is a manual-reset event object that's initially unset. It's manual-reset because there will probably be several threads blocking on it, and an autoreset event object can't release more than one waiting thread. Setting m_event via SetEvent causes ThreadFunc's call to WaitForMultipleObjects to return immediately even if no change occurred in the file system. CDriveView::OnDestroy sets m_event just before the drive-view window is destroyed to terminate all running worker threads. It then calls WaitForMultipleObjects itself to wait for all the threads to terminate. When you write multithreaded MFC classes, you should make it a practice to terminate the threads that your class created before an object of that class goes away. Otherwise, the threads will continue to run after the object has been destroyed. So far, so good. There's just one little problem: file-change notifications don't work with floppy drives if the drive is empty when a thread blocks on the notification object. That's why AddDriveNode doesn't call CreateMonitoringThread for floppy drives. In Windows¨ 95, file-change notifications don't work with network drives, either (they do under Windows NT¨). So even though it's called for network drives, CreateMonitoringThread is ineffectual on those drives unless it's running under Windows NT. Removable-media drives complicate the picture even further. What happens, for example, if a TreeView is displayed for drive A: and the user removes the disk from the drive? Ideally, the drive node should collapse (and maybe even become disabled) to indicate that the drive is empty. But the system doesn't tell you when a floppy disk is removed, and if you use a timer or a low-priority idle thread to check the status of a floppy drive, you degrade performance by continually pinging the drive. CDriveView's solution to the problem of remote and removable-media drives is to perform a media check on a removable drive every time a branch is expanded or collapsed or the TreeView selection changes. Both CDriveView:: OnItemExpanding and CDriveView:: OnSelChanged call CDriveView::IsMediaValid and collapse the appropriate drive node if the function returns zero (which means either the drive is empty or the disk has been changed since the last time IsMediaValid was called). Media changes are detected by comparing serial numbers. If the drive passes the IsMediaValid test, the OnItemExpanding and OnSelChanged functions then call CDriveView::IsPathValid to verify that the path to the folder that's about to change appearance is still valid. A negative return value causes the folder item to be removed from the TreeView. Therefore, a folder deleted from a floppy disk won't immediately disappear from a CDriveView, but if it's clicked, expanded, or collapsed, it will go away. Enough about CDriveView internals; let's talk about using a CDriveView in your applications. There are two ways to put CDriveView to work. You can use it as is in a doc/view app by passing the document template a CRuntimeClass pointer to CDriveView, or you can derive your own class from it and use the derived class as your view. The DVDemo application in Figure 2 illustrates the first of these methods. DVDemo is an SDI application that uses a CDriveView to display a clickable map of the host PC's drive and directory structure (see Figure 3). Figure 3 Using CDriveView You can call CView functions on a CDriveView, and you can call CTreeCtrl functions if you first call GetTreeCtrl. CDriveView also adds a few functions of its own (see Figure 4). Its external interface consists of three public member functions ("operations" in MFC parlance) and one virtual function (overridable). DVDemo's frame-window class uses one of CDriveView's public function members, RefreshDrive, to implement the Refresh Drive commands in the Options menu. CDriveView Primer
Using CDriveView
Figure 4 CDriveView's External Interface
BOOLExpandPath(LPCTSTRpszPath,BOOLbSelectItem=TRUE)
Call ExpandPath to expand the tree to display a specified folder. A FALSE return usually means that pszPath specifies an invalid path name. The path name is case-sensitive, so this function will fail if pszPath points to "C:\My documents" and the actual path name is "C:\My Documents."
Return Value
TRUE if path was successfully expanded, FALSE if it was not.
Parameters
pszPath | String specifying the path to the folder to be displayed. |
bSelectItem | TRUE if the folder should be selected,FALSE if not. |
CString GetPathFromItem (HTREEITEM hItem)
Given the handle of an item in the tree view, GetPathFromItem returns a path name for the item-for example, "C:\My Documents."
Return Value
ACString with the fully qualified path name for the folder represented by hItem.
Parameters
hItem | Handle of the TreeView item. |
void RefreshDrive (UINT nDrive)
Call this function to programmatically refresh the view of a drive and its folders. If the drive supports removable media and the current view of the drive is invalid because the media has changed or been removed, RefreshDrive collapses the drive node.
Return Value
None
Parameters
nDrive | Drive number (0=A:, 1=B:, and so on). |
virtual void OnSelectionChanged (CString& strPathName)
Override OnSelectionChanged to respond to selection changes within the TreeView control-for example, to update the text in a status bar. The default implementation of this function calls the document's UpdateAllViews function to update other views of the document.
Return Value
None
Parameters
strPathName Path name for the selected folder.
CDriveView::GetPathFromItem converts a TreeView item handle (HTREEITEM) into a fully qualified path name. Say you need the path name for the folder that's currently selected in the drive view to process a menu command. Here's how you'd go about it:
HTREEITEM hItem =
GetTreeCtrl ().GetSelectedItem ();
CString strPathName =
pView->GetPathFromItem (hItem);
Converting a path name into an HTREEITEM is equally easy. First use CDriveView::ExpandPath to make sure the path to the folder you're interested in is expanded. Since ExpandPath by default also selects the folder whose path is expanded, you can then use CTreeCtrl::GetSelectedItem to retrieve the folder's HTREEITEM:
ExpandPath ("C:\\WINDOWS");
HTREEITEM hItem = GetTreeCtrl ().GetSelectedItem ();
You can customize CDriveView's behavior by deriving your own view class from it and overriding virtual functions. Suppose you'd like your drive view to appear with the path to the current drive and directory already expanded. Simply override OnInitialUpdate in the derived class, call GetCurrentDirectory to get the current drive and directory, and pass the path name to CDriveView::ExpandPath:
void CMyDriveView::OnInitialUpdate ()
{
CDriveView::OnInitialUpdate ();
char szPath[MAX_PATH];
::GetCurrentDirectory (sizeof (szPath), szPath);
ExpandPath (szPath);
}
It's important to call the base class's OnInitialUpdate function first, or else the view won't be initialized and there will be no items for ExpandPath to expand.
Another virtual function that's useful in a derived class is CDriveView::OnSelectionChanged. CDriveView uses message reflection to map TVN_SELCHANGED notifications, which indicate that the TreeView selection has changed to an internal handler named OnSelChanged. OnSelChanged, in turn, does some housekeeping and then calls OnSelectionChanged. The default implementation of OnSelectionChanged calls the document's UpdateAllViews function to update other views of the document, which is useful if you're writing an Explorer-like application that displays drives and folders in one pane and the contents of the currently selected folder in another pane.
Maybe your app's main window has a status bar, and one of the status bar's panes shows the path to the folder that is currently selected in the drive-view window. In that case, you can override OnSelectionChanged and update the status-bar pane each time the selection changes:
void CMyDriveView::OnSelectionChanged (
CString& strPathName)
{
CDriveView::OnSelectionChanged (strPathName);
AfxGetMainWnd ()->m_wndStatusBar.SetPaneText (
PANE_INDEX, (LPCTSTR) strPathName);
}
This example assumes that m_wndStatusBar is a public data member of the application's frame-window class. In real life, the status bar is usually a private or protected class member and the pane text is set by sending a message to the frame window, which in turn calls the status bar's SetPaneText function. Note that you can safely skip the call to the base class's OnSelectionChanged function if you don't need to call UpdateAllViews.
CDriveView also includes more than a dozen protected member functions that could be useful to you in a derived class. One, GetSerialNumber, returns either a disk's serial number or 0xFFFFFFFF if the drive is currently empty. Another, FindItem, searches a group of TreeView items for one that's assigned a specified text string. Refer to the CDriveView source code for further information.
Are there tough Win32 programming questions you'd like to see answered in this column? If so, email them to me at the address listed below. I regret that time doesn't permit me to respond individually to all questions, but rest assured that I'll read every one and consider each for inclusion in a future installment of Wicked Code.
Have a tricky issue dealing with Windows? Send your questions via email to Jeff Prosise: 72241.44@compuserve.com
From the October 1996 issue of Microsoft Systems Journal.