Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Yet Another Custom Tree Control

0.00/5 (No votes)
2 Dec 2002 3  
Tree control allowing 'per item style' and requiring no bitmap arrays

Sample Image - CustomTreeImage.png

Introduction

I needed a 'check-tree' type of control for a project I am working on. However, in this application, having a check box on every tree node did not seem like the right approach. So, after a few false starts, I ended up implementing a fully owner drawn tree. Now, I have not seen too many of those around. Plus, once I got started, I started to go a bit over the top in what I created. In the end, the approach I elected to use offers 'mixed elements' to exist in the tree simultaneously. The class seemed novel enough to warrant a quick submission to Code Project. So, here goes.

Getting Started

I've included a simple dialog based application for illustrating how to use this control class. There is nothing too complex about that, so I won't go into it at all except to explain how it sets up the tree control itself. First off, referencing CCustomTreeDlg::OnInitDialog() (in the file: CustomTreeDlg.cpp), you can see that the tree is initialized via calls to:

CreateTreeControl();
FillTreeControl();

Now, drilling into these two methods, you can see that all that CreateTreeControl() does is to create the control itself. I chose to use this approach to allow me to have more control to experiment with various window styles for the tree control without having to resort to the resource editor. The only non-obvious thing going on in this method is that we measure the rectangle of a hidden static control on the dialog and use its rectangle as the placement location for the tree control.

Next, in the CCustomTreeDlg::FillTreeControl() method, you can see that there are several calls to HTREEITEM CCustomTreeCtrl::AddNode( HTREEITEM hParent, HTREEITEM hInsertAfter, const char* pchLabelText, int iStyle );. The only parameter to this method that is not completely self-explanatory is the iStyle argument. The value of this parameter defines the type of node that will be placed into the tree, so it is somewhat important that the meaning of this parameter be well understood. Referencing CustomTreeControl.h, you can see that there are the following constants defined:

// item styles
//
const int ciEditable    = 1;  // means the element will be editable
const int ciCheckedNode = 2;  // means the element 
    //will be rendered as a check box
const int ciRadioNode   = 4;  // means the element will 
    //be rendered as a radio button
const int ciSelectable  = 8;  // means the element can be 
    //rendered as a selected node

These styles can be logically OR-ed together to get the node type that is needed. Note that in this version, no sanity checks are performed and it would be possible to define a node as being both a radio button and a check box. This is a hole that I'll leave to those interested in the problem to fix up.

So, referring back to CCustomTreeDlg::FillTreeControl(), you can see that several different node types are added to the tree. The results of this are what you can see in the screen shot above.

Robbing Data Space

I elected to store each element's state and style data in the ItemData value for each element. This approach was very expeditious for this approach, but if you need to store application data for each tree node, then you'll need to revisit this approach and either come up with another data map, or revise how I use the ItemData for style and state of each node. I mix the style and state of each tree element into a single DWORD with a call to MAKEWPARAM( state, style ). Now, to work with an element's style and/or state, I need to unpack these values. So, you will see code like this:

DWORD dwData    = GetItemData( hTreeItem );
UINT  iStyle    = HIWORD( dwData );
UINT  iState    = LOWORD( dwData );

throughout several methods of the CCustomTreeCtrl class. Additionally, if I need to change state values, then code like the following will be invoked:

DWORD dwData    = GetItemData( hTreeItem );
UINT  iStyle    = HIWORD( dwData );
UINT  iState    = LOWORD( dwData );

// mess around with the state data

// store it
SetItemData( hTreeItem, MAKEWPARAM( iState, iStyle ) );

Now, all specific changes to an element's style are simple variations on this theme.

Emulating Common Controls.

This tree can have 'check box' and 'radio button' nodes in the tree. Check boxes are easy. They simply check and uncheck. However, since I elected to put 'radio button nodes' in this tree, I needed some way to handle them, that 'made sense'. So, I selected the following approach:

  1. All peer-nodes that are radio buttons will be mutually exclusive. In other words, only one of a set of radio button nodes that share a common parent can be checked at once. If a radio button node becomes checked, then all of its peer radio buttons must be unchecked.
  2. If a radio button node becomes unchecked, then all of its child nodes will become inactive.

