Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

CRHTree - An Owner-drawn CTreeCtrl that has Open/Close and Checkboxes on the Right

4.76/5 (10 votes)
22 Aug 2007CPOL8 min read 1   4.2K  
An owner-drawn CTreeCtrl that has checkboxes and open/close controls aligned vertically on the right hand edge of the tree for easy viewing regardless of the horizontal scroll position.
Screenshot - CRHTree.jpg

Introduction

Recently I needed a tree-style presentation, but the standard CTreeCtrl just didn't cut it. I wanted the open/close controls and checkboxes to be vertically aligned down one side of the tree, not zigzagging in and out with each item's indentation. Why? Well if you're doing some kind of monitoring application, I feel it's better to have all the checkboxes in a straight line, so you can assess the current state in a single glance.

I headed for The Code Project and looked at both trees and lists, but I didn't find anything that fit the bill, so realized I'd have to write my own. Normally when this happens I start by trying to adapt one of the standard common controls, but finish up writing the whole control from scratch because I'm having to fight the default behavior too much. However, during my initial search I came across Jim Alsup's Vivid Tree. Though not appropriate for my needs, his article did show me that you can accomplish a lot by simply hijacking the OnPaint handler and doing your own thing. So I took a deep breath, created a subclass of CTreeCtrl, and got to work.

The result is CRHTree, which has the following features:

  • Open/Close buttons are customized, appear on the rightmost side of the tree and have a subtle hover effect.
  • Checkboxes - when present - are also drawn on the right hand side. They follow the Vista/XP themed look when available, reverting to the "classic" look otherwise.
  • Lines connecting children to parent are drawn in the usual way.
  • Horizontal scrolling has been tweaked to affect only the item title, icon and lines - the checkboxes and open/close widgets always remain on screen, vertically aligned on the right hand side.

Prerequisites

Before we get started, please note that the control uses GDI+ to take care of alpha-blending and other drawing effects. All systems from XP upwards have this by default, but Windows 2000 doesn't. Happily, you can download the latest version of the GDI+ DLL from Microsoft here. If that link doesn't work (thanks to Microsoft reorganizing its Websites yet again!) just Google for "gdiplus redistributable" and you should find it. The beauty of the GDI+ DLL is that installation is trivial. Just drop it in the same folder as your executable and you're done. Note that I haven't included the DLL in the downloads however, to keep their size down.

Also, to take some of the pain out of the Vista/XP theme stuff, I've used the tremendously handy CTheme class from Rail Jon Rogut's article, CHoverBitmapButton. This is included in the downloads.

Finally, please note that the CRHTree class and its sample project were written in Visual Studio 2003 (VC 7.1?). I've used Stephane Rodriguez's VC7 -> VC6 converter to provide VC6 project files, but I can't guarantee that the project will in fact build under VC6.

Using the Code

Using CRHTree isn't that different from using the standard CTreeCtrl. You just have a few extra files to include in your project, and optionally a bit of graphic editing if you want to give your open/close widget a different look from the one provided. Here's a quick run down of the steps involved.

Step 1 - Add Files and Resources to Your Project

You need to add RHTree.h/.cpp, ResourceUtils.h/.cpp, Theme.h/.cpp and ThemeLib.h to your project.

You also need to import the four expand/collapse PNG files (two standard, two "hot") as custom PNG resources with the IDs IDR_COLLAPSE, IDR_COLLAPSEHOT, IDR_EXPAND and IDR_EXPANDHOT. To find out why I chose to use PNG files for this, take a look at the Points of Interest section below.

Step 2 - Hook Up to GDI+

Include gdiplus.h in your stdafx.h, and add gdiplus.lib in the Linker Input section of your project.

Also, be sure to initialize and de-initialize the library in your app's InitInstance and ExitInstance methods:

C++
// Initialize GDI+ Library
Gdiplus::GdiplusStartup(&m_gdiToken, &m_gdiStartup, NULL);

To de-initialize:

C++
// Shutdown GDI+ Library
if ( 0 != m_gdiToken )
    Gdiplus::GdiplusShutdown( m_gdiToken );

Step 3 - Use CRHTree Instead of CTreeCtrl

