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

CStaticTreeCtrl - A CStatic derived custom Tree control

0.00/5 (No votes)
1 May 2006 1  
Step-by-step creation of a custom Tree control from a CStatic control, implementing basic functionality, eye-candy (font, bitmap background, etc.), scrolling (bars and wheel), multiline (wrapping) text, and audio context menu.

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):

// Attributes

protected:
    CFont    m_Font;
    COLORREF m_crDefaultTextColor;

// Operations

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:

// Attributes

protected:
    HTREENODE m_pTopNode;

// Operations

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.

/////////////////////////////////////

// Constructor

/////////////////////////////////////


CStaticTreeCtrl::CStaticTreeCtrl()
{
    m_pTopNode = new CTreeNode();
}

/////////////////////////////////////

// Public methods

/////////////////////////////////////


HTREENODE CStaticTreeCtrl::InsertSibling( HTREENODE pInsertAfter, 
             const CString& csLabel,
             COLORREF crText /* = 0 */, 
             BOOL bUseDefaultTextColor /* = TRUE */,
             BOOL bInvalidate /* = FALSE  */)
{
    // Make sure the node exists

    ASSERT( pInsertAfter != NULL );
    
    HTREENODE pNewNode     = new CTreeNode();

    // New node's label

    pNewNode->csLabel      = csLabel;

    if( bUseDefaultTextColor )
        // Use the default text color

        pNewNode->bUseDefaultTextColor = TRUE;
    else
        // New node's text color

        pNewNode->crText   = crText;

    // Nas the same parent

    pNewNode->pParent      = pInsertAfter->pParent;

    // Insert the new node between

    // pInsertAfter and its next sibling

    pNewNode->pSibling     = pInsertAfter->pSibling;
    pInsertAfter->pSibling = pNewNode;

    // Repaint the control if so desired

    if( bInvalidate )
        Invalidate();
    
    return pNewNode;
}

