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

A custom-drawn TreeList Control

0.00/5 (No votes)
17 Mar 2000 1  
A custom-drawn tree-list hybrid, with explanations on how the control was developed.

Sample Image - TreeListCtrlGerolf.gif

Introduction

This treelist control is an owner-drawn tree control. I know that there are many treelist controls in CodeProject or CodeGuru, but I didn't find one that fits to my needs. Most of them override the WM_PAINT message. That's very simple but a little bit slow (in my tests) if you have many tree items (about 500 and more).

David Lantsman's article helped me a lot, so also the article of Garen Hartunian about custom drawn tree controls.

I mixed everything I found about tree list controls and custom-drawn tree controls and made a new class: CTreeListView. You can use this class like a CTreeView and manipulate columns like in CListView. I took the definitions of the functions from these classes so that it is mostly compatible to the MFC classes. But now, it's enough gossip.

How to use CTreeListView in your project

Just add the class to your project and inherit your view from CTreeListView instead of CTreeView. Now, replace all calls to your earlier base class (perhaps CTreeView) with CTreeListView. That's it. Now, you can begin parameterising the class like below in OnInitialUpdate. You see me inserting columns and pictures, and also items and subitems. It's nearly the same as in CTreeView and CListView. The functions are described below (look here for functions). After that, you will find the overview over the classes (overview of classes, the complete documentation can be found here. Download class reference - 27.6 Kb) and the description of the implementation of the drawing functions (look here for implementation).

Example of use of CTreeListView

void CTreeListCtrlView::OnInitialUpdate()
{
    CTreeListView::OnInitialUpdate();

    m_Images.Create(16,16, ILC_COLOR | ILC_MASK, 0, 1);
    CBitmap bm;
    bm.LoadBitmap(IDB_BITMAP1);
    m_Images.Add(&bm, RGB(255, 255, 255));
    bm.DeleteObject();
    bm.LoadBitmap(IDB_BITMAP2);
    m_Images.Add(&bm, RGB(255, 255, 255));

    CTreeCtrl& ctrl = GetTreeCtrl();

    InsertColumn(0, _T("first column"), LVCFMT_LEFT, 200);
    InsertColumn(1, _T("second column"), LVCFMT_LEFT, 200);
    InsertColumn(2, _T("third column"), LVCFMT_LEFT, 200);

    ctrl.SetImageList(&m_Images, TVSIL_NORMAL);
    ctrl.SetImageList(&m_Images, TVSIL_STATE);

    HTREEITEM hItem, hItem2;

    hItem = ctrl.InsertItem(_T("ItemText1"), 0, 1);
    SetSubItemText(hItem,    1,_T("Subitem    1"));
    SetSubItemText(hItem, 2, 
      _T("Subitem2################################################"));
    hItem2 = ctrl.InsertItem(_T("ItemText  1.2"), 0, 1, hItem);
    SetSubItemText(hItem2, 1, _T("Subitem 1"));
    SetSubItemText(hItem2, 2, _T("Subitem 2"));
    hItem2 = ctrl.InsertItem(_T("ItemText 1.2"),0, 1, hItem);
    SetSubItemText(hItem2, 2, _T("Subitem 2"));
    for(long i = 2;i <  150; i++)
    {
        CString str;
        str.Format("ItemText %d", i);
        hItem = ctrl.InsertItem(str, 0, 1);
        SetSubItemText(hItem, 1, _T("Subitem 1"));
        SetSubItemText(hItem, 2, _T("Subitem 2"));
    }
}

