自定义绘制的 TreeList 控件





5.00/5 (9投票s)
2000年2月21日

410487

13604
一个自定义绘制的树形列表混合控件,并解释了该控件的开发过程。
引言
此树形列表控件是一个所有者绘制的树形控件。我知道在 CodeProject 或 CodeGuru 上有很多树形列表控件,但我找不到一个符合我需求的。它们大多数重写了 WM_PAINT
消息。这很简单,但(在我测试中)当你有许多树项(大约 500 个及以上)时会有点慢。
David Lantsman 的文章给了我很大的帮助,Garen Hartunian 关于自定义绘制树形控件的文章也一样。
我将我找到的关于树形列表控件和自定义绘制树形控件的所有知识混合在一起,并创建了一个新类:CTreeListView
。你可以像使用 CTreeView
一样使用这个类,并像在 CListView
中一样操作列。我从这些类中获取了函数定义,使其在很大程度上兼容 MFC 类。但现在,闲话就到这里。
如何在项目中 CTreeListView
只需将该类添加到你的项目中,并将你的视图从 CTreeView
继承为 CTreeListView
。现在,将所有指向你之前基类(可能是 CTreeView
)的调用替换为 CTreeListView
。就是这样。现在,你可以开始像下面在 OnInitialUpdate
中那样参数化该类了。你会看到我插入了列和图片,以及项和子项。这几乎与 CTreeView
和 CListView
中的一样。函数在下面有描述(查看函数)。之后,你会找到类的概述(类概述,完整文档可以在这里找到。 下载类参考 - 27.6 Kb)以及绘制函数实现的描述(查看实现)。
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")); } }
CTreeListView 公共函数说明
int InsertColumn( int nCol, const LVCOLUMN* pColumn )
在
CTreeListView
中插入一列。你必须像在CListView
中一样填充结构。int InsertColumn( int nCol, LPCTSTR lpszColumnHeading, int nFormat = LVCFMT_LEFT, int nWidth = -1, int nSubItem = -1 )
在
CTreeListView
中插入一列。你必须像在CListView
中一样填充参数。BOOL DeleteColumn( int nCol )
删除指定的列以及为该列指定的所有文本。
BOOL SetSubItemText( HTREEITEM hItem, int nSubItem, CString strBuffer)
设置指定的树控件项(
hItem
)和指定的列(nSubItem
)的文本。nSubItem = 0
表示第一列(主名称)。BOOL GetSubItemText( HTREEITEM hItem, int nSubItem, CString& strBuffer)
返回指定的树控件项(
hItem
)和指定的列(nSubItem
)的文本。nSubItem = 0
表示第一列(主名称)。你将在strBuffer
中收到文本。CImageList* GetHeaderImageList()
检索由标题使用的图像列表。
CImageList* SetHeaderImageList( CImageList * pImageList)
设置标题可以用来显示图像的图像列表。
CTreeCtrl& GetTreeCtrl()
此函数返回对使用的树控件的引用。你可以使用此引用来删除或插入项。不要使用此函数更改项文本,因为文本是以子项文本的形式保存的,你只会看到它们。
void ShowHeader(bool bShow)
此函数隐藏和显示标题(列标题)。你可以通过
bShow
参数控制(true
表示显示标题,false
表示隐藏标题)。
这里是我使用的类和结构
struct _HeaderData
包含一列的数据,以便我在绘制时可以快速获取,而无需询问标题控件。
class CMyTreeObj : public CObject
表示树中的一个对象及其所有列。
class CMyTreeCtrl : public CTreeCtrl
一个重写的树控件,使我能够识别何时将项插入树中。该类发送一个用户定义的带有
HTREEITEM
参数的消息。class CTreeListView : public CView
包含树形列表的主视图。我没有继承自
CTreeView
,因为CTreeView
会子类化CTreeCtrl
,而且我找不到如何调整树的大小以在其顶部放置一个标题控件。但这种方式也可以。
实现
树形项的绘制
要使用树控件中的所有者绘制,你可以重写控件父类的 WM_NOTIFY
消息处理程序。在那里,你可以测试通知代码是否为 NM_CUSTOMDRAW
。我重写了 TVN_DELETEITEM
以删除每个项的列文本的行。我还为标题控件重写了 HDN_ENDTRACK
,以便更新用于绘制的标题数据的内部结构。
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); }
这个函数有点棘手。
我使用静态变量来保存一个对象的 CDDS_ITEMPREPAINT
到 CDDS_ITEMPOSTPAINT
通知之间的数据。CDDS_ITEMPOSTPAINT
通知紧随该对象的绘制之后。在另一个对象之前不会有 CDDS_ITEMPREPAINT
。但大多数数据仅通过 CDDS_ITEMPREPAINT
通知发送,所以必须保存它。在绘制(CDDS_PREPAINT
通知)之前,我必须在水平滚动条的情况下设置视口,我使用了水平滚动条。我的用法描述在 David Lantsman 的文章中。我以同样的方式使用它。
在 CDDS_ITEMPREPAINT
通知时,我将前景色和背景色设置为相同的值,这样你就看不到树的绘制。我也可以通过不提供文本来做到这一点,但那样的话我将不得不自己实现很多东西(例如通过按键跳转到某个项)。我想尽可能多地使用原始的树。
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; }
这个函数非常简单,我绘制一个焦点矩形和一个焦点背景(如果需要)。然后我在一个 for
循环中绘制列文本。单个矩形是从 rcItem
参数和 HeaderData
结构计算出来的。我这样做,是因为如果每次需要时都从标题控件获取矩形和对齐方式,效率会更低。
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); } }
当要删除一个项时,我必须删除其列的字符串。它们存储在一个 CMap<...>
中,所以我只需要删除与项句柄映射的对象。
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;
}
我发送 WM_SIZE
消息,因为我在该消息处理程序中重新计算我的 HeaderData
结构。我不直接发送消息,因为标题控件必须先自行更新。这是解决此时时间问题的方案。
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; }