HTREENODE CStaticTreeCtrl::InsertChild( HTREENODE pParent, 
             const CString& csLabel,
             COLORREF crText /* = 0 */, 
             BOOL bUseDefaultTextColor /* = TRUE */,
             BOOL bInvalidate /* = FALSE  */)
{
    // Make sure the node exists

    ASSERT( pParent != NULL );

    if( pParent == HTOPNODE )
    // Check for top node

        pParent = m_pTopNode;

    HTREENODE pNewNode   = new CTreeNode();

    // Basic node information

    pNewNode->csLabel    = csLabel;
    // New node's label


    if( bUseDefaultTextColor )
        // Use the default text color

        pNewNode->bUseDefaultTextColor = TRUE;
    else
        // New node's text color

        pNewNode->crText = crText;

    // New node's parent

    pNewNode->pParent    = pParent;

    // Insert the new node as pParent's first child

    pNewNode->pSibling   = pParent->pChild;
    pParent->pChild      = pNewNode;

    // Repaint the control if so desired

    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.

/////////////////////////////////////

// Public methods

/////////////////////////////////////


void CStaticTreeCtrl::DeleteNode( HTREENODE pNode, 
                      BOOL bInvalidate /* = FALSE  */)
{
    ASSERT( pNode != NULL );        // Make sure the node exists


    // Don't delete the top node

    if( pNode == HTOPNODE )
        DeleteNode( m_pTopNode, bInvalidate );

    // Delete childs

    if( pNode->pChild != NULL )
        DeleteNodeRecursive( pNode->pChild );

    // If the node is not the top node, fix pointers

    // in sibling list and then delete the node

    if( pNode != m_pTopNode )
    {
        HTREENODE pRunner = pNode->pParent;
        
        // If the node is the first child,

        // set the parent pointer to the next sibling

        // Otherwise, find sibling before the node

        // and set this sibling pointer to the node's sibling

        if( pRunner->pChild == pNode )
            pRunner->pChild = pNode->pSibling;
        else
        {
            pRunner = pRunner->pChild;

            // Loop until the next node is the one being deleted

            while( pRunner->pSibling != pNode )
                pRunner = pRunner->pSibling;

            pRunner->pSibling = pNode->pSibling;
        }

        // Pointers fixed, the node can be safely deleted

        delete pNode;

        pNode = NULL;
    }

    if( bInvalidate )
        Invalidate();
}

////////////////////////////////////////////////////////////////

//    PROTECTED METHODS

////////////////////////////////////////////////////////////////


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:

/////////////////////////////////////

// Destructor

/////////////////////////////////////


CStaticTreeCtrl::~CStaticTreeCtrl()
{
    DeleteNode( m_pTopNode );  // Delete all childs if there are any

    delete m_pTopNode;         // Delete top node

    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()
{
    // Device context for painting

    CPaintDC dc(this);
    
    // Double-buffering

    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 );

    // START DRAW -------------------------------------------------


    pDCMem->FillSolidRect( rFrame, RGB(255,255,255) );    // Background


    pDCMem->Draw3dRect( rFrame, RGB(0,0,0), RGB(0,0,0) ); // Border


    // END DRAW   -------------------------------------------------


    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).

/////////////////////////////////////////////////////////////////////

//    PROTECTED METHODS

/////////////////////////////////////////////////////////////////////


int CStaticTreeCtrl::DrawNodesRecursive( CDC* pDC, HTREENODE pNode, 
                                         int x, int y, CRect rFrame )
{
    int   iDocHeight = 0; // Total document height

    CRect rNode;

    // The node's location and dimensions on screen

    rNode.left   = x;
    rNode.top    = y;
    rNode.right  = rFrame.right - m_iPadding;
    rNode.bottom = y + m_iLineHeight;

    pNode->rNode.CopyRect( rNode ); // Record the rectangle


    COLORREF cr        = 
        ( pNode->bUseDefaultTextColor )? m_crDefaultTextColor:pNode->crText;
    COLORREF crOldText = pDC->SetTextColor( cr );

    // Draw the text - THIS LINE WILL BE REPLACED BY THE MULTILINE CODE

    pDC->DrawText( pNode->csLabel, rNode, DT_LEFT | DT_SINGLELINE | DT_VCENTER );

    pDC->SetTextColor( crOldText );

    // If there are no child or siblings, then this branch is done

    if( pNode->pChild == NULL &&  pNode->pSibling == NULL )
        return pNode->rNode.Height();

    // If the node is open AND it has childs, then draw those

    if( pNode->bOpen && pNode->pChild != NULL )
        iDocHeight = DrawNodesRecursive( pDC,
                                         pNode->pChild,
                                         x + m_iIndent,
                                         y + pNode->rNode.Height(),
                                         rFrame );

    // If the node has siblings, then draw those

    if( pNode->pSibling != NULL )
        iDocHeight += DrawNodesRecursive( pDC,
                                          pNode->pSibling,
                                          x,
                                          y + pNode->rNode.Height() + iDocHeight,
                                          rFrame );

    return iDocHeight + pNode->rNode.Height();
}

/////////////////////////////////////////////////////////////////////////////

// CStaticTreeCtrl message handlers


void CStaticTreeCtrl::OnPaint() 
{
    ....
    ....

    // START DRAW -------------------------------------------------


    pDCMem->FillSolidRect( rFrame, RGB(255,255,255) );    // Background


    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) ); // Border


    // END DRAW   -------------------------------------------------


    ....
    ....
}

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:

/////////////////////////////////////////////////////////////////////////////

//    PROTECTED METHODS

/////////////////////////////////////////////////////////////////////////////


int CStaticTreeCtrl::HowMuchTextFits( CDC* pDC, 
                     int iAvailableWidth, CString csText )
{
    int iValidSoFar = csText.GetLength() - 1; // Assume the entire text fits


    // If the text's pixel width is larger than what's available

    if( pDC->GetTextExtent( csText ).cx > iAvailableWidth )
    {
        int iNextBlank  = 0; // Position of the next blank in text

        int iPixelWidth = 0; // Text's pixel width


        // Loop until we can fit no more of the text

        while( iPixelWidth < iAvailableWidth )
        {
            iValidSoFar = iNextBlank;  // Record the char pos so far

            iNextBlank  = csText.Find( ' ', iNextBlank + 1 );
            // Advance one word at a time


            // Have reached the end of the string?

            if( iNextBlank == -1 )
                iNextBlank = csText.GetLength();

            // Calculate the new width

            iPixelWidth = pDC->GetTextExtent( csText.Left( iNextBlank ) ).cx;
        }
    }

    return iValidSoFar;
}