Description of the public functions of CTreeListView:

  • int InsertColumn( int nCol, const LVCOLUMN* pColumn )

    Inserts a column in the CTreeListView. You have to fill the structure like in CListView.

  • int InsertColumn( int nCol, LPCTSTR lpszColumnHeading, int nFormat = LVCFMT_LEFT, int nWidth = -1, int nSubItem = -1 )

    Inserts a column in the CTreeListView. You have to fill the parameters like in CListView.

  • BOOL DeleteColumn( int nCol )

    Deletes the specified column and all texts that are specified for this column.

  • BOOL SetSubItemText( HTREEITEM hItem, int nSubItem, CString strBuffer)

    Sets the text of a specified tree control item (hItem) and a specified column (nSubItem). nSubItem = 0 means the first column (main name).

  • BOOL GetSubItemText( HTREEITEM hItem, int nSubItem, CString& strBuffer)

    Returns the text of a specified tree control item (hItem) and a specified column (nSubItem). nSubItem = 0 means the first column (main name). You will receive the text in strBuffer.

  • CImageList* GetHeaderImageList()

    Retrieves the imagelist that is used by the header.

  • CImageList* SetHeaderImageList( CImageList * pImageList)

    Sets the imagelist that the header can use to display images.

  • CTreeCtrl& GetTreeCtrl()

    This function returns a reference to the tree control that is used. You can use this reference to delete or insert items. Do not change item texts with this function, because the texts are saved as subitem texts, and you will only see them.

  • void ShowHeader(bool bShow)

    This function hides and shows the header (column headers). You can control this by the bShow parameter (true means show the header, false means hide the header).

Here are the classes and structures that I use

  • struct _HeaderData

    Contains the data for one column so I can get it quickly at painting time without asking the header control.

  • class CMyTreeObj : public CObject

    Represents one object in the tree with all its columns.

  • class CMyTreeCtrl : public CTreeCtrl

    An overridden tree control that enables me to acknowledge when an item is inserted in the tree. The class sends a user defined message with a HTREEITEM parameter.

  • class CTreeListView : public CView

    Main view containing the treelist. I didn't inherit from CTreeView because this subclasses CTreeCtrl and I didn't find out how to resize the tree to put a header control on top of it. But this way is also OK.

Implementation

Drawing of the TreeItems

To use ownerdraw in tree controls, you can override the WM_NOTIFY message handler of the parent class of the control. There, you can test if the notify code is NM_CUSTOMDRAW. I've overridden TVN_DELETEITEM to delete my row for column texts for each item. I've also overridden HDN_ENDTRACK for the header control so I can update my internal structure of header data that I use for drawing:

BOOL CTreeListView::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
    LPNMHDR pNmhdr = (LPNMHDR)lParam;

    //TreeCtrl notifications

    if(m_ctrlTree.m_hWnd == pNmhdr->hwndFrom)
    {
        switch (pNmhdr->code)
        {
        case NM_CUSTOMDRAW:
            return OnCustomdrawTree(pNmhdr, pResult);
        case TVN_DELETEITEM:
            return OnDeleteItem(pNmhdr, pResult);
        }
    }
    //HeaderCtrl notifications

    else if(m_ctrlHeader.m_hWnd == pNmhdr->hwndFrom)
    {
        switch (pNmhdr->code)
        {
        case HDN_ENDTRACK:
            return OnEndTrack(pNmhdr, pResult);
        }
    }

    return CView::OnNotify(wParam, lParam, pResult);
}

This function is a bit tricky.

I use static variables to save the data of one object from CDDS_ITEMPREPAINT to CDDS_ITEMPOSTPAINT notification. The CDDS_ITEMPOSTPAINT notification follows immediately the drawing of this object. There will be no CDDS_ITEMPREPAINT for another object before. But most data is only send with the CDDS_ITEMPREPAINT notification, so I have to save it. Before drawing (CDDS_PREPAINT notification), I have to set the Viewport in case of the horizontal scrollbar that I use. The way I use it is described at David Lantsman's article. I use it the same way.

At the CDDS_ITEMPREPAINT notification, I set the foreground color and the background color to the same value, so you won't see the drawing of the tree. I could have done this also by not giving it the texts, but then I would have to implement many things (like jump to an item by pressing a key) by myself. I wanted to use as much as possible of the originally tree.

