Advanced Tree Control

Annotation

This article presents an advanced control based on a standard tree view control. This control supports multiple-selection mode and some visual enhancement compared to the original control. Also, it explains some implementation details and may be useful to anyone who needs to fine-tune this control to his or her needs or even write something completely different.

Introduction

The initial idea of this class was to implement a good-looking multiple-selection tree control for the Yet Another Instant Messenger Project.

Writing a tree view from scratch is not an easy thing to do, so the subclassing seemed to be a nice solution. After a brief search on CodeGuru, I found two classes that seemed to be suitable… if combined together (see the References section at the end of the article).

It was not that interesting just to merge two classes and they didn’t cover all my needs, so the final problem statement included the following aspects:

  • The class must be ATL/WTL-based. The two original ones were MFC-based.
  • Multiple-selection support. Of course, that’s what all this mess about; keyboard navigation and selection should be available too.
  • Similar to a list control multiple selection interface (GetSelectedCount(), GetFirstSelectedItem(), and GetNextSelectedItem() methods) along with some useful utility methods (SelectChildren(), SelectAll(), and SelectRange() ).
  • Supports multiple item icons (actually, supports icons, bitmaps, image lists and animations, as a series of bitmaps) that may lead and follow item text.
  • All kinds of item icons should support transparency (through specifying a transparent color for bitmaps and animations).
  • Supports changing control font (for all items simultaneously).
  • Supports variable animation speed (for the whole control).
  • Supports changing element gap (horizontal distance, in pixels, between item icon and text/icon).
  • Supports setting all needed colors to paint the control (selection color, selected item color, and control background color).
  • Supports these per-item properties—color, text attributes (bold, italic), and indent—horizontal distance between parent’s baseline and current item’s baseline.
  • Supports setting custom bitmaps for expanded and collapsed item states (all other item bitmaps are drawn to the right of this state bitmap).
  • As smooth a repainting process as possible.

The controls in the sample application look like this:

Implementation Details

To store item-specific data, there is the TreeItemData structure declared as:

class TreeItemData
{
public:
   TreeItemData();

   ~TreeItemData();

   COLORREF itemColor;

   bool bBold;
   bool bItalic;
   UINT nIndent;

   DWORD dwNextImageId;
   CAtlList< TreeItemImage* > aPreImages;
   CAtlList< TreeItemImage* > aPostImages;
};

Most of the fields are self-explanatory. The only trick there is item image storing. To speed up painting slightly, I’ve divided the image array into two lists: aPreImages and aPostImages for left and right images, respectively (“left” and “right” here refer to the position relative to the item text). To control a specific image, each image structure contains a special ImageId field that is created as an incremented value of dwNextImageId.

Because actually there are four different image types that should behave similarly, all image structures are inherited from the TreeItemImage interface that is declared this way:

class TreeItemImage
{
public:
   TreeItemImage() : TransparentColor( DefaultTransparentColor ) {};
   virtual ~TreeItemImage() {};

   virtual bool IsAnimation() = 0;
   virtual void RenderItem( HDC dc, POINT upper_left,
                            bool bNextFrame = false ) = 0;
   virtual CSize GetSize() = 0;

   DWORD dwItemId;

   COLORREF GetTransparentColor();
   COLORREF SetTransparentColor( COLORREF color );

private:
   COLORREF TransparentColor;
};

The only unobvious thing here is the bNextFrame parameter of the RenderItem() method. There are two different events on which repainting may occur: First, when a particular area was covered by the other window or just appeared on screen (usual OnPaint() handing); the second case is when you need to display the next frame of animation (you want to get constant-speeded animation, yes?). In the first case, you must play the previous frame of animation; in the latter, the next one, remembering it. bNextFrame controls this item image behavior.

Child classes: TreeItemBitmap, TreeItemIcon, TreeItemAnimation, and TreeItemImgList implement specific item image types.

It should be understood that all image types may be mixed freely, even with one tree item.

The initial way to store item-specific data was a usage of the tree item’s ItemData value. It was quite easy to implement to store a pointer to TreeItemData structure there, but this approach created several problems:

  • To remove all items from the tree view without memory leaking, you must recursively delete TreeItemData for each structure.
  • Other applications may use ItemData value for their internal data. This leads to the third problem:
  • To determine whether there is a correct data pointed by ItemData field, first I used the IsBadReadPtr() WinAPI function. But, the performance was sluggish and also a series of “first change exceptions” in the debugger output window looked ugly.