int CStaticTreeCtrl::DrawNodesRecursive( CDC* pDC, 
                     HTREENODE pNode, int x, int y, CRect rFrame )
{
    ....
    ....

    // MULTILINE TEXT - begins

    CString cs   = pNode->csLabel;
    int     iPos = 0;

    // Draw text until there is nothing left to draw

    while( cs.GetLength() > 0 )
    {
        // Height of a line of text

        rNode.bottom = rNode.top + m_iLineHeight;

        // Find out how much text fits in one line

        iPos = HowMuchTextFits( pDC, 
               rFrame.right - m_iPadding - rNode.left, cs );

        // Draw only if the node is visible

        if( rNode.bottom > 0 && rNode.top < rFrame.bottom )
            pDC->DrawText( cs.Left( iPos + 1 ), 
                 rNode, DT_LEFT | DT_SINGLELINE | DT_VCENTER );

        // Eliminate the text that has been already drawn

        cs = cs.Mid( iPos + 1 );

        // The node grows everytime another line of text is drawn

        pNode->rNode.UnionRect( pNode->rNode, rNode );

        // Move down the drawing rectangle for the next line of text

        rNode.top = rNode.bottom;
    }
    // MULTILINE TEXT - ends


    ....
    ....
}

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.

/////////////////////////////////////////////////////////////////////////////

//    PROTECTED METHODS

/////////////////////////////////////////////////////////////////////////////


void CStaticTreeCtrl::DrawLinesRecursive( CDC* pDC, HTREENODE pNode )
{
    // Draw lines from childs if the node is open

    // before drawing lines from this node

    if( pNode->bOpen && pNode->pChild != NULL )
        DrawLinesRecursive( pDC, pNode->pChild );

    // Where is the elbow joint of this connecting line?

    int iJointX = pNode->rNode.left - m_iIndent - 6;
    int iJointY = pNode->rNode.top + ( m_iLineHeight / 2 );

    // If the parent is not the top node, throw a connecting line to it

    if( pNode->pParent != m_pTopNode )
    {
        // How far up from the joint is the parent?

        int iDispY = 
            iJointY - pNode->pParent->rNode.top - ( m_iLineHeight / 2 );
        
        // Use 1 pixel wide rectangles to draw lines

        pDC->FillSolidRect( iJointX, iJointY, 
             m_iIndent, 1, m_crConnectingLines ); // Horizontal line

        pDC->FillSolidRect( iJointX, iJointY, 1, 
             -iDispY, m_crConnectingLines );   // Vertical line

    }

    // Put a solid dot to mark a node

    pDC->FillSolidRect( iJointX + m_iIndent - 2, 
                        iJointY - 2, 5, 5, m_crConnectingLines );

    // Hollow out the dot if the node has no childs

    if( pNode->pChild == NULL )
        pDC->FillSolidRect( iJointX + m_iIndent - 1, 
                            iJointY - 1, 3, 3, RGB(255,255,255) );

    // Draw the next sibling if there are any

    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:

/////////////////////////////////////////////////////////////////////////////

//    PROTECTED METHODS

/////////////////////////////////////////////////////////////////////////////


void CStaticTreeCtrl::ResetScrollBar()
{
    // Flag to avoid a call from OnSize while resetting the scrollbar

    m_bScrollBarMessage = TRUE;

    CRect rFrame;

    GetClientRect( rFrame );

    // Need for scrollbars?

    if( rFrame.Height() > m_iDocHeight + 8 )
    {
        ShowScrollBar( SB_VERT, FALSE );    // Hide it

        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;
}

/////////////////////////////////////////////////////////////////////////////

// CStaticTreeCtrl message handlers


void CStaticTreeCtrl::OnPaint() 
{
    ....
    ....

    // START DRAW -------------------------------------------------


    ....
    ....

    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 );
    }

    ....
    ....

    // END DRAW   -------------------------------------------------


    ....
    ....

    // Has the total document height changed?

    if( iLastNodePos != m_iDocHeight )
    {
        BOOL bInvalidate = ( ( m_iDocHeight < rFrame.Height() ) 
                        != ( iLastNodePos < rFrame.Height() ) );
 
        m_iDocHeight = iLastNodePos;
 
        ResetScrollBar();
 
        // If the scrollbar has just been hidden/shown, repaint

        if( bInvalidate )
            Invalidate();
    }
}

void CStaticTreeCtrl::OnSize(UINT nType, int cx, int cy) 
{
    // Setting the scroll sends its own size message.

    // Prevent it thus avoiding an ugly loop.

    // Other than that, resizing the control means

    // that the tree height may change (word-wrap).

    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.

///////////////////////////////////////////////////////////////////////////

// CStaticTreeCtrl message handlers


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:

/////////////////////////////////////////////////////////////////////////////

// CStaticTreeCtrl message handlers


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):

