Introduction
In a previous tutorial (CWinListBox), I discussed the step-by-step creation of a listbox from scratch, as a custom control derived from CWin
. I would now like to move on to the development of a simple tree control using a slightly different approach, namely, using a CStatic
control as the base class.
As I explained earlier, my intended audience is the rookie programmer, and my only goal is to show how the basic functionality of apparently complex GUI controls can be recreated with relatively straightforward code. Remember however, that, in general, it is not a good idea to develop custom controls from scratch unless the functionality one wants to implement is clearly outside of the standard, and that, in no way, the code introduced here is meant to replace the available MFC CTreeCtrl
.
What Is to Be Accomplished
The target is a tree control implementing the most elementary functionality: insertion and removal of nodes, expand/collapse on single click, vertical scroll (scrollbar and mouse-wheel), some eye-candy (color and font, bitmap background), and owner-drawn audio context-menu. The standard CTreeCtrl
control only allows single-line text per node (no wrap), each limited to some 260 chars, so I have decided to resolve this colossal catastrophe by means of an "extra" feature, namely, auto-resizing multiline (wrapping) text.
The keen reader may like to know that I have written this tutorial as I wrote the demo project. The instructions, explanations, and the code below do amount to the development of the custom tree control in the image above.
On with the code.
Step-by-Step Procedure
Project Kick-off
The setup is simple. Create a new dialog-based project, and set the warning level to 4 (Project Settings, C/C++ tab). Level 4 will ensure that anything suspicious is brought up to our attention so that it is up to us to decide what to do with 'informational warnings which in most cases can be safely ignored' (from the docs).
[Aside: This is also a good time to create UNICODE project configurations if these are needed (I have included them in the demo project for the enthusiastic internationalist). Very briefly, you would need to add the defines _UNICODE
and UNICODE
to the preprocessor defines (Project Settings, C/C++ tab) while making sure to remove the _MBCS
define, and then add wWinMainCRTStartup
as the entry-point symbol (Project Settings, Link tab, Category 'Output'). If you do go UNICODE, save yourself any headaches by enabling right now the display of UNICODE strings while debugging (Tools/Options, Debug tab) and by reading Chris Maunder's article, Unicode, MBCS and Generic text mappings.
Let's start working on the tree control. Create a new MFC class named CStaticTreeCtrl
that uses CStatic
as the base class.
In the resource editor, add a static text control with ID IDC_ST_TREE
, and then, using the MFC ClassWizard, add a member variable to IDC_ST_TREE
named m_ST_Tree
, making sure to select Control
as the Category and CStaticTreeCtrl
as the Variable Type.
On clicking on OK, a message box warns us to make sure we have included the header file for the class CStaticTreeCtrl
in our dialog code. Do it now if you haven't already.
Since the control will be made from scratch, we may as well start by choosing our own font and default text color. The declarations look as follows, and their implementation is trivial (check the sources):
protected:
CFont m_Font;
COLORREF m_crDefaultTextColor;
public:
virtual CStaticTreeCtrl& SetTextFont ( LONG nHeight, BOOL bBold,
BOOL bItalic,
const CString& csFaceName );
virtual CStaticTreeCtrl& SetDefaultTextColor ( COLORREF crText );
The Data Structure
The backbone of any kind of tree control is a data structure where to keep the information that will be displayed. There are a number of ways to do this, and here I will use the simplest I can think of.
class CTreeNode
{
public:
CTreeNode()
{
pParent = NULL;
pSibling = NULL;
pChild = NULL;
}
CTreeNode* pParent;
CTreeNode* pSibling;
CTreeNode* pChild;
};
Any node in our tree control will know its parent, its first child, and the next sibling in line. These three pointers will allow us to travel from any node to any other node in a rather simple manner, as you can see below. Nodes B, C, and D are all siblings (having the same parent, namely, node A) and have no children (pointers to null). Node D has no next sibling (pointer to null). Node A only needs a pointer to the first child node (node B in this case) to be able to access all its children.
So, what information do we want each node in the tree to have? Its own font? Foreground and background colors? Selection toggle or check mark? Icons/bitmaps? What? I will code the foundation, and you can add to it as you see fit.
class CTreeNode
{
public:
CTreeNode()
{
csLabel.Empty();
rNode.SetRectEmpty();
bUseDefaultTextColor = TRUE;
bOpen = TRUE;
pParent = NULL;
pSibling = NULL;
pChild = NULL;
}
virtual ~CTreeNode()
{
csLabel.Empty();
}
CString csLabel;
CRect rNode;
COLORREF crText;
BOOL bUseDefaultTextColor;
BOOL bOpen;
CTreeNode* pParent;
CTreeNode* pSibling;
CTreeNode* pChild;
};
#define HTREENODE CTreeNode*
#define HTOPNODE ( (HTREENODE) -0x10000 )
Each node will have a text label and foreground color. Beyond that, it will be useful to remember whether a node is opened/closed (for drawing, searching, etc.) and the portion of the control area it occupies (for mouse clicks, drawing connecting lines, etc.).
For the structure to be at all useful, it is necessary to be able to insert and delete nodes. We will also need to keep track of the top node of our tree structure. The declarations look as follows:
protected:
HTREENODE m_pTopNode;
public:
HTREENODE InsertSibling ( HTREENODE pInsertAfter, const CString& csLabel,
COLORREF crText = 0, BOOL bUseDefaultTextColor = TRUE,
BOOL bInvalidate = FALSE );
HTREENODE InsertChild ( HTREENODE pParent, const CString& csLabel,
COLORREF crText = 0, BOOL bUseDefaultTextColor = TRUE,
BOOL bInvalidate = FALSE );
void DeleteNode ( HTREENODE pNode, BOOL bInvalidate = FALSE );
protected:
void DeleteNodeRecursive ( HTREENODE pNode );
Note that the top node is initialized in the CStaticTreeCtrl
constructor and will be our handle to the tree during its lifetime.
Regarding the addition of new nodes, these can be inserted as children or as siblings of existing nodes. Properly conceptualized, it will allow us to insert a node anywhere in the tree.
CStaticTreeCtrl::CStaticTreeCtrl()
{
m_pTopNode = new CTreeNode();
}
HTREENODE CStaticTreeCtrl::InsertSibling( HTREENODE pInsertAfter,
const CString& csLabel,
COLORREF crText ,
BOOL bUseDefaultTextColor ,
BOOL bInvalidate )
{
ASSERT( pInsertAfter != NULL );
HTREENODE pNewNode = new CTreeNode();
pNewNode->csLabel = csLabel;
if( bUseDefaultTextColor )
pNewNode->bUseDefaultTextColor = TRUE;
else
pNewNode->crText = crText;
pNewNode->pParent = pInsertAfter->pParent;
pNewNode->pSibling = pInsertAfter->pSibling;
pInsertAfter->pSibling = pNewNode;
if( bInvalidate )
Invalidate();
return pNewNode;
}
HTREENODE CStaticTreeCtrl::InsertChild( HTREENODE pParent,
const CString& csLabel,
COLORREF crText ,
BOOL bUseDefaultTextColor ,
BOOL bInvalidate )
{
ASSERT( pParent != NULL );
if( pParent == HTOPNODE )
pParent = m_pTopNode;
HTREENODE pNewNode = new CTreeNode();
pNewNode->csLabel = csLabel;
if( bUseDefaultTextColor )
pNewNode->bUseDefaultTextColor = TRUE;
else
pNewNode->crText = crText;
pNewNode->pParent = pParent;
pNewNode->pSibling = pParent->pChild;
pParent->pChild = pNewNode;
if( bInvalidate )
Invalidate();
return pNewNode;
}
The first step when inserting a node is to create it and, that done, to proceed to adjust existing pointers so that the new node is made a functional part of the tree structure. For example, if inserting a child, the new node's sibling becomes the parent's child so that the parent's child can be made to be this new node.
Deleting a node involves some design decisions. For instance, should we only delete a node if it has no children, or should we simply delete all its children recursively? Each option has its merits but, hold on to your pants, I will implement the latter so that we can take a first look at recursion.
void CStaticTreeCtrl::DeleteNode( HTREENODE pNode,
BOOL bInvalidate )
{
ASSERT( pNode != NULL );
if( pNode == HTOPNODE )
DeleteNode( m_pTopNode, bInvalidate );
if( pNode->pChild != NULL )
DeleteNodeRecursive( pNode->pChild );
if( pNode != m_pTopNode )
{
HTREENODE pRunner = pNode->pParent;
if( pRunner->pChild == pNode )
pRunner->pChild = pNode->pSibling;
else
{
pRunner = pRunner->pChild;
while( pRunner->pSibling != pNode )
pRunner = pRunner->pSibling;
pRunner->pSibling = pNode->pSibling;
}
delete pNode;
pNode = NULL;
}
if( bInvalidate )
Invalidate();
}
void CStaticTreeCtrl::DeleteNodeRecursive( HTREENODE pNode )
{
if( pNode->pSibling != NULL )
DeleteNodeRecursive( pNode->pSibling );
if( pNode->pChild != NULL )
DeleteNodeRecursive( pNode->pChild );
delete pNode;
pNode = NULL;
}
Both methods are pretty straightforward. The protected recursive method DeleteNodeRecursive
calls itself over and over until it reaches the last sibling of the last child, and then deletes all visited nodes from that one backwards. In this manner, it is guaranteed that we never delete a node that has links to deeper nodes, that is, that has non-null pointers to a child or a sibling. Otherwise, if we delete a node that still has children or next siblings, these will become unreachable and, thus, impossible to delete (memory leaks galore).
The public method DeleteNode
checks to see whether the method has been invoked to delete the entire tree and, if so, preserves the top node pointer (our handle to the tree throughout its lifetime). Top node or not, the method then proceeds to check if the node to be deleted has children and, if so, calls the recursive method to get rid of these. This done, the method moves on to find where in the structure the node to be deleted lives, and then takes it out of the chain of siblings.
Note, again, that the top node cannot be deleted with this method as it is our handle to the tree, and we need to keep it alive until the program closes. Thus, the CStaticTreeCtrl
destructor looks as follows:
CStaticTreeCtrl::~CStaticTreeCtrl()
{
DeleteNode( m_pTopNode );
delete m_pTopNode;
m_pTopNode = NULL;
}
And that's it. The tree structure is in place, it has enough functionality to be useful, and, on program termination, it cleans up after itself, preventing memory leaks and other unsightly nasties.
Drawing the Tree: The Basics
What does it take to paint a tree? Well, thanks to recursion, navigating through the tree is pretty simple as we have seen. But, first, let's set the thing up by adding a message handler for WM_PAINT
via the ClassWizard. We will paint off-screen (double-buffering) to avoid flickering. Check out the skeleton implementation.
void CStaticTreeCtrl::OnPaint()
{
CPaintDC dc(this);
CDC* pDCMem = new CDC;
CBitmap* pOldBitmap = NULL;
CBitmap bmpCanvas;
CRect rFrame;
GetClientRect( rFrame );
pDCMem->CreateCompatibleDC( &dc );
bmpCanvas.CreateCompatibleBitmap( &dc, rFrame.Width(), rFrame.Height() );
pOldBitmap = pDCMem->SelectObject( &bmpCanvas );
pDCMem->FillSolidRect( rFrame, RGB(255,255,255) );
pDCMem->Draw3dRect( rFrame, RGB(0,0,0), RGB(0,0,0) );
dc.BitBlt( 0, 0, rFrame.Width(), rFrame.Height(), pDCMem, 0, 0, SRCCOPY );
pDCMem->SelectObject( pOldBitmap );
delete pDCMem;
}
At this point, you can compile and run the application. You will see a white rectangle with a black border.
We will now add the recursive drawing method to paint the tree nodes (you should also add a few nodes to the tree to see how it looks while testing it).
int CStaticTreeCtrl::DrawNodesRecursive( CDC* pDC, HTREENODE pNode,
int x, int y, CRect rFrame )
{
int iDocHeight = 0;
CRect rNode;
rNode.left = x;
rNode.top = y;
rNode.right = rFrame.right - m_iPadding;
rNode.bottom = y + m_iLineHeight;
pNode->rNode.CopyRect( rNode );
COLORREF cr =
( pNode->bUseDefaultTextColor )? m_crDefaultTextColor:pNode->crText;
COLORREF crOldText = pDC->SetTextColor( cr );
pDC->DrawText( pNode->csLabel, rNode, DT_LEFT | DT_SINGLELINE | DT_VCENTER );
pDC->SetTextColor( crOldText );
if( pNode->pChild == NULL && pNode->pSibling == NULL )
return pNode->rNode.Height();
if( pNode->bOpen && pNode->pChild != NULL )
iDocHeight = DrawNodesRecursive( pDC,
pNode->pChild,
x + m_iIndent,
y + pNode->rNode.Height(),
rFrame );
if( pNode->pSibling != NULL )
iDocHeight += DrawNodesRecursive( pDC,
pNode->pSibling,
x,
y + pNode->rNode.Height() + iDocHeight,
rFrame );
return iDocHeight + pNode->rNode.Height();
}
void CStaticTreeCtrl::OnPaint()
{
....
....
pDCMem->FillSolidRect( rFrame, RGB(255,255,255) );
UINT nMode = pDCMem->SetBkMode( TRANSPARENT );
CFont* pOldFont = pDCMem->SelectObject( &m_Font );
DrawNodesRecursive( pDCMem,
m_pTopNode->pChild,
rFrame.left + m_iIndent,
m_iPadding,
rFrame );
pDCMem->SelectObject( pOldFont );
pDCMem->SetBkMode( nMode );
pDCMem->Draw3dRect( rFrame, RGB(0,0,0), RGB(0,0,0) );
....
....
}
The recursive method to draw the nodes needs to be called from OnPaint
after filling in the background, but before drawing the border (to make sure we don't paint over it). The parameters to DrawNodesRecursive
are a handle to the device context where to draw, the node to draw (we don't draw the top node), the location of the node (x and y), and the dimensions of the control area.
Now, how does the DrawNodesRecursive
method work? First, it calculates the dimensions of the current node and draws its text. Second, it goes through the recursive code which, again, is quite simple. If there are no children or siblings to the current node, then return, otherwise if it is opened and has children, then draw these and, that done, draw any other siblings (if there are any). Picture it in your head, it makes sense. If a node is open, we need to draw its children before moving on to the next sibling.
(Note that m_iIndent
and m_iPadding
are defaults, and that m_iLineHeight
is calculated in SetTextFont
.)
How about calculating the coordinates of the node to be painted while making recursive call after recursive call? This is the key to the whole thing after all, isn't it? Well, when going to draw a child, the horizontal displacement of the child is incremented by m_iIndent
which is easy to do, namely, the current position plus a few pixels (the constant value m_iIndent
).
Now, a bit more difficult, note that the vertical displacement is the current total height of the tree (i.e., the position of the next node to draw). A groovy pachouli way to calculate the height of the tree is to increment it every time a node is drawn and then pass it back as the return value of DrawNodesRecursive
. As usual, recursion produces clean and powerful code that may prove somewhat difficult to understand at first, but don't despair, or pollute the waterways, and persist. You'll figure it out.
Compile and run. You should see something like the following:
Not bad, it actually looks like a tree (mind you, one never knows what to expect)... and yet, you may have noticed that some of the nodes have text that runs off the side, so let's wrap the text and turn the nodes multiline.
Drawing the Tree: Word Wrap and Connecting Lines
But, ohh la la mon ami, word warp! Just how on earth? Well, let's segment the node's text one word at a time, calculating how much of it we can fit in one line and so on. Something like this:
int CStaticTreeCtrl::HowMuchTextFits( CDC* pDC,
int iAvailableWidth, CString csText )
{
int iValidSoFar = csText.GetLength() - 1;
if( pDC->GetTextExtent( csText ).cx > iAvailableWidth )
{
int iNextBlank = 0;
int iPixelWidth = 0;
while( iPixelWidth < iAvailableWidth )
{
iValidSoFar = iNextBlank;
iNextBlank = csText.Find( ' ', iNextBlank + 1 );
if( iNextBlank == -1 )
iNextBlank = csText.GetLength();
iPixelWidth = pDC->GetTextExtent( csText.Left( iNextBlank ) ).cx;
}
}
return iValidSoFar;
}
int CStaticTreeCtrl::DrawNodesRecursive( CDC* pDC,
HTREENODE pNode, int x, int y, CRect rFrame )
{
....
....
CString cs = pNode->csLabel;
int iPos = 0;
while( cs.GetLength() > 0 )
{
rNode.bottom = rNode.top + m_iLineHeight;
iPos = HowMuchTextFits( pDC,
rFrame.right - m_iPadding - rNode.left, cs );
if( rNode.bottom > 0 && rNode.top < rFrame.bottom )
pDC->DrawText( cs.Left( iPos + 1 ),
rNode, DT_LEFT | DT_SINGLELINE | DT_VCENTER );
cs = cs.Mid( iPos + 1 );
pNode->rNode.UnionRect( pNode->rNode, rNode );
rNode.top = rNode.bottom;
}
....
....
}
Note that there is a built-in alternative approach to multiline word-wrap, namely, making two calls to DrawText
, the first with the formatting flag DT_CALCRECT
, and the second to actually draw the text. Using the flag DT_CALCRECT
, the method DrawText
'determines the width and height of the rectangle. If there are multiple lines of text, DrawText
will use the width of the rectangle pointed to by lpRect
and extend the base of the rectangle to bound the last line of text. If there is only one line of text, DrawText
will modify the right side of the rectangle so that it bounds the last character in the line. In either case, DrawText
returns the height of the formatted text, but does not draw the text' (from the docs). As usual, writing your own code gives you more flexibility and, for this project, I have opted for that route.
Only things missing now are the connecting lines, so let's add them. The idea is that a connecting line looks like a capital 'L'. Thus, we can calculate the position of the elbow joint, and then throw two lines from there, one vertical and one horizontal.
void CStaticTreeCtrl::DrawLinesRecursive( CDC* pDC, HTREENODE pNode )
{
if( pNode->bOpen && pNode->pChild != NULL )
DrawLinesRecursive( pDC, pNode->pChild );
int iJointX = pNode->rNode.left - m_iIndent - 6;
int iJointY = pNode->rNode.top + ( m_iLineHeight / 2 );
if( pNode->pParent != m_pTopNode )
{
int iDispY =
iJointY - pNode->pParent->rNode.top - ( m_iLineHeight / 2 );
pDC->FillSolidRect( iJointX, iJointY,
m_iIndent, 1, m_crConnectingLines );
pDC->FillSolidRect( iJointX, iJointY, 1,
-iDispY, m_crConnectingLines );
}
pDC->FillSolidRect( iJointX + m_iIndent - 2,
iJointY - 2, 5, 5, m_crConnectingLines );
if( pNode->pChild == NULL )
pDC->FillSolidRect( iJointX + m_iIndent - 1,
iJointY - 1, 3, 3, RGB(255,255,255) );
if( pNode->pSibling != NULL )
DrawLinesRecursive( pDC, pNode->pSibling );
}
And this is how the tree looks now, with connecting lines and multiline wrapped text:
A node has a solid square dot if it has children, and a hollow one if it doesn't. Uncomplicated and effective.
Drawing the Tree: The Scrollbar
How about a scrollbar? What sort of madness is required to get this kind of functionality working? Worry not, nearly all CWin
derived classes can have scrollbars, all one needs to do is to call ShowScrollBar( SB_VERT, TRUE )
and then handle the WM_VSCROLL
message. Here is the code:
void CStaticTreeCtrl::ResetScrollBar()
{
m_bScrollBarMessage = TRUE;
CRect rFrame;
GetClientRect( rFrame );
if( rFrame.Height() > m_iDocHeight + 8 )
{
ShowScrollBar( SB_VERT, FALSE );
SetScrollPos( SB_VERT, 0 );
}
else
{
SCROLLINFO si;
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_PAGE | SIF_RANGE;
si.nPage = rFrame.Height();
si.nMax = m_iDocHeight + 8;
si.nMin = 0 ;
SetScrollInfo(SB_VERT, &si);
EnableScrollBarCtrl( SB_VERT, TRUE );
}
m_bScrollBarMessage = FALSE;
}
void CStaticTreeCtrl::OnPaint()
{
....
....
....
....
int iLastNodePos = 0
if( m_pTopNode->pChild != NULL )
{
iLastNodePos = DrawNodesRecursive( pDCMem,
m_pTopNode->pChild,
rFrame.left + m_iIndent,
m_iPadding - GetScrollPos( SB_VERT ),
rFrame );
if( m_bShowLines )
DrawLinesRecursive( pDCMem, m_pTopNode->pChild );
}
....
....
....
....
if( iLastNodePos != m_iDocHeight )
{
BOOL bInvalidate = ( ( m_iDocHeight < rFrame.Height() )
!= ( iLastNodePos < rFrame.Height() ) );
m_iDocHeight = iLastNodePos;
ResetScrollBar();
if( bInvalidate )
Invalidate();
}
}
void CStaticTreeCtrl::OnSize(UINT nType, int cx, int cy)
{
if( !m_bScrollBarMessage )
ResetScrollBar();
CStatic::OnSize(nType, cx, cy);
}
Remember that the recursive method DrawNodesRecursive
returns the location of the last node, in other words, the height of the tree. Comparing this return value with the previously stored one tells us if the vertical scrollbar needs to be reassessed (to hide it or modify its range). Also, when resizing the control (it can happen, don't roll your eyes), the word-wrap feature can change the height of the tree, so that's the other place where to organize a call to ResetScrollBar
.
The implementation of the WM_VSCROLL
looks as follows. Nothing fancy with the exception of an issue raised by G. Steudtel and worth mentioning (in fact, he raises a couple of issues worth a look). When loading very large amounts of nodes into the tree, the scrollbars behave abnormally. This is because the SB_THUMBTRACK
and SB_THUMBPOSITION
type of scroll messages are only 16-bits wide.
void CStaticTreeCtrl::OnVScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
int iScrollBarPos = GetScrollPos( SB_VERT );
CRect rFrame;
GetClientRect( rFrame );
switch( nSBCode )
{
case SB_LINEUP:
iScrollBarPos = max( iScrollBarPos - m_iLineHeight, 0 );
break;
case SB_LINEDOWN:
iScrollBarPos = min( iScrollBarPos + m_iLineHeight,
GetScrollLimit( SB_VERT ) );
break;
case SB_PAGEUP:
iScrollBarPos = max( iScrollBarPos - rFrame.Height(), 0 );
break;
case SB_PAGEDOWN:
iScrollBarPos = min( iScrollBarPos + rFrame.Height(),
GetScrollLimit( SB_VERT ) );
break;
case SB_THUMBTRACK:
case SB_THUMBPOSITION:
{
SCROLLINFO si;
ZeroMemory( &si, sizeof(SCROLLINFO) );
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_TRACKPOS;
if( GetScrollInfo( SB_VERT, &si, SIF_TRACKPOS ) )
iScrollBarPos = si.nTrackPos;
else
iScrollBarPos = (UINT)nPos;
break;
}
}
SetScrollPos( SB_VERT, iScrollBarPos );
Invalidate();
}
Problem is, we are not getting any message when clicking on the scrollbar!! #**@$*!@#*?!!
This is the point where you wish you had never voted Schwarzenegger into office and had used CWin
rather than CStatic
as the base class. This would have never happened with CWin
, you rightly bemoan, just what kind of sssschmuck would think of putting scrollbars to a static control?
There, there,... there is a hack. Fire up the ClassWizard and, under messages (although this one is not), double-click on WindowProc
. Include the following code:
LRESULT CStaticTreeCtrl::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
if( message == WM_NCHITTEST || message == WM_NCLBUTTONDOWN ||
message == WM_NCLBUTTONDBLCLK )
return ::DefWindowProc( m_hWnd, message, wParam, lParam );
return CStatic::WindowProc(message, wParam, lParam);
}
The idea is to redirect the relevant messages to avoid the standard processing of static controls (thanks to Vasily Pavlik).
The Mouse: Open/Close Nodes and Use the Wheel to Scroll
A tree control needs to respond to mouse clicks or it will not be worth much. Get the ClassWizard up, and add message handlers for WM_LBUTTONUP
and WM_MOUSEWHEEL
. Note that we use the left-button-up message rather than the left-button-down message because we respect the idiosyncrasies of those users given to change their mind's mid-click. Political correctness gone digital? Indeed, let us all congratulate ourselves, ain't we the best?
Back to reality now, and just for a sec mind you, a neat recursive method is again used to travel the tree, searching for the node that has been clicked on (or rather, clicked off, as the message is handled when the mouse button is released):
void CStaticTreeCtrl::ToggleNode( HTREENODE pNode, BOOL bInvalidate )
{
ASSERT( pNode != NULL );
pNode->bOpen = !( pNode->bOpen );
if( bInvalidate )
Invalidate();
}
HTREENODE CStaticTreeCtrl::FindNodeByPoint( const CPoint& point, HTREENODE pNode )
{
HTREENODE pFound = NULL;
if( pNode->rNode.PtInRect( point ) )
pFound = pNode;
if( pFound == NULL && pNode->bOpen && pNode->pChild != NULL )
pFound = FindNodeByPoint( point, pNode->pChild );
if( pFound == NULL && pNode->pSibling != NULL )
pFound = FindNodeByPoint( point, pNode->pSibling );
return pFound;
}
void CStaticTreeCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
HTREENODE pClickedOn = NULL;
if( m_pTopNode->pChild != NULL)
pClickedOn = FindNodeByPoint( point, m_pTopNode->pChild );
if( pClickedOn != NULL )
ToggleNode( pClickedOn, TRUE );
else
CStatic::OnLButtonUp(nFlags, point);
}
BOOL CStaticTreeCtrl::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
OnVScroll( ( zDelta > 0 )? SB_LINEUP:SB_LINEDOWN, 0, NULL );
return CStatic::OnMouseWheel(nFlags, zDelta, pt);
}
Only thing to note is that the control must have the focus to receive WM_MOUSEWHEEL
messages. Nothing weird really, I tell you this just in case you start torturing your mouse's wheel and nothing happens. Click on the tree control first, or simply make sure it has the focus, then work that wheel to exhaustion Fonda style.
Finishing Touches: Bitmap Background and Context Menu
Yes, none of this is necessary, the tree control works and, to all practical effects, it is finished. Still, Murphy recommends to meddle with perfectly sound code on account of sudden caprice. Let's fasten our seatbelts.
The bitmap background code is anything but hard. We need a public method to select a bitmap file, and a little modification to the OnPaint()
method to draw the bitmap on the device context before scribbling the tree nodes and lines onto it. Something like this:
void CStaticTreeCtrl::SetBackgroundBitmap( BOOL bInvalidate )
{
CFileDialog fd( TRUE, NULL, NULL, OFN_EXPLORER | OFN_FILEMUSTEXIST,
"Bitmap Files (*.bmp)|*.bmp||", this );
if( fd.DoModal() == IDOK )
{
if( m_bmpBackground.GetSafeHandle() != NULL )
m_bmpBackground.DeleteObject();
HBITMAP hBitmap = (HBITMAP)LoadImage( NULL, fd.GetPathName(), IMAGE_BITMAP,
0, 0,
LR_LOADFROMFILE |
LR_CREATEDIBSECTION | LR_DEFAULTSIZE );
m_bmpBackground.Attach( hBitmap );
if( bInvalidate )
Invalidate();
}
}
void CStaticTreeCtrl::OnPaint()
{
....
....
if( m_bmpBackground.GetSafeHandle() != NULL )
{
CDC* pDCTemp = new CDC;;
BITMAP bmp;
pDCTemp->CreateCompatibleDC( &dc );
m_bmpBackground.GetBitmap( &bmp );
CBitmap* pOldBitmap = (CBitmap*) pDCTemp->SelectObject( &m_bmpBackground );
pDCMem->StretchBlt( 0, 0, rFrame.Width(), rFrame.Height(), pDCTemp,
0, 0, bmp.bmWidth, bmp.bmHeight, SRCCOPY);
pDCTemp->SelectObject( pOldBitmap );
delete pDCTemp;
}
else
pDCMem->FillSolidRect( rFrame, RGB(255,255,255) );
UINT nMode = pDCMem->SetBkMode( TRANSPARENT );
CFont* pOldFont = pDCMem->SelectObject( &m_Font );
int iLastNodePos = 0
if( m_pTopNode->pChild != NULL )
{
....
....
....
....
}
You could also easily set it up so that, if there is no bitmap, the background color could be changed or made gradient, etc.
We are going to finish this tutorial by adding an owner-drawn audio context-menu to our tree from where to access node functionality, eye-candy frills, and what not. Hey, hey, hey... hold on a minute, Mork from Ork, did you say an audio context-menu? That's right, Mindy, here is the chance to frighten grandpa or na-na your little sister.
So, let's create a new class derived from CMenu
.
The basic steps to turn a menu to owner-drawn is to override two virtual methods, namely, MeasureItem
and DrawItem
(guess what each one does). But first, let's set the thing up with a protected class where to store menu item information and some other related public methods.
public:
virtual CContextMenu& AppendMenuItem ( UINT nFlags, UINT nID, CString csText,
CString csWavFile, CDC* pDC );
virtual CContextMenu& SetTextFont ( CFont* font );
virtual CContextMenu& SetColors ( COLORREF crText, COLORREF crBackground,
COLORREF crDisabled, COLORREF crSelected,
COLORREF crBorder );
protected:
class CContextMenuItem
{
public:
CContextMenuItem( CString csText, CString csWavFile )
{
m_csText = csText;
m_csWavFile = csWavFile;
}
~CContextMenuItem()
{
m_csText.Empty();
m_csWavFile.Empty();
}
CString m_csText;
CString m_csWavFile;
};
The class CContextMenuItem
can be used to store anything relevant to the menu item, be those icons, thumbnail bitmaps, URLs, whatever. Here, I've coded the basics (text) and the otiose (Wav filename).
The methods SetColors
and SetTextFont
simply populate protected members. The method AppendMenuItem
, however, does a couple of interesting things. First, it creates a menu item object that contains the menu text and Wav filename to play. Second, it adds this object to the menu with MF_OWNERDRAW
as one of its flags (this will trigger the calls to MeasureItem
and DrawItem
). Third, the size of the menu item's text is calculated and stored.
CContextMenu& CContextMenu::AppendMenuItem( UINT nFlags, UINT nID,
CString csText, CString csWavFile, CDC* pDC )
{
CContextMenuItem* cccItem = new CContextMenuItem( csText, csWavFile );
m_cptrMenuItems.Add( cccItem );
CMenu::AppendMenu( nFlags | MF_OWNERDRAW, nID, (ODDCHAR*)cccItem );
if( !csText.IsEmpty() )
{
CSize cSize = pDC->GetTextExtent( csText );
m_iWidth = max( m_iWidth, cSize.cx );
m_iHeight = max( m_iHeight, 8 + cSize.cy );
}
return *this;
}
CContextMenu::~CContextMenu()
{
for( int i = 0; i < m_cptrMenuItems.GetSize(); i++ )
delete (CContextMenuItem*)( m_cptrMenuItems.GetAt( i ) );
m_cptrMenuItems.RemoveAll();
}
The CPtrArray
protected object m_cptrMenuItems
is used to store the pointer of each menu item. And don't forget, as I did (thanks Steve Mayfield), to delete all menu item objects in the destructor once the context menu is discarded!
Now, we deal with MeasureItem
as follows. Each time the system needs to know the size of a menu item, it calls this method. The crucial intellection here is that each individual menu item could have its own height.
void CContextMenu::MeasureItem( LPMEASUREITEMSTRUCT lpMIS )
{
if( GetMenuState( lpMIS->itemID, MF_BYCOMMAND ) & MF_SEPARATOR )
{
lpMIS->itemWidth = m_iWidth;
lpMIS->itemHeight = 6;
}
else
{
lpMIS->itemWidth = m_iWidth;
lpMIS->itemHeight = m_iHeight;
}
}
Last, the crux of any owner-drawn menu, the method DrawItem
. This method is called for each item, so we cannot draw the entire thing at once as we do with our tree control. Instead, we draw one menu item at a time, as the system goes requesting them. Take a look at the code, it has plenty of comments.
void CContextMenu::DrawItem( LPDRAWITEMSTRUCT lpDIS )
{
CDC* pDC = CDC::FromHandle( lpDIS->hDC );
CRect rItem = lpDIS->rcItem;
BOOL bSelected = lpDIS->itemState & ODS_SELECTED;
UINT nAction = lpDIS->itemAction;
UINT nState = GetMenuState( lpDIS->itemID, MF_BYCOMMAND );
CContextMenuItem* cccItem =
reinterpret_cast<CContextMenuItem*>( lpDIS->itemData );
if( nAction & ODA_SELECT || nAction & ODA_DRAWENTIRE )
{
pDC->FillSolidRect( rItem, m_crBackground );
if( nState & MF_SEPARATOR )
{
rItem.DeflateRect( 4, 2, 4, 2 );
pDC->FillSolidRect( rItem, m_crBorder );
}
else
{
COLORREF crOldColor = pDC->SetTextColor( m_crText );
int iMode = pDC->SetBkMode( TRANSPARENT );
CFont* pOldFont = pDC->SelectObject( m_pFont );
if( nState & MFS_DISABLED )
{
rItem.DeflateRect( 8, 0, 0, 0 );
pDC->SetTextColor( m_crDisabled );
pDC->DrawText( cccItem->m_csText, rItem,
DT_VCENTER | DT_LEFT | DT_SINGLELINE );
}
else
{
if( bSelected )
{
rItem.DeflateRect( 2, 2, 2, 2 );
pDC->Draw3dRect( rItem, m_crBorder, m_crBorder );
rItem.DeflateRect( 1, 1, 1, 1 );
pDC->FillSolidRect( rItem, m_crSelected );
rItem.DeflateRect( 5, -3, 0, -3 );
if( m_bSoundOn )
{
PlaySound( NULL, NULL, SND_NOWAIT | SND_PURGE );
PlaySound( cccItem->m_csWavFile,
NULL, SND_NOWAIT | SND_FILENAME | SND_ASYNC );
}
}
else
rItem.DeflateRect( 8, 0, 0, 0 );
pDC->DrawText( cccItem->m_csText,
rItem, DT_VCENTER | DT_LEFT | DT_SINGLELINE );
}
pDC->SelectObject( pOldFont );
pDC->SetBkMode( iMode );
pDC->SetTextColor( crOldColor );
}
}
}
The magic boils down to getting the relevant information out of the LPDRAWITEMSTRUCT
structure and then following the logical paths. Does the menu item need to be drawn? If so, is it a separator, or a regular menu item? If it is a menu item, is it enabled or disabled? If it is enabled, is it also selected? Well, if it is selected, then let's paint a wicked rectangle with some funky background before drawing the text. And, yes, since we are at it, let's also play a Wav file to mock the occasion.
Note that in order to be able to use PlaySound
, we need to include the header mmsystem.h in our source and the library winmm.lib in our project (Project Settings, Link tab, Category 'General', Object/library modules). The code to play a Wav in a menu is so simple that one may wonder why it is not done more often (I mean, aside from the fact that it can easily become an infernal nuisance). By the way, if the Wav file is not found or some such, you will hear a ting, clink, or some other gracious system chime.
The context menu is done. We will now define a message for each menu item, and write code for OnContextMenu
in our tree control to create the menu on the fly. Let's go.
#define CM_INSERTCHILD WM_APP + 10000
#define CM_INSERTSIBLING WM_APP + 10001
#define CM_DELETENODE WM_APP + 10002
#define CM_MODIFYNODETEXT WM_APP + 10003
#define CM_CHANGENODECOLOR WM_APP + 10004
#define CM_TOGGLECONNECTINGLINES WM_APP + 10010
#define CM_SETCONNECTINGLINESCOLOR WM_APP + 10011
#define CM_SETFONT WM_APP + 10020
#define CM_SETDEFAULTCOLOR WM_APP + 10021
#define CM_SETBACKGROUNDBITMAP WM_APP + 10022
#define CM_TOGGLEMENUSOUND WM_APP + 10030
void CStaticTreeCtrl::OnContextMenu(CWnd* , CPoint point)
{
CPoint cp( point );
ScreenToClient( &cp );
if( m_pTopNode->pChild == NULL )
m_pSelected = NULL;
else
m_pSelected = FindNodeByPoint( cp, m_pTopNode->pChild );
CContextMenu ccmPopUp;
ccmPopUp.CreatePopupMenu();
ccmPopUp
.ToggleSound ( m_bAudioOn )
.SetTextFont ( &m_Font )
.SetColors ( RGB(70,36,36), RGB(253,249,249), RGB(172,96,96),
RGB(244,234,234), RGB(182,109,109) );
CDC *pDC = GetDC();
int iSaved = pDC->SaveDC();
CFont* pOldFont = pDC->SelectObject( &m_Font );
if( m_pSelected != NULL )
{
CString csDots = ( m_pSelected->csLabel.GetLength() > 45 )? _T("..."):_T("");
CString cs = m_pSelected->csLabel.Left( 45 ) + csDots;
ccmPopUp.AppendMenuItem( MF_DISABLED, WM_APP, cs, _T(""), pDC );
ccmPopUp.AppendMenuItem( MF_SEPARATOR, 0, _T(""), _T(""), pDC );
}
UINT nFlag = ( m_pSelected != NULL )? MF_ENABLED:MF_GRAYED;
ccmPopUp.AppendMenuItem( MF_ENABLED, CM_INSERTCHILD,
_T("Insert Child"),
_T("insertChild.wav"), pDC );
ccmPopUp.AppendMenuItem( nFlag, CM_INSERTSIBLING,
_T("Insert Sibling"),
_T("insertSibling.wav"), pDC );
ccmPopUp.AppendMenuItem( nFlag, CM_DELETENODE,
_T("Delete Node"),
_T("deleteNode.wav"), pDC );
ccmPopUp.AppendMenuItem( nFlag, CM_MODIFYNODETEXT,
_T("Modify Node Text"),
_T("modifyNodeText.wav"), pDC );
ccmPopUp.AppendMenuItem( nFlag, CM_CHANGENODECOLOR,
_T("Change Node Color"),
_T("changeNodeColor.wav"), pDC );
ccmPopUp.AppendMenuItem( MF_SEPARATOR, 0, _T(""), _T(""), pDC );
ccmPopUp.AppendMenuItem( MF_ENABLED, CM_TOGGLECONNECTINGLINES,
_T("Toggle Connecting Lines"),
_T("toggleConnectingLines.wav"), pDC );
ccmPopUp.AppendMenuItem( MF_ENABLED, CM_SETCONNECTINGLINESCOLOR,
_T("Set Connecting Lines Color"),
_T("setConnectingLinesColor.wav"), pDC );
ccmPopUp.AppendMenuItem( MF_SEPARATOR, 0, _T(""), _T(""), pDC );
ccmPopUp.AppendMenuItem( MF_ENABLED, CM_SETFONT,
_T("Set Font"),
_T("setFont.wav"), pDC );
ccmPopUp.AppendMenuItem( MF_ENABLED, CM_SETDEFAULTCOLOR,
_T("Set Default Text Color"),
_T("setDefaultColor.wav"), pDC );
ccmPopUp.AppendMenuItem( MF_ENABLED, CM_SETBACKGROUNDBITMAP,
_T("Set Background Bitmap"),
_T("setBackgroundBitmap.wav"), pDC );
ccmPopUp.AppendMenuItem( MF_SEPARATOR, 0, _T(""), _T(""), pDC );
ccmPopUp.AppendMenuItem( MF_ENABLED, CM_TOGGLEMENUSOUND,
_T("Toggle Menu Sound"),
_T("toggleMenuSound.wav"), pDC );
ccmPopUp.TrackPopupMenu( TPM_LEFTALIGN, point.x, point.y, this );
pDC->SelectObject( pOldFont );
pDC->RestoreDC( iSaved );
ReleaseDC( pDC );
}
The method OnContextMenu
is an easy jog even for the faint-hearted. First, find out which node was under the mouse when the context menu was invoked (we will need this information when reacting to the context-menu generated messages). Then, create the menu, customize its look, and populate it with the appropriate entries (enabled or disabled).
When the user clicks on a menu item, the corresponding message is sent to the owner window (the tree control). Thus, we write code to handle these messages. This requires three steps: first, declare protected methods to handle all messages; second, add ON_COMMAND
macros to the message map in the source file; third, write the implementation for these methods.
The declarations are as follows. Take a look at the sources for the implementation of these methods, nothing fancy there.
protected:
....
....
void OnCM_InsertChild();
void OnCM_InsertSibling();
void OnCM_DeleteNode();
void OnCM_ModifyNodeText();
void OnCM_ChangeNodeColor();
void OnCM_ToggleConnectingLines();
void OnCM_SetConnectingLinesColor();
void OnCM_SetFont();
void OnCM_SetDefaultColor();
void OnCM_SetBackgroundBitmap();
void OnCM_ToggleMenuSound();
BEGIN_MESSAGE_MAP(CStaticTreeCtrl, CStatic)
....
....
ON_COMMAND(CM_INSERTCHILD, OnCM_InsertChild)
ON_COMMAND(CM_INSERTSIBLING, OnCM_InsertSibling)
ON_COMMAND(CM_DELETENODE, OnCM_DeleteNode)
ON_COMMAND(CM_MODIFYNODETEXT, OnCM_ModifyNodeText)
ON_COMMAND(CM_CHANGENODECOLOR, OnCM_ChangeNodeColor)
ON_COMMAND(CM_TOGGLECONNECTINGLINES, OnCM_ToggleConnectingLines)
ON_COMMAND(CM_SETCONNECTINGLINESCOLOR, OnCM_SetConnectingLinesColor)
ON_COMMAND(CM_SETFONT, OnCM_SetFont)
ON_COMMAND(CM_SETDEFAULTCOLOR, OnCM_SetDefaultColor)
ON_COMMAND(CM_SETBACKGROUNDBITMAP, OnCM_SetBackgroundBitmap)
ON_COMMAND(CM_TOGGLEMENUSOUND, OnCM_ToggleMenuSound)
END_MESSAGE_MAP()
Compile and run. We are finished. I think. I can't quite feel my toes anymore.
Feedback
My intention has been to provide a tutorial that is coded clearly, as simple to understand and follow as possible. I am sure that there are finer solutions to the functionality I have implemented here. Any suggestions that improve, simplify, or better explain the code are welcome.
Acknowledgments
For the demo project, I've used an old version of CResizableDialog by Paolo Messina that I found lying around my HD. Great code, thanks Paolo.
Other than that, I want to acknowledge everyone at CodeProject. My appreciation goes to the folks that make it happen and, especially, to all those guys that continue to freely share what they know. Thank you all.