Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Multi-State Checkbox Tree Views


May 2002/Multi-State Checkbox Tree Views

The “tree view” control used in the Windows Explorer left pane is a familiar interface. Among the Windows common controls, the CTreeCtrl class provides this functionality. A useful feature in the tree view is its item’s checkbox that can be marked as a checked or unchecked state. For example, an installation program can use a tree view to work interactively with the user selections and then process the collected items in groups. However, sometimes the bi-state checkbox may not satisfy your needs where three or more states are required. Also in programming, it’s frustrating there is no notification called TVN_CHECKCHANGED, to inform you which item is checked and what is its current state in action. Besides, you could come across some troubles in the initialization of a simple checkbox tree view.

In this article, I will extend the bi-state checkbox in a tree view to the multiple states. I first create a class derived from CTreeCtrl to make the multi-state checkbox available, where I also create a custom message to notify the state change. Based on this class, I implement the three-state checkbox tree view (checked, unchecked, and partial-checked); checking an item automatically adjusts the logical states for its children and parent items. You can easily get this functionality by coupling the class in your application, and learn how to manage multiple states to fit your requirements.

About Check States

Generally, a tree view control represents a window that displays a hierarchical list of items, such as entries, files, folders, and combinations. Each item consists of a label, an optional bitmapped image, and an optional checkbox. Three categories of images are available in the tree view (item image, overlay image, and state image). Each item can have a list of sub items associated with it, and allows the user to expand or collapse the sub item list.

Every item in the tree view has a current state such as selected, disabled, expanded, bold, and etc. For the most part, a tree view automatically sets an item’s state to reflect the user’s action. However, you can also access an item’s state by using member functions in CTreeCtrl to set/get item states, so as to make more flexible use in the tree view. For details, you can see the state definition in the SDK documentation or commctrl.h in VC++.

A tree view with the TVS_CHECKBOXS style can add a checkbox to each item on the left side of its label, or its item image if any. Likewise, in addition to clicking a checkbox to change its state, you can use the function SetCheck()/GetCheck() to access an item’s CHECK state. In this article, I always discuss the item’s CHECK state, rather than the previously mentioned ITEM state. Thus any following “state” should refer to the “check state”.

How I Come To This Land

My first try was creating a dialog-based MFC executable skeleton. In the dialog template in resource editor, I placed a tree view control and in the Properties sheet|More Styles page, I checked the “Check boxes” button to enable the TVS_CHECKBOXS style. Then in OnInitDialog(), I added a few items to make a tree and called SetCheck() to let some items be checked. For simplicity, I didn’t put any image list involved. When all done, I ran the program and saw the tree view as shown in Figure 1 on my Win98SE system.

The result seems unexpected. First, no item has been checked. Second, the annoying horizontal scroll bar cannot turn off unless you disable it in resource. Third, some computers show checkboxes overlapped (Figure 1) and some don’t.

By searching MSDN, I found a description on TVS_CHECKBOXES, which claims that you must set the TVS_CHECKBOXES style after creating the tree view control and before you populate the tree; otherwise, the checkboxes might appear unchecked. This doesn’t fit my problem, as I haven’t called a creation method there.

Another vague point is inconsistency in the Microsoft implementation of TVS_CHECKBOXES. The documentation says that TVS_CHECKBOXES is used since comctl32.dll version 4.70(MS-IE 3.0 or later) where it sets a state image list containing two images, image 1 for unchecked and image 2 for checked, and a checkbox is displayed only if an image is associated with the item. While in comctl32.dll version 5.80(MS-IE 5.0 or later), Microsoft displays a checkbox even without a state image list associated.

I was reluctant to spend time to delve into comctl32.dll. However, this prompts me to try my own state image list to see what happens. Surprisingly easy is the outcome of the remedy: either using your own state image list or item image list could avert the previous hassles. Also the findings lead me further to the extension of multiple check states in a tree view.

Multiple Check States

To the best of my knowledge in research, I can conclude that the multiple states in a checkbox depend on the images loaded in a tree view’s state image list, and obey the following regulations:

R1 The number of state images determines the number of the states, and each image corresponds to a state (exclude the image zero).

R2 The state image uses one-based indexes for states, and the index zero for non-state (disregard the zero-indexed image).

R3 The check state transfers to the next state in circulation, from state 1, to state 2,..., to state n, and goes back to state 1.

R4 The default state is always state 1.

For example, loading the IDB_5STATES bitmap in Figure 2 lets you use five states represented by the five images, a green circle for state 1, a blue square for state 2,..., and a gray parallelogram for state 5. Notice the first solid square that I put purposely to denote the ignored image zero. Now I can make a five-state tree view as shown in Figure 3, where I set the “My System” item to non-state (index zero), “Computers” to state 1, “Windows” to state 2, and so on. You can click on a state image to go through five states roundly. The edit box at the bottom responds your click by showing the current clicked item name and its state.