/////////////////////////////////////////////////////////////////////////////

//    PUBLIC METHODS

/////////////////////////////////////////////////////////////////////////////


void CStaticTreeCtrl::ToggleNode( HTREENODE pNode, BOOL bInvalidate /* = FALSE  */)
{
    ASSERT( pNode != NULL );

    pNode->bOpen = !( pNode->bOpen );

    if( bInvalidate )
        Invalidate();
}

/////////////////////////////////////////////////////////////////////////////

//    PROTECTED METHODS

/////////////////////////////////////////////////////////////////////////////


HTREENODE CStaticTreeCtrl::FindNodeByPoint( const CPoint& point, HTREENODE pNode )
{
    HTREENODE pFound = NULL;

    // Found it?

    if( pNode->rNode.PtInRect( point ) )
        pFound = pNode;
    
    // If this node isn't it then check the node's childs

    // if it is open and there are any

    if( pFound == NULL && pNode->bOpen && pNode->pChild != NULL )
        pFound = FindNodeByPoint( point, pNode->pChild );

    // If didn't find it among the node's childs, then check the next sibling 

    if( pFound == NULL && pNode->pSibling != NULL )
        pFound = FindNodeByPoint( point, pNode->pSibling );

    return pFound;
}

/////////////////////////////////////////////////////////////////////////////

// CStaticTreeCtrl message handlers


void CStaticTreeCtrl::OnLButtonUp(UINT nFlags, CPoint point) 
{
    HTREENODE pClickedOn = NULL;        // Assume no node was clicked on


    if( m_pTopNode->pChild != NULL)     // If the tree is populated, search it

        pClickedOn = FindNodeByPoint( point, m_pTopNode->pChild );

    if( pClickedOn != NULL )            // If a node was clicked on

        ToggleNode( pClickedOn, TRUE );
    else
        CStatic::OnLButtonUp(nFlags, point);
}

