Click here to Skip to main content
15,861,125 members
Articles / Desktop Programming / MFC

A Faster Tree Control

,
Rate me:
Please Sign up or sign in to vote.
4.69/5 (13 votes)
16 Dec 2003CPOL5 min read 389.8K   3.7K   82   97
An article about an open source and free fast tree control

Sample Image - RGTree.gif

Introduction

I'm writing an application for exploring the registry. Therefore, I need the tree control. First, I used InsertItem directly with the registry keys. Inserting an item in the SysTree is fast enough but deleting the items is very slow. I used optimizations described in Tibors article, but it wouldn't be much faster. Then I used text callback items, but the same.

For example: Adding about 50000 registry keys to system tree control needs about 6 sec, deleting these items needs about 22 sec (!) on my Thunderbird 1200.

Because each quick tree control I've found I must pay for and the free controls I've found aren't faster than the standard tree control, I decided to write my own. The tree control should work like the system tree control.

The Control Internally

The control data storage is based on the template CRGTreeT<_ITEMDATA>. It's an object implementing a bi-directional linked list of elements, each containing a member of type ITEMDATA.

C++
template<class _ITEMDATA> class CRGTreeT
{
 public:
   // Construction / Destruction
   CRGTreeT();
   ~CRGTreeT();

   // returns the count of all items
   UINT GetItemCount() const;
   // returns the count of all child items
   UINT GetChildCount( /*[in]*/ POSITION posParent) const;
   // returns the root
   POSITION GetRootPosition() const;

   // returns the parent of an item
   POSITION GetParentPosition( /*[in]*/ POSITION pos) const;

   // adds an items at the begin of the list
   POSITION AddHead( /*[in]*/ const _ITEMDATA *pItemData, 
                     /*[in]*/ POSITION posParent=NULL);
   // adds an items at the end of the list
   POSITION AddTail( /*[in]*/ const _ITEMDATA *pItemData, 
                     /*[in]*/ POSITION posParent=NULL);

   POSITION InsertAfter( /*[in]*/ const _ITEMDATA *pItemData, 
                         POSITION posParent=NULL, 
                         /*[in]*/POSITION posAfter=NULL);
   // Remove the item at position pos including all subitems
   void RemoveAt( /*[in]*/ POSITION pos);
   // Removes all items
   void RemoveAll();

   // returns the first/last child item of the item at position posParent
   POSITION GetChildPosition( /*[in]*/ POSITION posParent, 
                              /*[in]*/ BOOL bFirst=TRUE) const;
   // returns the next list element, NULL if none
   POSITION GetNextPosition( /*[in]*/ POSITION pos) const;

   // returns the previous element, NULL if none
   POSITION GetPrevPosition( /*[in]*/ POSITION pos) const    

   // returning a copy of the ITEMDATA at position pos
   void GetAt( /*[in]*/ POSITION pos, /*[out]*/ _ITEMDATA* pItemData) const;
   // Returning a pointer to the ITEMDATA at position pos
   _ITEMDATA* GetAt( /*[in]*/ POSITION pos);
   // replace the ITEMDATA at position pos
   void SetAt( /*[in]*/ POSITION pos, /*[in]*/ const _ITEMDATA* pItemData);

   // sort childrens of the item at position posParent
   BOOL SortChildren( /*[in]*/ POSITION posParent, 
                      /*[in]*/ BOOL bAscending=TRUE);
   // search for an item based on the compare function
   POSITION Find( /*[in]*/ const _ITEMDATA *pItemData, 
                  /*[in]*/ POSITION posParent);

   // setting a callback function for sorting and finding items
   CompareFunc SetCompareFunc( /*[in]*/ CompareFunc func);
   DeleteFunc SetDeleteFunc( /*[in]*/ DeleteFunc func, LPARAM lParam);

protected:
   // remove all childs from an item
   void RemoveChilds( POSITION pos);

   // quick sort algorithms for sorting up- or downwards
   void QSortA( /*[in]*/ CTreeItem **pLow, /*[in]*/ CTreeItem **pHigh);
   void QSortD( /*[in]*/ CTreeItem **pLow, /*[in]*/ CTreeItem **pHigh);

private:
   CTreeItem      m_ItemRoot;
   UINT           m_nCount;
   CompareFunc    m_pCompareFunc;
   DeleteFunc     m_pDeleteFunc;
   LPARAM         m_lParamDeleteFunc;
};

