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:
Gdiplus::GdiplusStartup(&m_gdiToken, &m_gdiStartup, NULL);
To de-initialize:
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:
IsGroup | Currently, 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. |
GetPartColor | Item and group (parent) title and background colors are chosen in here |
DrawBackground | Draws the main background for the control |
DrawWidget | Draws the open/close button |
DrawGroupWash | Draws the background for group (parent) items |
DrawGroupTitle | Draws the title for group (parent) items |
DrawItemTitle | Draws 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.
void CRHTree::OffsetTextRect( CRect& rText )
{
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.
void CRHTree::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
this->SetRedraw( FALSE );
CTreeCtrl::OnHScroll(nSBCode, nPos, pScrollBar);
this->SetRedraw( TRUE );
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:
if ( bSelected )
{
Gdiplus::Graphics gfx( pDC->GetSafeHdc() );
COLORREF crHi = ::GetSysColor(COLOR_HIGHLIGHT);
Gdiplus::Color clrSel( 100,
GetRValue(crHi),
GetGValue(crHi),
GetBValue(crHi) )
Gdiplus::Rect rGPItem( rDraw.left, rDraw.top,
rDraw.Width(), rDraw.Height());
Gdiplus::SolidBrush brushFill( clrSel );
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