BOOL CStaticTreeCtrl::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) 
{
    // zDelta greater than 0, means rotating away

    // from the user, that is, scrolling up

    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:

/////////////////////////////////////////////////////////////////////////////

//    PUBLIC METHODS

/////////////////////////////////////////////////////////////////////////////


void CStaticTreeCtrl::SetBackgroundBitmap( BOOL bInvalidate /* = FALSE  */)
{
    CFileDialog fd( TRUE, NULL, NULL, OFN_EXPLORER | OFN_FILEMUSTEXIST,
                    "Bitmap Files (*.bmp)|*.bmp||", this );

    // If the user clicked 'ok'

    if( fd.DoModal() == IDOK )
    {
        // If there is a bitmap already loaded, delete it

        if( m_bmpBackground.GetSafeHandle() != NULL )
            m_bmpBackground.DeleteObject();
        
        // Load the bitmap from the file selected

        HBITMAP hBitmap =  (HBITMAP)LoadImage( NULL, fd.GetPathName(), IMAGE_BITMAP, 
                                    0, 0,
                                    LR_LOADFROMFILE | 
                                    LR_CREATEDIBSECTION | LR_DEFAULTSIZE );

        // Attach it to the CBitmap object

        m_bmpBackground.Attach( hBitmap );

        // Repaint if so desired

        if( bInvalidate )
            Invalidate();
    }
}

/////////////////////////////////////////////////////////////////////////////

// CStaticTreeCtrl message handlers


void CStaticTreeCtrl::OnPaint() 
{
    ....
    ....

    // START DRAW -------------------------------------------------


    // If there is a bitmap loaded, use it

    // Otherwise, paint the background white

    if( m_bmpBackground.GetSafeHandle() != NULL )
    {
        CDC*   pDCTemp = new CDC;;
        BITMAP bmp;

        pDCTemp->CreateCompatibleDC( &dc );

        m_bmpBackground.GetBitmap( &bmp );

        // Select the bitmap into the temp device context

        CBitmap* pOldBitmap = (CBitmap*) pDCTemp->SelectObject( &m_bmpBackground );

        // Stretch the bitmap to fill the entire control area

        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 )
    {

    ....
    ....

    // END DRAW   -------------------------------------------------


    ....
    ....
}

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.

/////////////////////////////////////////////////////////////////////////////

//    PUBLIC METHODS

/////////////////////////////////////////////////////////////////////////////

CContextMenu& CContextMenu::AppendMenuItem( UINT nFlags, UINT nID, 
                     CString csText, CString csWavFile, CDC* pDC )
{
    CContextMenuItem* cccItem = new CContextMenuItem( csText, csWavFile );

    // Store the pointer

    m_cptrMenuItems.Add( cccItem );

    // Append menu

    CMenu::AppendMenu( nFlags | MF_OWNERDRAW, nID, (ODDCHAR*)cccItem );
    
    // Calculate the size of the menu's text

    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;
}

//////////////////////////////////////////////////////////////////////

// Construction/Destruction

//////////////////////////////////////////////////////////////////////

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 )
{
    // Separator?

    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 )
{
    // Get the relevant information

    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 );

    // Does this menu item need to be drawn?

    if( nAction & ODA_SELECT || nAction & ODA_DRAWENTIRE )
    {
        // Background

        pDC->FillSolidRect( rItem, m_crBackground );

        // Separator or Text

        if( nState & MF_SEPARATOR )
        {
            rItem.DeflateRect( 4, 2, 4, 2 );

           // A thin rectangle that could be anything you want

           pDC->FillSolidRect( rItem, m_crBorder );
        }
        else
        {
            // Prepare the device context and store previous values

            COLORREF crOldColor = pDC->SetTextColor( m_crText );
            int      iMode      = pDC->SetBkMode( TRANSPARENT );
            CFont*   pOldFont   = pDC->SelectObject( m_pFont );

            // Is the item disabled?

            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 the item is selected, paint a rectangle,

                // change the background color

                // and play the wav file if relevant

                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 )
                    {
                        // Stop any currently playing wav

                        PlaySound( NULL, NULL, SND_NOWAIT | SND_PURGE );
                        // Play this item's wav

                        PlaySound( cccItem->m_csWavFile, 
                                   NULL, SND_NOWAIT | SND_FILENAME | SND_ASYNC );
                    }
                }
                else
                    rItem.DeflateRect( 8, 0, 0, 0 );

                // Last, draw the text on top of everything else

                pDC->DrawText( cccItem->m_csText, 
                               rItem, DT_VCENTER | DT_LEFT | DT_SINGLELINE );
            }

            // Clean up

            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.

// In StaticTreeCtrl.h


#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

// In StaticTreeCtrl.cpp


void CStaticTreeCtrl::OnContextMenu(CWnd* /*pWnd*/, CPoint point) 
{
    CPoint cp( point );

    // WM_CONTEXTMENU passes absolute coordinates, we need them local

    ScreenToClient( &cp );

    // Find the node that has been clicked on

    if( m_pTopNode->pChild == NULL )
        m_pSelected = NULL;    // Empty tree

    else
        m_pSelected = FindNodeByPoint( cp, m_pTopNode->pChild );

    CContextMenu ccmPopUp;

    ccmPopUp.CreatePopupMenu();

    // Customize the menu appearance and behavior

    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) );

    // Get a device context so that it'll be possible for the context menu

    // to calculate the size of the menu item's text

    CDC    *pDC     = GetDC();
    int    iSaved   = pDC->SaveDC();
    CFont* pOldFont = pDC->SelectObject( &m_Font );

    // ADDING MENU ITEMS - Start


    // If a node has been clicked on, use the first 45 chars of its text as the

    // first menu item (always disabled)

    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;

    // Node related items

    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 );

    // Connecting lines related items

    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 );
    
    // Tree appearance items

    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 );
    
    // Context menu sound toggle item

    ccmPopUp.AppendMenuItem( MF_ENABLED, CM_TOGGLEMENUSOUND,
                             _T("Toggle Menu Sound"),
                             _T("toggleMenuSound.wav"), pDC );

    // ADDING MENU ITEMS - End


    // Display the context menu

    ccmPopUp.TrackPopupMenu( TPM_LEFTALIGN, point.x, point.y, this );

    // Clean up

    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.

// In StaticTreeCtrl.h


protected:

    ....
    ....

    // Message handlers

    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();

// In StaticTreeCtrl.cpp


BEGIN_MESSAGE_MAP(CStaticTreeCtrl, CStatic)
    //{{AFX_MSG_MAP(CStaticTreeCtrl)


    ....
    ....
    
    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)
    //}}AFX_MSG_MAP

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.

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