The messages will be processed in its own window procedure. I've implemented some small functions directly, the others exist as standalone functions with the following form:

C++
LRESULT On[MessageName]( HWND hwnd, TREE_MAP_STRUCT *ptms, 
                         WPARAM wParam, LPARAM lParam)

There is a data structure holding the characteristics for each tree HWND:

C++
struct TREE_MAP_STRUCT
{
   HWND         hwnd;                  // the handle of the control
   HWND         hWndEdit;              // the handle of the controls 
                                       //   edit window
   WNDPROC      pOldWndProc;           // the old window procedure
   int          nItemCount;            // all items inserted
   int          nMaxWidth;             // max width of the control
   int          nVScrollPos;           // vertical scroll position
   int          nHScrollPos;           // horizontal scroll position
   HTREEITEM    hItemFirstVisible;     // items ...
   HTREEITEM    hItemSelected; 
   HTREEITEM    hItemEditing;
   HTREEITEM    hItemClicked;
   HIMAGELIST   hNormalImageList;      // image list
   HIMAGELIST   hStateImageList;
   bool         bRedrawFlag;           // should the control be redrawed 
                                       //   on each inserting/erasing
   _rgTree      rgTree;                // the CRGTreeT template
};

For faster calculating of the item widths, Tibor has written class holding repeatable used objects, especially the HDC and the HFONTs. You can cache their creation by SetRedraw() calls.

Next optimization is used in the internal GetItemVisibleIndex(). With many items, it tries to find the items near the first visible one.

C++
class CItemRectCache
{
public:
   CItemRectCache(HWND hWnd);
   ~CItemRectCache();
   void SetItem( RGTVITEM* pItem);
   int GetItemTextWidth();
   SIZE GetStateImageSize(TREE_MAP_STRUCT *ptms);
   SIZE GetNormalImageSizeAndOffset(TREE_MAP_STRUCT *ptms);
protected:
   bool         m_bHasLinesAtRoot;
   bool         m_bStateImageSizeInitialized;
   SIZE         m_StateImageSize;
   bool         m_bNormalImageSizeInitialized;
   SIZE         m_NormalImageSize;
   HWND         m_hWnd;
   HDC          m_hDC;
   HGDIOBJ      m_hFont;
   HGDIOBJ      m_hBoldFont;
   HGDIOBJ      m_hOldFont;
   bool         m_bBoldSelected;
   RGTVITEM*    m_pItemSet;
}

Known To-dos and Limitations

  • Full(er) TreeCtrl functionality. At the moment, we have only implemented cases interesting for us. The main missing cases are:
    • Custom drawing
    • TVM_INSERTITEM with TVI_SORT
    • TVM_SETSCROLLTIME
    • Tooltip support
    • Infotips
    • Keyboard notification and incremental search
  • Slower sorting because the quicksort algorithm is used and an array of all items to sort must be built and after sorting the previous/next position of an item must be updated
  • More straight code without duplicities and not commented parts
  • More 1:1 implementation
    • Are cases when hItemFirstVisible after Expand call is not the same like in system tree
    • Edit control size and position depending on horizontal scrollbar position
  • More and more intelligent done user interface optimization
    • Can be sometimes more redraws than necessary
    • More searches can be optimized (see __GetItemVisibleIndex call in SetItem)
    • See bInSetScrollBars usage as horror example
  • Absolutely correct code
    • Now change of item's TVIS_BOLD state not recomputes horizontal scrollbar
    • Directly defined ID_EDIT can conflict with your dialog control id (?)
  • With SetRedraw(false) before InsertItem()/TVIF_TEXT system tree returns item's rectangle with 0-widthed text, rgtree with computed size. System tree needs explicit SetItemText() or SetRedraw(true) before GetItemRect(... , true).

The Demo Project

I've written a demo project comparing the speed of the CRGTreeCtrl with the standard tree control. It measures the time for inserting, sorting and deleting items.

At first, you must build RGTree than TreeDlg configuration.

We spent some time searching why the release version is slower than debug in DeleteAllItems(). You will not believe it was in the delete[] pItemData->pszText call. The funny thing is all is ok when you do not run it from developer studio (why did we not find it sooner?).

How to Use in Your Project