In your project, change use of CTreeCtrl to CRHTree as required. For example, if you've added a tree control to a dialog resource, use Class Wizard to hook the tree to a CTreeCtrl variable, then edit the dialog's header file and replace CTreeCtrl with CRHTree (adding a #include "RHTree.h" in the process).

You can turn lines and checkboxes on/off just by changing the styles on the tree itself, just as you would with a normal CTreeCtrl. Note however that CRHTree ignores the TVS_HASBUTTONS style - it always draws the open/close widget.

Likewise, item icons are handled in much the same way as CTreeCtrl - create and populate an image list, assign the image list to the tree, and specify an image index with each item you insert into the tree.

Step 4 - Optional Customization

Most of CRHTree's drawing has been split up into small, virtual functions so it should be easy to change the appearance of the tree. The Open/close button (widget) currently gets its appearance from four PNG files (with an alpha channel for transparency) but you could easily switch to using an icon or even draw the whole thing in code.

Key functions to consider changing or overriding include:

IsGroupCurrently, this just returns TRUE if the item in question is a parent. In my own projects however, I've overridden this so that even items that don't necessarily have children are drawn with an open/close button.
GetPartColorItem and group (parent) title and background colors are chosen in here
DrawBackgroundDraws the main background for the control
DrawWidgetDraws the open/close button
DrawGroupWashDraws the background for group (parent) items
DrawGroupTitleDraws the title for group (parent) items
DrawItemTitleDraws the title for standard (non-parent) item

Points of Interest

This section looks at some of the problems I encountered while building the class, and also points out some of the coding tricks and techniques I've used that may come in handy for other projects, whether you want to use CRHTree or not.

Adjusting Item Rectangles

The first problem I hit when getting the tree to draw correctly was that the text rectangles returned by CTreeCtrl::GetItemRect were typically wrong horizontally. Not surprising, given that the open/close buttons and checkboxes have switched sides!

I wrote a single function to fix this, called OffsetTextRect. Basically, it measures the horizontal difference between the text rectangle and the whole item rectangle for the first root in the tree. It uses this value as a basic leftward offset for each item in the tree, but also factors in the current horizontal scroll position.

C++
void CRHTree::OffsetTextRect( CRect& rText )
// Depending on the styles that are set, the text rectangle might need
// to have its left edge adjusted, since the underlying control will make
// room for items that are normally on the left (+/- button, checkbox)
{
    CRect    rRoot;
    CRect    rRootText;
    //

    if ( this->GetItemRect(this->GetRootItem(), rRoot, FALSE) &&
     this->GetItemRect(this->GetRootItem(), rRootText, TRUE) )
    {
          rText.left = m_nHSpacer + 
        max(0, rText.left - (rRootText.left - rRoot.left))
        - this->GetScrollPos(SB_HORZ);
    }

    if ( this->GetImageList(TVSIL_NORMAL) )
        rText.left += ::GetSystemMetrics(SM_CXSMICON) + m_nHSpacer;
}

Fixing Up Drawing After Horizontal Scrolling / Sizing

The next major problem became apparent when I added plenty of items to my sample application and made the window resizable. The standard tree control performs drawing optimizations based on the idea that the whole area of each tree item is affected during scrolling. In my tree however, I wanted the checkboxes and open/close buttons to hold position at the far right of the control, whilst the rest of the tree scrolled normally.

I tried various "smart" ways to fix this, but in the end brute force won the day.

C++
void CRHTree::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
// Since we've arranged our tree differently, the standard ctrl won't
// get things right...
{
    // Inhibit standard drawing...
    this->SetRedraw( FALSE );

    // Handle the scroll normally...
    CTreeCtrl::OnHScroll(nSBCode, nPos, pScrollBar);

    // Re-enable drawing..
    this->SetRedraw( TRUE );

    // Redraw the whole thing thoroughly in one pass...
    CRect rClient;
    this->GetClientRect( rClient );
    this->RedrawWindow( rClient, NULL,
    RDW_NOCHILDREN | RDW_UPDATENOW | RDW_INVALIDATE);
}

The handler for OnSize is very similar.

Using GDI+

This falls into the "handy for other projects" category.

As long as you've got the necessary headers and libraries (probably an issue for VC6 users), GDI+ is a superb way to get sophisticated effects for minimal effort. It's used in CRHTree to provide a nice alpha-blended selection effect, and to handle drawing of PNGs that have an alpha (transparency) channel.

Here's a snippet from CRHTree::DrawItem, showing the drawing of the selection effect:

C++
if ( bSelected )
{
    //
    // GDI+ lets us easily paint a transparent colored layer over the item
    //

    // Let's have our GDI+ graphics wrapper...
     Gdiplus::Graphics gfx( pDC->GetSafeHdc() );

    // Define the hilite color, but reduce the alpha for a blended look
    COLORREF crHi = ::GetSysColor(COLOR_HIGHLIGHT);

    // Note the alpha - 100 (255 is opaque)
    Gdiplus::Color clrSel( 100,
               GetRValue(crHi),
               GetGValue(crHi),
               GetBValue(crHi) )

    // Define a simple brush with this color...
    Gdiplus::Rect rGPItem( rDraw.left, rDraw.top,
               rDraw.Width(), rDraw.Height());
    Gdiplus::SolidBrush brushFill( clrSel );

    // Apply it
    gfx.FillRectangle( &brushFill, rGPItem );
}

What's All This PNG Icon Stuff?

Well, even as I write this, I just know someone's going to post a comment saying "Why did you do all that? All you needed to do was..."

For what it's worth, here's the story behind the PNG stuff.

I wanted my open/close widgets to have nice smooth edges. That means anti-aliasing, which pretty much requires an alpha channel. Now, XP supports icons with an alpha channel, but earlier OSes don't. Or at least I don't think they do. So a simple icon wouldn't cut it.

Now, I'd already committed to using GDI+, and GDI+ can load and draw a variety of formats, including PNG with an alpha channel. So instead of using an icon, I could use a PNG. Great. Except that PNG drawing might be a bit slow in a big list with a lot of parent items.

The solution I arrived at was this:

  • Create an offscreen DC and paint it to match the current background color used for parent (group) items.
  • Get GDI+ to load up and draw the anti-aliased image onto the offscreen canvas.
  • Stick the resulting bitmap in an image list and from that create an icon.
  • Use that icon for nice fast drawing, but rebuild it if color choices change.

This code is in CRHTree::PrepareWidgetIcons.

Unpacking a Resource to a File

When I was first working on drawing the open/close buttons (see above), I just had the *.png files sitting alongside the executable. That would have been OK, but I felt it would be neater to have them housed directly in the executable itself. It was trivial to import them into resources under the custom type "PNG" but GDI+ didn't seem to want to have anything to do with them in this form. So I wrote a set of static routines in CR<code>esourceUtils to unpack the resources to a temporary file. I reckon these are pretty handy in their own right.

Credits

Once again, thanks to Rail Jon Rogut for his handy CTheme class, and to Jim Alsup, whose 'Vivid Tree' article gave me the confidence to stick with CTreeCtrl as the base class.

History

  • 6th August, 2007: First version
  • 22nd August, 2007: Second version
    Main changes:
    • Added OnMouseWheel and EnsureVisible overrides to ensure drawing in further cases where horizontal scrolling occurs
    • Added tooltip function to zap the text for checkboxes and open/close widgets

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)