This example is implemented with the class CMutiChkTreeCtrl derived from CTreeCtrl. mtchtree.h and mtchtree.cpp (available online) show its interface and implementations. Let’s first take a look at the virtual function PreSubclassWindow() that is an ideal place to adapt a tree view to obtain multiple check states. I add the style TVS_CHECKBOXES to guarantee the tree view with checkboxes. The other styles are recommended for clearness of the tree view display. I then load a resource bitmap preset in the m_IDBMP member. If loading ok, I calculate the number of states for m_nStates by the bitmap sizes, assuming each image being square. Once completing the state image list, I set m_bImgList to TRUE for the subsequent OnDestroy().

Since destroying a tree view control does not destroy its state image list, I override OnDestroy() to do my own. I found that if loading bitmap fails and image list isn’t set, GetImageList() with the parameter TVSIL_STATE will not return NULL. So the flag m_bImgList is necessary.

A pair of functions SetCheckState()/GetCheckState() are simple extensions from SetCheck()/GetCheck() in CTreeCtrl (see winctrl2.cpp in VC++). They all rely on sending TVM_SETITEM/TVM_GETITEM messages with the TVITEM structure. The only difference is that the state accessed in my function is multiple valued instead of BOOL type. Check the SDK documentation for more detail about the tree view messages and structures.

The last trick is how to trap the state change and send the clicked item handle and its state to the parent window. There is no such notification at hand. In trace step by step, I found a parent window couldn’t catch the clicked item handle in processing a message like NM_CLICK or WM_LBUTTONDOWN. Using message reflection in CMutiChkTreeCtrl doesn’t help either. So I decided to define a custom message _TVN_CHECKCHANGED and send it after the Windows click message completes. That is what I did in CMutiChkTreeCtrl::OnLButtonDown(). I first get the item handle by hit test, and if the click occurs within the checkbox, I call GetCheckState() to get this item’s state. Notice at this moment, the framework has not updated this checkbox to the next state, so that I need to calculate the state after the click. Next, I call CTreeCtrl::OnLButtonDown() to make it done and then send the _TVN_CHECKCHANGED message to the parent window with the state in wParam and the item handle in lParam.

Using Multi-State Checkbox

Similar to using CTreeCtrl, you only need to change your tree view type to CMutiChkTreeCtrl. Since PreSubclassWindow() is called implicitly by the framework’s initialization procedure, you should make m_IDBMP valid before PreSubclassWindow() gets called. Thus my five-state example is initialized like this:

BOOL COtherDlg::OnInitDialog()
{
    m_ctrlTr.SetBitmapID(IDB_5STATES);   // Set m_IDBMP 
    
    CDialog::OnInitDialog(); // Call PreSubclassWindow() 

    HTREEITEM hRoot, hGroup, hItem;
    hRoot = m_ctrlTr.InsertItem("My System", TVI_ROOT);
    m_ctrlTr.SetCheckState(hRoot, 0);    // No state 

    hGroup = m_ctrlTr.InsertItem("Computers", hRoot);
    hItem = m_ctrlTr.InsertItem("Windows", hGroup);
    m_ctrlTr.SetCheckState(hItem, 2);    // Set state 2

    // Populate other items...
}

where m_ctrlTr is of the type CMutiChkTreeCtrl, and I let the item “Computers” be the default state. When a click occurs, I intercept it in COtherDlg::WindowProc() to expose the current action as shown in Figure 3 like this:

if (message==_TVN_CHECKCHANGED && (hItem=HTREEITEM(lParam)))
{
    str.Format("   (State %d)", (int)wParam); // Get state
    str = m_ctrlTr.GetItemText(hItem) +str;   // Get name
    GetDlgItem(IDC_EDIT)->SetWindowText(str);
}

The Three-State Checkbox

Now, I will implement a special case of the multi-state checkbox tree view, the tree view with three-state checkbox, which contains unchecked (state 1), checked (state 2), and partial-checked (state 3). When you click an item’s checkbox, the states of its parent and child items are dynamically adjusted.

Figure 4 shows such a demonstration. Except for the standard unchecked and checked boxes, I simply invert the checked image to represent the third partial-checked state. Here I suppose that the “Programs” are organized in groups (Library, Utility, and Internet) and sub-groups; e.g., System and Application in Library. You can check or uncheck an individual item, or a group/sub-group for all items contained. Once you click a checkbox, you can see an automatic update of states in the tree view. The list box on the right side collects all checked items and the bottom edit box records the current change.