The code that does this work starts at the method: CCustomTreeCtrl::ChangeItemState( HTREEITEM hItem ). This method first checks/unchecks a node. Then, if the node has become checked, then all child nodes well be enabled.

This work happens through the recursive method: CCustomTreeCtrl::EnableSubTree( HTREEITEM hTreeItem, bool bEnable ). After this work is completed, all peers of the newly-checked node will be visited. If a peer node is also a radio button, then it will be unchecked, and all of its children will be disabled via a call to the CCustomTreeCtrl::EnableSubTree(HTREEITEM hTreeItem, bool bEnable) method as well.

Look Ma! No Bitmap Arrays

For various reasons, I elected to use Operating System glyphs to render the tree elements. This saved me the trouble of having to create a whole set of bitmaps for various element states and styles. It also allows this control to be dropped into any project without the user having to remember to add a bitmap to their project. It also made short order out of rendering elements, because instead of hand-placing bitmaps and/or messing with a whole slew of CDC methods, I could place glyphs with a call to DrawFrameControl. Easy stuff. To see how this works, refer to the following snips from the OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) method:

// if this node has kids, then must put up an indicator for that
//
if ( ItemHasChildren( hTreeItem ) )
{
  if ( !bExpanded )
  {
    poDC->DrawFrameControl( oButton,
                            DFC_SCROLL,
                            DFCS_SCROLLRIGHT );
  }
  else
  {
    poDC->DrawFrameControl( oButton,
                            DFC_SCROLL,
                            DFCS_SCROLLDOWN | DFCS_PUSHED );
  }
}

     .......

if ( iStyle & ciCheckedNode || iStyle & ciRadioNode )
{
  CRect oCtrlRect( oRect.left,
                   oRect.top,
                   oRect.left + oRect.Height(),
                   oRect.bottom );
  bool  bChecked  = ( 0 != ( iState & ciChecked ) );

  DWORD dwButton   = ( iStyle & ciCheckedNode )
                     ? DFCS_BUTTONCHECK : DFCS_BUTTONRADIO;
  DWORD dwChecked  = ( bChecked )               ? DFCS_CHECKED     : 0;
  DWORD dwInactive = ( iState & ciDisabled )    ? DFCS_INACTIVE    : 0;

  poDC->DrawFrameControl( oCtrlRect,
                          DFC_BUTTON,
                          dwButton | dwChecked | dwInactive );
  pxRects->m_oCheckRect = oCtrlRect;
  oRect.left += oCtrlRect.Width() + 4;
}

Of these approaches, the one that is most likely to be controversial is my choice to use scroll buttons to indicate a node's expanded state. I like the way it looks, but it is purely subjective and I'm certain that other folks' opinions may vary. But, it works for me.

Owner Drawn Trees

Now, rendering the node elements is pretty straight forward. Simply work from left to right drawing each 'sub-part' of the element as required. In fact, this is very simple. But, the real trick to custom drawing trees is to return CDRF_SKIPDEFAULT from the CDDS_ITEMPREPAINT handler of OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult). Most examples I've seen return CDRF_DODEFAULT. However, to get full on owner drawing, you need to disable all paints in the base class, and to do this, you must return CDRF_SKIPDEFAULT. So, if you need to do any kind of over the top painting in a tree, this is the one gem you need to know.

ToDo...

No code is ever truly 'done' until it gets thrown away. That being said, there are some outstanding items that may need to be addressed if you decide to use this control.

  1. Sometimes, the location of the in-place edit control seems to be badly placed with relation to the item being edited.
  2. "Tree Lines" are not rendered in this version. I did not need it, so I did not attempt to solve the problem. However, some others may not be so lucky.
  3. If you need to store application data with each tree element, then you'll need to revise how I store item state and style, and/or come up with another approach to mapping the application data with the tree elements.
  4. Sanity checking of the element styles would be an easy and good fix.
  5. Fix bugs I'm not yet aware of.

Moving On

This was an interesting project, and getting owner-drawn trees to work is no mean feat. So, I'm hopeful that some of the tips and techniques I've presented here are possibly of use to some of you out there in the Code Project space.

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.

A list of licenses authors might use can be found here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here