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;
if(m_ctrlTree.m_hWnd == pNmhdr->hwndFrom)
{
switch (pNmhdr->code)
{
case NM_CUSTOMDRAW:
return OnCustomdrawTree(pNmhdr, pResult);
case TVN_DELETEITEM:
return OnDeleteItem(pNmhdr, pResult);
}
}
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:
*pResult = CDRF_NOTIFYITEMDRAW;
::SetViewportOrgEx(pCustomDraw->nmcd.hdc, m_nOffset, 0, NULL);
bRet = TRUE;
break;
case CDDS_ITEMPREPAINT:
pCustomDraw->clrText = m_colBackColor;
pCustomDraw->clrTextBk = m_colBackColor;
bFocus = false;
if( pCustomDraw->nmcd.uItemState & CDIS_FOCUS)
{
bFocus = true;
}
pCustomDraw->nmcd.uItemState &= ~CDIS_FOCUS;
m_ctrlTree.GetItemRect((HTREEITEM) pCustomDraw->nmcd.dwItemSpec,
&rcItem, TRUE);
rcItem.right =
(pCustomDraw->nmcd.rc.right > m_nHeaderWidth) ?
pCustomDraw->nmcd.rc.right : m_nHeaderWidth;
*pResult = CDRF_NOTIFYPOSTPAINT;
bRet = TRUE;
break;
case CDDS_ITEMPOSTPAINT:
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(bFocus == true)
{
RECT rcFocus = rcItem;
rcFocus.left = 1;
::DrawFocusRect(hdc, &rcFocus);
::FillRect(hdc, &rcItem, (HBRUSH)m_BackBrush.m_hObject);
colText = m_colHilightText;
}
::SetBkMode(hdc, TRANSPARENT);
::SetTextColor(hdc, colText);
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);
PostMessage(WM_SIZE);
return FALSE;
}