It’s easier to summarize the following definitions for an item’s checkbox being clicked:

D1 The state of THIS checkbox is toggled between unchecked and checked.

D2 All its children’s states are the same as the state of THIS checkbox (unchecked or checked). This rule should recursively apply to a child downward to all its own children.

D3 The parent’s state depends on the states of its children (THIS checkbox and all its siblings). A parent is unchecked or checked, if all its children are unchecked or checked. A parent is partial-checked, if one of its children is partial-checked, or if some children unchecked and some checked. This rule should recursively apply to a parent upward to its own parent.

Obviously, D1 conflicts with the previous model of the state machine transferring three states sequentially (R3). I have to modify the state machine to the diagram in Figure 5. The class for the three-state checkbox tree view is C3SChkboxTrCtrl, derived from CMutiChkTreeCtrl, and implemented in s3chtree.h and s3chtree.cpp (available online).

My state machine triggered by a click works in C3SChkboxTrCtrl::OnLButtonDown(). Again, the state I get first is a state before the click. If it’s checked, I set it to partial-checked. Recall that the framework will make this checkbox go next to unchecked. But the framework doesn’t care for other items so that I still need the state after the click to adjust the states of parents and children. I calculate the nState and pass it to SetChildrenState() and SetParentState(). At last, I call CMutiChkTreeCtrl::OnLButtonDown() to ask for sending _TVN_CHECKCHANGED for an immediate dialog update.

SetChildrenState() is a recursive implementation of D2 and SetParentState() is for D3. The helper GetSiblingState() implements the criteria in D3 to determine the parent state, where the function logic is a clear mapping to the definitions. UpdateParentState() can be called in adding an item to a tree view, where you want parents at all levels to maintain the state by D3:

hItem = m_ctrlTree.InsertItem(szItem, hSub);
nState = bEnable? CKTC_CHECKED: CKTC_UNCHECKED;
m_ctrlTree.SetCheckState(hItem, nState);
// Recursive update
m_ctrlTree.UpdateParentState(hItem, nState); 

Another point worth emphasizing in C3SChkboxTrCtrl is that I try to create the three-state bitmap on the fly, instead of relying on a loaded bitmap. This makes C3SChkboxTrCtrl more independent and transplant to an application without concerning the state bitmap resource. And it can achieve a standard look of the checkbox. For this purpose, I load the predefined bitmap OBM_CHECK, draw three state images manually, and add them to the image list one by one, just as SetCheckImageList() does.

Again, I override the virtual function PreSubclassWindow(), where I call CMutiChkTreeCtrl::PreSubclassWindow() to consolidate the checkbox style, set m_nStates to three, and call SetCheckImageList() finally.

An effective alternative to draw a check mark would be using DrawFrameControl(), but I failed with it to obtain different types of checkboxes in SetCheckImageList(). If someone knows how to make it, please kindly drop me a few lines.

At this time I might realize why in Figure 1 one machine shows checkboxes overlapped, while another doesn’t. The size of the predefined bitmap OBM_CHECK appears to be different, normally 13 by 13 in pixel, but 15 by 15 can be found on a machine with overlapped checkboxes. To avoid potential trouble, I validate the OBM_CHECK size and limit it to 13 by 13 in SetCheckImageList() defined by SIZE_CHECK.

Summary

This article not only gives an overview on check state of the tree view, but also reveals the underlying mechanism of the check state implementation. My study produced two results: The class CMutiChkTreeCtrl provides the fundamental functions for the multi-state checkbox in the tree control, while C3SChkboxTrCtrl presents you with the reusable three-state checkbox functionality. A sample program named TreeDemo is available in this month’s archive online to show you how to use these classes. Running TreeDemo directly brings you the three-state demonstration in Figure 4, and then pressing Alt-5 pops up the five-state sample in Figure 3 or pressing Alt-2 lets you check the problematic bi-state example in Figure 1.

Just before finishing this article, I read “An MFC Options Tree Class (WDJ, June 1998) written by Raj Mohan. Raj proposed a tree view implementation based on the model come from the Advanced page in IE’s Internet Options dialog. Although he displayed bi-state checkboxes, he used the item image list in the tree view for all images, without touching state image and its mechanism.

Actually, the tree view control has other image lists I haven’t discussed here, item image and overlay image. Mixing different image categories could generate flexibility in the UI, but it also generates complexity in implementations. Could we extend the checkbox to multiple states in a tree view with one of other image lists? That’s a topic worth a whole separate conversation.


Zuoliu Ding has been working in Windows development for years. He can be reached at [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.