If you want to use it build (release) RGTree.dll. This will produce import library RGTree.lib. Include it into Project\Settings\Link\General\Object/Library modules. Derive your tree control from CRGTree.

CYourTreeCtrl.h

C++
#include "RGTreeCtrl.h"
class CYourTreeCtrl : public CRGTreeCtrl

change all CTreeCtrl mentions like...

C++
BEGIN_MESSAGE_MAP(CYourTreeCtrl, CTreeCtrl)

...to CRGTreeCtrl into CYourTreeCtrl.* and rebuild. Do not forget to call SetRedraw() before and after big tree data changes.

If you will create own project for tree sources, do not forget to define RGTREECTRL_EXPORTS there.

Finally

Generally, the control is usable. Especially, inserting/deleting many items is faster compared to the system one.

If you find corrections, improvements or add functionality, let us know to update this article. At first, we are interested in bugs (with defined situations) and absolutely the best (you are programmers) with possible solutions.

If you're interested in joining us and you don't have problems reading the sources, you're very welcome. Please contact us before your changes to avoid duplicate development.

Update History

  • InvalidateItemRect() calls GetItemRect() with FALSE
  • OnGetItem()/TVIF_TEXT fix
  • HitTest() knows TVHT_TOLEFT and similar flags
  • WM_MOUSEWHEEL handler
25 Jan 2002
  • InsertItem_UpdateVScrollPos()
  • HitTest() vs horizontal scroll
2 Mar 2002
  • SetTVITEMforNotify()
  • DoScroll()
  • GetNextItem()/TVGN_LASTVISIBLE
15 Apr 2002
  • Edit and scroll timers using GetDoubleClickTime()
  • TVS_FULLROWSELECT works without TVS_HASLINES only
  • Drag and drop support
  • IsBeforeFirstVisibleItem()
23 May 2002
  • TVE_COLLAPSE fix
7 Aug 2002
  • NM_RCLICK thanks to Doug Garno
  • I_IMAGECALLBACK thanks to Doug Garno
  • DeleteObject(hBrush) into OnPaint()
  • SetItem() with image change cases
12 Sep 2002
  • WM_CONTEXTMENU vs NM_RCLICK's return
  • Expand() in SetItem() without notify
  • GetNextItem(NULL) and similarly newly will return NULL and not crash
  • MessageBeep() for not handled events to OnKeyDown()
  • two TVGN_DROPHILITE items - second one for "pressed" state (but pressed rgtree still enables by-tab switching)
  • nmtv.itemNew corrected to nmtv.itemOld in TVN_DELETEITEM thanks to Doug Garno
  • (mask | TVIS) corrected to (mask & TVIS) in SetTVITEMforNotify() thanks to Doug Garno
13 Sep 2002
  • GetChildItem(NULL) returns root item and not NULL thanks to Doug Garno
9 Dec 2002
  • no WM_CHAR beeps in editing thanks to Doug Garno
  • EndEditLabelNow() sends one TVN_ENDLABELEDIT only thanks to Doug Garno
  • SetTVITEMforNotify() into OnEditLabel() uses lParam instead of ptms->hItemEditing thanks to Doug Garno
  • font handling made more straight
28 Mar 2003
  • better (but still not 1:1) MessageBeep() for not handled events to OnKeyDown() - see article comment
  • hBoldFont into OnPaint() at request only and derived from WM_GETFONT not hOldFont
17 Dec 2003
  • fixed scrolling problems when inserting items to zero-sized window (_ScrollAbsolutelyDownWhenRequired)
  • new cases into OnKeyDown() thanks to Doug Garno

Please search article comments for actual code updates too.

License

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


Written By
Software Developer (Senior)
Slovakia Slovakia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Written By
Software Developer
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: SetItem() is missing image change cases Pin
Anonymous7-Aug-02 6:07
Anonymous7-Aug-02 6:07 
GeneralRight click support via NM_RCLICK Pin
Doug Garno11-Jun-02 1:03
Doug Garno11-Jun-02 1:03 
GeneralRe: Right click support via NM_RCLICK Pin
Anonymous1-Aug-02 4:33
Anonymous1-Aug-02 4:33 
Generalgeneral question? Pin
Tibor Blazko6-Aug-02 20:42
Tibor Blazko6-Aug-02 20:42 
GeneralRe: general question? Pin
Anonymous7-Aug-02 6:04
Anonymous7-Aug-02 6:04 
GeneralRe: general question? Pin
Tibor Blazko7-Aug-02 20:15
Tibor Blazko7-Aug-02 20:15 
GeneralRe: general question? Pin
VJ8-Aug-02 7:02
VJ8-Aug-02 7:02 
GeneralQ222905 Pin
Tibor Blazko14-Aug-02 23:29
Tibor Blazko14-Aug-02 23:29 
not so easy, solution based on
Knowledge Base article Q222905 : HOWTO: Display a Context Menu for CTreeCtrl
t!

