Keith Bugg
Need to display data both graphically and in a hierarchy? The new tree control in Windows 95 and NT is just what the doctor ordered. Here’s how to climb this handy interface tool.
PRIOR to 1994 B.C. (Before CTreeCtrl), you had to work pretty hard to display data that was related in a hierarchy. It was a teeth-gnashing, Jolt-guzzling kind of job. The advent of the new common controls for Windows 95/NT has changed this situation. The class CTreeCtrl included in the new 32-bit common controls allows you to display hierarchical data and a lot more. In this article, I’ll delve into the theory and operation of this control and illustrate it with an example program you’ll find on the Developer’s Disk. The program uses a tree control to find all the directories and files on the root of the user’s C drive, and displays them accordingly. For the sake of simplicity, the program doesn’t scan beyond the first two levels, but you can easily extend the source to generate a complete directory tree. Figure 1 shows the control in action.
Figure 1. Output from the sample program.
The idea behind the tree control is based on its namesake-the tree. Just as a tree has a root, a trunk, limbs, and leaves, so your data is similarly structured and each “level” can be represented by a unique bitmap. In addition, the bitmap can be modified on the fly to represent a change of state. For example, as shown in Figure 2, whenever you click on a directory icon in the example program, I change the bitmap from a closed folder to an open folder.
Figure 2. Icon change of state.
Although the tree control draws all the lines when the data is collapsed or expanded, it’s up to you to manage the images. Because tree controls make considerable use of bitmap images, let’s take a closer look at how they’re used and managed. To begin, you’ll have to create (or import) all the different images you plan to use. My program uses four distinct images: the hard disk icon, the closed directory folder icon, the open (or selected) folder icon, and a generic file icon.
These images are stored in a derived object from the class CImageList. An image list is not a control, but rather a drawing tool that can be thought of as a glorified bitmap manager. Another analogy for image lists is to think of them as a roll of film. Just like a roll of film, image lists are a stream of bitmaps that come with their own API functions you can call to perform most of the work for you. And just as a roll of film has a fixed size (such as 35mm), so the image list has a maximum height of 32,668 pixels. In other words, an image list bitmap can range in size from a single 32K by 32K pixel bitmap to 32,668 bitmaps, each one pixel wide. To determine the maximum number of images an image list will support, just divide 32K by the width of one of your images. When you need a particular image, you simply reference it by its index in the image list.
The images you retrieve from your image list will be either masked or unmasked. The masked variety includes transparent pixels so that the background appears to shine through-just like an icon on the Windows 95 Desktop. On the other hand, the unmasked variety overwrites whatever is under it. For working with tree view controls, you’ll probably want to use the masked version.
Your tree control will need a supporting cast of special structures and unique data objects:
HTREEITEM¾A handle to an item in a tree control
TV_ITEM¾Structure for reading/writing tree item attributes.
TV_INSERTSTRUCT¾Structure used to add items to a tree control.
Of these, the last two are the most important and most frequently used. They also work in tandem-the TV_ITEM is a “child” of the TV_INSERTSTRUCT “parent.” In general, the basic approach to programming a tree control can be summarized as follows:
Define the control’s styles.
Define an image list.
Define members of a TV_ITEM structure.
Insert the TV_ITEM into a TV_INSERTSTRUCT.
Let’s examine each of these steps and then look at the sample program.
Objects of class CTreeCtrl support several styles, all of which have a profound effect on the way a control looks and behaves. You’ll most likely assign one of the following styles to a tree control:
TVS_HASBUTTONS. This style causes the control to display a box/button with a plus (+) sign in it when the item is collapsed and a minus (-) sign when it’s expanded. Clicking one of these buttons expands or collapses the item accordingly. Figure 1 shows this style in action.
TVS_EDITLABELS. This style allows the user to edit the labels associated with the tree items.
TVS_HASLINES. This style will display a dotted line between the items to show the relationship between them; the tree control in Figure 1 also has this style.
TVS_HASLINESATROOT. This style tells the tree view control to draw lines linking child items to the root of the hierarchy. This style is also used with the control in Figure 1.
There are additional but less frequently used styles. For an overview of all the styles, consult the Visual C++ documentation.
Your next step concerns the image list, which is where you’ll define the images that will be used in the tree control. You’ll need to create images that show the tree control item in its normal state plus any other special conditions (such as when the item is selected). The tree control requires these images to be of the type HICON and not HBITMAP as you might have expected. In the example program, I instantiated an object of class CImageList with the name m_ImageList. I then trapped the WM_CREATE message and used the Create() method to create the actual image list:
int CTreeDlg::OnCreate( LPCREATESTRUCT lpCreateStruct) { if (CDialog::OnCreate(lpCreateStruct) == -1) return -1; // TODO: Add your specialized creation code here // Create the image list you will need m_ImageList.Create(BITMAP_WIDTH, BITMAP_HEIGHT, FALSE, // list does not include masks NUM_BITMAPS, 0 ); // list will not grow HICON hIcon;
Load the icon images when the image list is created. The order in which these are added is very important from a project management point of view because you’ll index into the list to use an appropriate image. For example, the icon for the root of the tree is the symbol for a hard disk drive-its index should be zero in your list. The next level down is a closed directory, with an index of 1. This makes it easy to relate the meaning of the image with the position in the tree control’s hierarchy. Here’s the code for this step (you might want to add some error traps and ASSERT statements here since I’ve omitted these for the sake of brevity):
// // load the icon images and add them // to the image lists // hIcon = ::LoadIcon (AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_HARDDRV)); iDiskImage = m_ImageList.Add(hIcon); // // The order in which icons are added is // related to the hierarchy level!! // hIcon = ::LoadIcon (AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_FOLDER_CLOSE)); iDirectoryImage = m_ImageList.Add(hIcon); hIcon = ::LoadIcon (AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_FOLDER_OPEN)); iFolderOpen = m_ImageList.Add(hIcon); hIcon = ::LoadIcon (AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_FILE)); iFileImage = m_ImageList.Add(hIcon); // // Make sure all of the bitmaps were added // if (m_ImageList.GetImageCount() < NUM_BITMAPS) return -1; return 0;
Now you have a control, assigned it some styles, and created the image list. The next step is to build the “leaves” of the tree. For this you’ll need the TV_ITEM structure. This is used to set and retrieve the attributes of a tree item. Its prototype is as follows (I’ve added descriptive comments for each member and discuss the less-than-obvious meaning of some of the members):
typedef struct _TV_ITEM { tvi // specifies which members are valid UINT mask; // handle of tree item in question HTREEITEM hItem; // current state of the item // (high-lighted and so on) UINT state; // the bits of the state member that are valid UINT stateMask; // text to display next to an item LPSTR pszText; // number of characters in pszText int cchTextMax; // index in CImageList for normal state int iImage; // index in CImageList for high-lighted state int iSelectedImage; // flag indicating if an item has children int cChildren; // user-defined 32-bit value for the item LPARAM lParam; } TV_ITEM, FAR *LPTV_ITEM;
The mask member is very important-it defines which of the other members have meaning. It’s most important when retrieving information from the control. Only the items you specify in the mask member are valid when reading. The various values that a mask can have (these can be ORed together) are summarized in Table 1.
TVIF_CHILDREN The cChildren member is valid.
TVIF_HANDLE The hItem member is valid.
TVIF_IMAGE The iImage member is valid.
TVIF_PARAM The lParam member is valid.
TVIF_SELECTEDIMAGE The iSelectedImage member is valid.
TVIF_STATE The state and stateMask members are valid.
TVIF_TEXT The pszText and cchTextMax members are valid.
The cChildren member acts as a flag indicating whether the associated tree item is at the end of the line or has something under it. When this member is 0, it means there are no children. The item is the parent of another, subordinate item when it is 1 or greater. Finally, the member can have the reserved word value I_CHILDRENCALLBACK, which indicates the parent window stores the index of the child node. In this case, the control must send a TVN_GETDISPINFO message to the parent whenever it needs the index of the child item.
Now that the items are defined, they can be added to the control. This requires the TV_INSERTSTRUCT structure. Again, I’ve added comments to the prototype to help illuminate its members:
typedef struct _TV_INSERTSTRUCT { HTREEITEM hParent; // the handle of the parent item; // use TVI_ROOT or NULL for the root HTREEITEM hInsertAfter; // the handle of the item after which // the item is to be added TV_ITEM item; // information about the item being added } TV_INSERTSTRUCT;
In addition, hInsertAfter can have one of three values:
TVI_FIRST. Insert the item at the beginning of the list.
TVI_LAST. Insert the item at the end of the list.
TVI_SORT. Insert the item into the list in alphabetical order.
You’ll find both of these structures used in a method called AddOneItem() in the sample program. Let’s start wrapping things up by looking at this example.
The tree control code in the sample program is located in files TREEDLG.CPP and TREEDLG.H, which define the dialog box containing the tree control. This dialog box includes an “invisible” list box where the WS_VISIBLE property has been omitted. I use this control to temporarily hold the names of the directories and files. I trapped the WM_INITDIALOG message, used _chdir() to change to the root of the C: drive, and searched the drive for all directories. I placed the search result in the invisible list box, which in the sample has the member variable m_Invisible:
// hard-code Drive C here (again!) _chdir("c:\\"); m_Invisible_LB.Dir(DDL_DIRECTORY,"*" ); // // Associate the image list with the tree //
Since I built the image list when I overrode WM_CREATE, it’s all right to go ahead and associate it with the tree control; I then call AddTreeViewItems(). This method adds the icon for the “C” drive to the control, then adds all the cached directories in the invisible list box. As each directory is appended to the list the code changes to that path and finds all the files within, adding them to the control. I chose to stop the recursion at this level but you can easily modify this code to scan any additional subdirectories.
m_Tree.SetImageList(&m_ImageList,TVSIL_NORMAL); // // Add the directories to the tree view control // AddTreeViewItems(); // // Now get just the files, no directories // // Flush the invisible listbox m_Invisible_LB.ResetContent(); // Save file names in list box m_Invisible_LB.Dir(0x0 | !DDL_DIRECTORY,"*.*" ); char lpFileName[_MAX_PATH]; // working variable // Initialize the working variable memset(lpFileName,0,sizeof(lpFileName)); for(int i=0; i < m_Invisible_LB.GetCount(); i++) { // Get the file name and add it to the tree control m_Invisible_LB.GetText(i,lpFileName); AddOneItem(htDirectory, lpFileName, (HTREEITEM)TVI_LAST, iFileImage,FALSE); }
The rest of the code, which is available on the Developer’s Disk, is devoted to finding the files using _findfirst() and _findnext(). In each case, AddOneItem() appends items found to the tree control. The arguments for this method are the handle of the tree control, the text to display with the node, the handle of the node where the new item is to be inserted, the CImageList index of the icon to display, and a flag indicating whether the node has any children:
HTREEITEM CTreeDlg::AddOneItem(HTREEITEM hParent, LPSTR szText, HTREEITEM hInsAfter, int iImage, BOOL bChildren) { HTREEITEM hItem; // return value TV_ITEM tvI; // item structure TV_INSERTSTRUCT tvIns; // item insert structure // *** set tvI.mask member *** if(bChildren) tvI.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM | TVIF_CHILDREN;//item has children else // item has no children tvI.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM; // set text, text length, image tvI.pszText = szText; tvI.cchTextMax = lstrlen(szText); tvI.iImage = iImage; // get type of icon to display if(bChildren) // selected a folder, OK to change bitmap if(hParent != NULL) tvI.iSelectedImage = iFolderOpen; else // selected the hard drive icon, //don't change bitmap!! tvI.iSelectedImage = iImage; else tvI.iSelectedImage = iImage; tvI.cChildren= 1; // allow one more level down // do the insert tvIns.item = tvI; tvIns.hInsertAfter = hInsAfter; tvIns.hParent = hParent; hItem = m_Tree.InsertItem(&tvIns); return (hItem); // return (add error trap here) }
Notice the BOOL flag bChildren-the value of this critter plays an important role in determining the settings for the tvI.mask element and in selecting the icon to display. For more sophisticated implementations of the tree control, you’ll probably want to change the bChildren flag to an integer value (call it iFlag) and use a switch() statement to manage icon selection. For example, setting iFlag to 0 might mean a .TXT file. A setting of 1 might be an .EXE file, 2 might be an .INI file, and so on.
Personally I found the tree control a stern taskmaster. It took time to get the hang of it-the paucity of sample code was my greatest obstacle. It took a lot of head-scratching, plus some trial and error, to come up with a working example. I strongly encourage you to examine the sample code in detail, especially the AddOneItem() method, since it uses the important structures.
Besides the obvious use of a tree control for disk structures, it would be useful in plenty of other types of data display. For example, you could use it to build a custom class inspector: the base class could be shown at the root, while derivatives would appear in the nodes. Or how about creating a fancy index into a WinHelp document or a database browser? Any time you have hierarchical data, you have an opportunity to spice up the user interface with the tree control.
Keith Bugg has been writing Visual C++ applications since version 1 was released. Keith has taken so much grief about his name he’s thinking of changing it to Viruss. When he isn’t programming, Keith enjoys brewing beer and riding his ATV. tristar@qsystems.net, http://www.qsystems.net/tristar/tristar.htm.
To find out more about Visual C++ Developer and Pinnacle Publishing, visit their website at: http://www.pinppub.com/vcd/
Note: This is not
a Microsoft Corporation website.
Microsoft is not responsible for its content.
This article is reproduced from the October 1996 issue of Visual C++ Developer. Copyright 1996, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. Visual C++ Developer is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call (800) 788-1900 or (206) 251-1900.