BOOL CTreeListView::OnCustomdrawTree(LPNMHDR pNmhdr, LRESULT* pResult)
{
    static CRect    rcItem;
    static CPoint    poi;
    static bool        bFocus;

    BOOL bRet = FALSE;

    LPNMTVCUSTOMDRAW pCustomDraw = (LPNMTVCUSTOMDRAW)pNmhdr;
    switch (pCustomDraw->nmcd.dwDrawStage)
    {
        case CDDS_PREPAINT:
            // Need to process this case and set

            // pResult to CDRF_NOTIFYITEMDRAW,

            // otherwise parent will never receive

            // CDDS_ITEMPREPAINT notification. (GGH)

            *pResult = CDRF_NOTIFYITEMDRAW;

            // reposuition the viewport so the TreeCtrl

            // DefWindowProc doesn't draw to 

            // viewport 0/0

            ::SetViewportOrgEx(pCustomDraw->nmcd.hdc, m_nOffset, 0, NULL);
            bRet = TRUE;
            break;

        case CDDS_ITEMPREPAINT:
            // set the background and foregroundcolor of the item 

            // to the background,

            // so you don't see the default drawing of the text

            pCustomDraw->clrText = m_colBackColor;
            pCustomDraw->clrTextBk = m_colBackColor;

            // reset the focus, because it will be drawn of us

            bFocus = false;
            if(    pCustomDraw->nmcd.uItemState & CDIS_FOCUS)
            {
                bFocus = true;
            }

            pCustomDraw->nmcd.uItemState &= ~CDIS_FOCUS;

            // remember the drawing rectangle

            // of the item so we can draw it ourselves

            m_ctrlTree.GetItemRect((HTREEITEM) pCustomDraw->nmcd.dwItemSpec, 
                                                                &rcItem, TRUE);
            rcItem.right = 
              (pCustomDraw->nmcd.rc.right > m_nHeaderWidth) ? 
              pCustomDraw->nmcd.rc.right : m_nHeaderWidth;

            // we want to get the CDDS_ITEMPOSTPAINT notification

            *pResult = CDRF_NOTIFYPOSTPAINT;
            bRet = TRUE;
            break;

        case CDDS_ITEMPOSTPAINT:

            // draw the item

            DrawTreeItem(bFocus, rcItem, pCustomDraw->nmcd.hdc, 
                         (HTREEITEM) pCustomDraw->nmcd.dwItemSpec);
            bRet = TRUE;
            break;
    }

    return bRet;
}

This function is very easy, I draw a focus rectangle and a focus background (if necessary). Then I draw the column texts in a for loop. The single rectangles are calculated from the rcItem parameter and the HeaderData structure. I use that way, because it would be slower if I get the rectangle and alignment from the header control each time I need it.

void CTreeListView::DrawTreeItem(bool bFocus, CRect rcItem, HDC hdc, HTREEITEM hItem)
{
    COLORREF colText = m_colText;

    // if the item has got the focus,

    // we have to draw a sorouinding rectangle and fill

    // a rect blue

    if(bFocus == true)
    {
        RECT rcFocus = rcItem;
        rcFocus.left = 1;
        ::DrawFocusRect(hdc, &rcFocus);

        ::FillRect(hdc, &rcItem, (HBRUSH)m_BackBrush.m_hObject);

        colText = m_colHilightText;
    }

    // always write text without background

    ::SetBkMode(hdc, TRANSPARENT);
    ::SetTextColor(hdc, colText);

    // draw all columns of the item

    RECT rc = rcItem;
    for(long i=0; i < m_nNrColumns; i++)
    {
        if(i != 0)
            rc.left = m_vsCol[i].rcDefault.left;
        rc.right = m_vsCol[i].rcDefault.right;
        CString str = m_Entries[hItem].m_strColumns[i];

        ::DrawText(hdc, str, -1, &rc, 
                   DT_BOTTOM | DT_SINGLELINE | 
                   DT_WORD_ELLIPSIS | m_vsCol[i].nAlingment);
    }
}

If an item is to be deleted, I have to delete the strings for its columns. They are stored in a CMap<...>, so I only have to delete the object in the map that is mapped with the item's handle.

BOOL CTreeListView::OnDeleteItem(LPNMHDR pNmhdr, LRESULT* pResult)
{
    UNUSED_PARAM(pResult);

    BOOL bRet = TRUE;

    NMTREEVIEW* pnmtv = (NMTREEVIEW*) pNmhdr;

    m_Entries.RemoveKey(pnmtv->itemOld.hItem);

    return bRet;
}

I post the WM_SIZE message, because I recalculate my HeaderData structure in that message handler. I don't post the message, because the header control must update itself before. This is a solution for the timing problem in this point.

BOOL CTreeListView::OnEndTrack(LPNMHDR pNmhdr, LRESULT* pResult)
{
    UNUSED_PARAM(pResult);
    UNUSED_PARAM(pNmhdr);

    // we need to post this message

    // so the header control can take the time to save 

    // the information of the new sizes

    // and we can then get it from the control

    PostMessage(WM_SIZE);
    return FALSE;
}

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