So, in the final version, a slightly different approach was used: There is a map m_mapItemData that maps the HTREEITEM handle to the pointer to the TreeItemData structure. While it first seemed to be a less effective solution (you have to perform a lookup every time you need to get or set any item’s property), in the long run this wins.

There are two tricks with changing a control’s font. The first problem is that we need four fonts actually: normal, bold, italic, and bold italic. The SetFont() function creates all four fonts modifying the LOGFONT structure of the original plain font passed as a parameter. The second problem is an obvious thing; fonts may differ by character height. To calculate the current item’s height, this code is used:

// Calculating new item height
CDC dc;
dc.CreateCompatibleDC();
HFONT hOldFont = dc.SelectFont( m_pDefaultItemFont->m_hFont );

CRect rect( 0, 0, 0, 0 );

dc.DrawText( _T( "ABCDEFGabcdefg" ), -1, rect, DT_TOP | DT_LEFT |
              DT_NOCLIP | DT_CALCRECT );
SetItemHeight( rect.Height() + 2 );

dc.SelectFont( hOldFont );

As can be seen, you calculate only one font height assuming that all needed variations do not change theirtext height (this is true for all fonts I’ve seen).

The trick of flickerless redrawing of the control is an undocumented feature of the WM_PAINT message for a tree view control. It seems (for versions of Windows I’ve tested the control on—98, ME, 2K, and XP) that it is possible to pass custom device context in the WPARAM parameter when calling the original handler for WM_PAINT. This is the same way as WM_PRINT should be called, but I failed to make WM_PRINT work.

Note: The custom implementation of the TransparentBlt() function uses a stock implementation (from msimg32.dll) on all OSes except Win9x because there it leaks resources insanely. Instead, a potentially slower TransparentBltSlow() is used.

Placing the control in a resizable window isn’t a wise idea either, because when a tree window’s size is changing, the standard tree control handles it by recalculating scroll bar ranges. But, items in your tree may have different horizontal dimensions so there should be another handling that sets the correct ranges. It is implemented in the UpdateHorzScroller() function that is called for each control drawing operation—in the WM_PAINT message handler. However, it is impossible to disable the default handling without impairing other functionality, so some flicker of the horizontal scroll bar occurs during window resizing.

Another problem is with tool-tip text for long items. Their appearance is based on the “original” item positions—for a non-skinned tree control. Tool-tips should also be handled manually.

Why the class name is CCherryTree? It is up to you guys to guess it.

What’s Next?

This control still is missing some features that may be useful, but I’m too lazy to implement them right now:

  • Custom cursors. Currently, the control uses a hand cursor for expandable items an and arrow cursor for all other areas. This behavior depends on the TVS_TRACKGROUPSELECT style. If this style is not set, the control uses an arrow cursor for the whole area.
  • Transparency. The control behaves perfectly when it has any custom background color, but as all complex Windows controls, I doubt it will be an easy task to make it transparent. All those painting shortcuts used internally (as partial repaint in WM_HSCROLL and WM_VSCROLL that completely bypass WM_PAINT) make it a painful thing to do.
  • Property inheritance. It may be useful in some applications to allow inheritance of all per-item properties—color, font properties, and indent.
  • Custom drawing support. “More custom? How’s that?” you may ask. Callback texts, user-supplied images, some pre- or post-painting special effects may be useful. This requires some callback hooks (because the original tree view’s ones are already used) to be supplied.
  • Per-item font support. The biggest problem with this feature is that this tree view control supports only fixed-height items. I didn’t experiment combining the TVS_NONEVENHEIGHT style with this implementation.
  • Multi-line items or even RTF-based items. The same problem as with per-item font support exists. Although this way the control will even emulate Outlook’s mail list control (group by date, sender, and so forth)

References

Richard Hazlewood July 25, 1999

http://www.codeguru.com/Cpp/controls/treeview/misc-advanced/article.php/c629/

Zafir Anjum August 6, 1998

http://www.codeguru.com/Cpp/controls/treeview/misc-advanced/article.php/c633/

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read