October 1996
Paul DiLascia is a freelance software consultant specializing in training and software development in C++ and Windows. He is the author of Windows++: Writing Reusable Code in C++ (Addison-Wesley, 1992). QI wasted half a day trying to get multiple items selected with a TreeView control. Can you tell me how to set what seems like an easy style bit? Jean Davy CompuServe A(For ease of reading, I will refer to TreeView and ListView controls as tree and list controls throughout this column.) First off, the tree (and list) common controls are notoriously difficult to work with. A number of people have asked me, for example, how to make a list control that highlights the entire line of a selected item instead of just the first column. The solution to this (you can find it on MSDN or one of the Microsoft¨ forums) involves hundreds of lines of convoluted code that only proves how unwieldy these controls are. (The new common controls DLL fixes this. Strohm Armstrong begins a two-part series on the new controls in this issue-Ed.) The unofficial word from Redmond is that the list and tree controls were originally written to provide functionality required by the Windows¨ Explorer, and nothing more. They succeeded in that regard. That said, tree and list controls are not entirely unmanageable. I was able to implement CMultiTreeCtrl, a tree control that does multiple selection by handling selections manually. I wrote a simple program, MTREE, to test it. CMultiTreeCtrl is just a baby class that illustrates the basic principle of handling selection in a tree control. It doesn't provide an easy way to retrieve the selected items, which any real CMultiTreeCtrl would surely have. While I tested CMultiTreeCtrl as thoroughly as possible, I can't vouch for it 100 percent because I am, after all, dealing with system code. Caveats aside, CMultiTreeCtrl is a good beginning-enough to get you started on multiple selection. CMultiTreeCtrl works like a normal tree control when the user clicks the mouse on an item; but if the user clicks while holding the Control key down, CMultiTreeCtrl adds the item to the selection without deselecting the other items. If the item is already selected, Control-click deselects it as per the normal Windows 95 User Interface guidelines. Figure 1 shows MTREE with multiple items selected. Figure 1 MTREE multiple selection sample When I first tried to implement CMultiTreeCtrl, I figured I should intercept TVN_SELCHANGING and TVN_SELCHANGED. Tree controls send these WM_NOTIFY codes when the user is about to select a new item, or already has. After getting nowhere I realized that multiple selection is really a mouse feature, better implemented in OnLButtonDown and OnLButtonUp. I used the list control as a model to base my GUI on since it supports multiple selections. When CMultiTreeCtrl gets a mouse-button down event, it forks to one of two paths depending on the state of the Control key. If the Control key is down, my code notes the fact, sets the focus to my tree control, and returns immediately without calling the default handler. This is important, since the default handler would do the normal single-selection thing. My code calls SetFocus to mimic the behavior of the default handler. This makes sure the focus rectangle gets drawn around the selected item. The main action of my control happens in the mouse-button up event. This keeps the feel of the control consistent with the way multiple selection works in a list control. When the Control key is not down, CMultiTreeCtrl lets the tree control do its own thing, but with one minor modification: if the user clicks over an item, my code deselects all currently selected items. Then, when my code passes the tree control the mouse-button down event, the tree control does its normal selection thing. To deselect all items, I wrote a helper function, SelectAll, that selects or deselects all items in the tree. SelectAll uses CTreeCtrl::SetItemState to do the actual selecting or deselecting. This function seems straightforward, but some programmers get confused because its arguments are a bit odd. Besides the handle of the item you want to select, SetItemState expects a state value and a state mask: The mask tells SetItemState which state flags you want to set. For example, TVIS_SELECTED indicates the selection state andTVIS_FOCUSED indicates the focus state. The state value contains the actual bit values for the flags specified in the mask. This is where people sometimes get into trouble. You might try writing to set the selection state on. If so, you would then have to beat your head against the wall trying to figure out why it doesn't work. The problem is that TRUE has value 0x0001, which does not coincide with TVIS_SELECTED. The nState parameter is a bitfield, not a Boolean value. Every bit of nState matters. The correct way to select an item (turn on the TVIS_SELECTED bit) is To turn the bits off, use Once you understand how SetItemState works, CMultiTreeControl::SelectAll is straightforward. It calls itself recursively to select or deselect all the items. The caller must pass uSelState as an argument telling SelectAll whether to select or deselect. Note that GetChildItem returns NULL if the item has no children, in which case SelectAll returns without doing anything. So much for when the mouse button goes down. What happens when Jane J. User releases it? CMultiTreeCtrl::OnLButtonUp is where most of the action happens. If the Control key was pressed when the mouse button went down, then my code sets the current selection to the item under the mouse cursor and twiddles the item's TVIS_SELECTED bit. Even though my code calls SetItemState to change the TVIS_SELECTED state, it still calls CTreeCtrl::SelectItem because that's the easiest (and most future-compatible) way to set the focus item. Once that's done, my code restores the state of the previously selected item since the tree control deselects the old item whenever you call SelectItem. To test CMultiTreeCtrl, I wrote a simple program. It displays a dialog box that lets you append or insert items into a list control so you can practice multiple-selecting. I also added buttons to select and deselect all the items in one fell swoop. My dialog class, CMyDialog, is more or less obvious. The only interesting function is OnShowSelection, which uses an AppendItemNames helper function. AppendItemNames recursively calls itself to append the names of the selected items to a CString for display in a message box. A real-world CMultiTreeCtrl should have an iterator or some other method to let programmers iterate over the selected items. You didn't tell me anything about your app, but there's another issue that may be important: does it make sense to select items at different levels in the tree? For example, in the classic file/directory model of Explorer, does it make sense to let the user select both files and directories? The answer depends on the operations you intend to make available. If the operation is Delete, it makes sense since you can delete both files and directories. (Of course, if the selected file was under the selected directory, you would have redundancy, and it would be a great idea to prompt the user before deleting anything. But I digress.) If the operation is Print, this would not make much sense, since you don't print a directory like you print a file (and most people don't want listings of a directory printed anyway). If it doesn't always make sense to select items of different types, you must then decide whether to disallow the selection in the first place or display some kind of error. If you want to prevent selection, the implementation will be more complex than I've shown. CMultiTreeCtrl may look easy, but I assure you the solution came only after several false starts and many hours spent spelunking. I had to trace messages, infer invisible behavior, and pray. The documentation for this sort of stuff falls somewhere between useless and misleading. Extending common controls is like performing brain surgery on a being with alien anatomy: you have no idea where to slice and splice, and most times you end up with a dead app. Fortunately, with software you can always reboot. QI have a form-based application written using MFC's CFormView. I've taken great care to design my form so it looks good, but when the user resizes the form, Windows always paints it aligned to the top-left corner, and there's a lot of background space in the lower-right that looks bad. Is there some easy way I can center the form when the user resizes the window? I noticed a new Windows 95 style flag, DS_CENTER, but that doesn't seem to work. AThis is not that hard to do. You just need to know a little about how MFC, forms, and Windows work. The problem has to do with the way Windows paints forms; the solution requires knowing a little about MFC. When you use CFormView as your view class, your view is not a normal MFC view window class but a Windows dialog. CFormView overrides CWnd::Create to create a child dialog instead of an AfxFormOrView window, which is what it would normally do. So now you have a normal Windows dialog as your view. When the user resizes the main window, MFC does all the normal stuff, including resizing the dialog/view view, before Windows repaints it. Regardless of the size of the dialog window, Windows always paints dialogs using the upper-left corner as the origin. All the coordinates of your dialog controls are relative to the upper-left corner of the dialog. Hence the dialog appears aligned to the top and the left (see Figure 2). Figure 2 A view paints the entire client area You might think the DS_CENTER style flag (new for Windows 95) would correct this problem, but DS_CENTER is designed to center a popup-only dialog box in the whole screen at WM_INITDIALOG time (actually, it centers in the working area, which is the screen minus the system tray). Alas, it doesn't center a child dialog in its parent window as you might hope. The easiest solution I can think of is to simply move the view/dialog. Normally, a view always fills the client area of its main frame (minus whatever toolbars and status lines are displayed). But there's no reason the view can't be smaller! If you could shrink the size of the view and move it down and to the right the appropriate distance, the form would appear centered at all times (see Figure 3). The only trick is, when do you adjust the view? Figure 3 Centering a dialog in a viewer One of the important virtual functions in CFrameWnd is RecalcLayout. MFC calls this function internally whenever it needs to calculate the layout of the windows in the main frame, which normally means the toolbars and view. For example, MFC calls RecalcLayout when the user sizes the window or hides or shows a control bar. The algorithm MFC uses is this: using the main frame's client area as the "universe," first position all the control bars at the top or bottom as per their MFC style flags (CBRS_ALIGN_TOP, CBRS_ALIGN_BOTTOM, and so on), then use whatever space is left over for the view. MFC even uses Begin/EndDeferWindowPos so all the windows move at once. Since RecalcLayout is virtual, you can override it. This is what I did in CFORM, a program that displays a centered form. Whenever MFC calls RecalcLayout, control flows to my overloaded version (because it's virtual). First, I call the base class CFrameWnd::RecalcLayout to let MFC do its thing. At this point, MFC has sized the view to fill the client area minus control bars. It's a trivial matter to compare the size of the view window with the size of the form; if the view is bigger, I move it appropriately (down and to the right for half the difference in width and height). The only trick is getting the size of the form. The width and length of the form are stored in the dialog template in dialog units-you have to convert them, which is no fun. Is there any easy way to get the width and length of the dialog in pixels? GetTotalSize is just what you need: this ScrollView function returns the total size, in pixels, of the document, which in this case is the form. Just what the doctor ordered. Naturally, once I implemented all this I just assumed it would work the first time-I was ready to write the article and ship it. Fortunately, I tested the code. Guess what? It had a bug. Apparently I was so proud of myself for figuring out CScrollView::GetTotalSize that I overlooked a rather obvious problem. If I move the view down and to the right, what happens to that vacant area in the upper-left? It never gets painted! As the user sizes the window, whatever happened to be there stays there (see Figure 4). Figure 4 Roach Motel--Didn't handle OnPaint! Fortunately, the fix for my bug is easy: just handle OnPaint and repaint that pesky L-shaped region. I used PatBlt, the fast way to paint a rectangle with a given brush. To be a good citizen, I didn't use gray or any hardwired color, but GetSysColorBrush(COLOR_3DFACE), which returns an HBRUSH for the 3D face color, the color Windows uses to draw forms. (That's for Windows 95; in Windows 3.1 use COLOR_WINDOW.) Figure 5 shows the full code. Most of the action is in MainFrm.cpp. Just so you can see what's happening more easily, I added commands to turn centering on and off, and also the paint fix-up code. The solution I've shown preserves the relative placement of the controls, which may or may not be desirable. If the user makes the dialog very big, he or she will end up with a little group of controls in the center of a vast gray sea, which still looks a little shoddy. There are two possible fixes for the sizing problem. First, you could override CView::OnSize to move all the controls relative to the new size of the form (which would completely obviate the solution above). Second, you could override OnMinMaxInfo to disallow sizing above or below a certain size. Frankly, I think the latter approach is best; most dialogs are designed for a particular window size, and they simply don't look right when you make them too big or too small. My recommendation for most form-based apps is to disallow sizing entirely. This is consistent with dialogs, which usually cannot be sized. Have a question about programming in MFC or C++? Send it to Paul DiLascia at 72400.2702@compuserve.com. UINT SetItemState(HTREEITEM hItem, // Item to modify
UINT nState,// New state value
UINT nStateMask); // Which bits
// of nState
// to use
SetItemState(hItem, TRUE, TVIS_SELECTED);
SetItemState(hItem, TVIS_SELECTED, TVIS_SELECTED);
SetItemState(hItem, 0, TVIS_SELECTED);
while (hItem) {
SetItemState(hItem, uSelState, TVIS_SELECTED);
SelectAll(GetChildItem(hItem), uSelState);
hItem = GetNextSiblingItem(hItem);
}