solution overview:
case WM_RBUTTONUP:

// Cancel the item pressed stuff.
if( ptms->hItemPressed != NULL )
CancelItemPressed( ptms );
SetFocus( hwnd );

{
POINT pt;
PT_FILL(pt, lParam);

if((abs((int)ptms->ptPressed.x - (int)pt.x) > GetSystemMetrics( SM_CXDRAG )
|| abs((int)ptms->ptPressed.y - (int)pt.y) > GetSystemMetrics( SM_CYDRAG )) )
{
SNDMSG(hwnd, WM_CONTEXTMENU, (WPARAM) hwnd, GetMessagePos());
}
else
{
TVHITTESTINFO tvhti;
PT_FILL( tvhti.pt, lParam );
HTREEITEM hItem = HitTest( ptms, &tvhti );

// Notify the parent of a RCLICK event via the WM_NOTIFY handler.
NMTREEVIEW nmTV = {0};
SetTVITEMforNotify( ptms, &nmTV.itemNew, hItem, TVIF_PARAM | TVIF_HANDLE );
if(PSNDNMSG( hwnd, nmTV, NM_RCLICK ) == 0)
{
PSNDMSG(hwnd, WM_CONTEXTMENU, (WPARAM) hwnd, GetMessagePos());
}
}
}

return 0;
GeneralLast version Pin
Wiktor Sadowski1-Jun-02 19:38
Wiktor Sadowski1-Jun-02 19:38 
GeneralRe: Last version Pin
Tibor Blazko13-Jun-02 22:25
Tibor Blazko13-Jun-02 22:25 
QuestionWho knows keyboard incremental search logic? Pin
Tibor Blazko14-Mar-02 0:19
Tibor Blazko14-Mar-02 0:19 
AnswerWho knows keyboard incremental search logic vs MessageBeep Pin
Tibor Blazko6-Feb-03 21:26
Tibor Blazko6-Feb-03 21:26 
GeneralScrolling doesn't work correctly Pin
Andreas28-Jan-02 4:20
Andreas28-Jan-02 4:20 
GeneralDoScroll() Pin
Tibor Blazko18-Feb-02 3:29
Tibor Blazko18-Feb-02 3:29 
GeneralA better way Pin
Neville Franks25-Jan-02 10:31
Neville Franks25-Jan-02 10:31 
GeneralRe: A better way Pin
Tibor Blazko11-Feb-02 1:48
Tibor Blazko11-Feb-02 1:48 
QuestionFill on demand ? Pin
25-Sep-01 2:51
suss25-Sep-01 2:51 
AnswerRe: Fill on demand ? Pin
Henk Devos13-Nov-01 2:21
Henk Devos13-Nov-01 2:21 
GeneralRe: Fill on demand ? Pin
Tibor Blazko13-Nov-01 4:40
Tibor Blazko13-Nov-01 4:40 
Generalfirst small update InvalidateItemRect() Pin
Tibor Blazko22-Aug-01 2:19
Tibor Blazko22-Aug-01 2:19 
Generalsecond update OnGetItem() Pin
Tibor Blazko23-Aug-01 6:29
Tibor Blazko23-Aug-01 6:29 
Generalthird update HitTest() Pin
Tibor Blazko9-Oct-01 22:13
Tibor Blazko9-Oct-01 22:13 
Generalfourth update WM_MOUSEWHEEL Pin
Tibor Blazko11-Oct-01 3:29
Tibor Blazko11-Oct-01 3:29 
Generalfifth update OnInsertItem() Pin
Tibor Blazko7-Nov-01 22:01
Tibor Blazko7-Nov-01 22:01 
Generalsixth update HitTest() Pin
Tibor Blazko14-Jan-02 1:39
Tibor Blazko14-Jan-02 1:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.