最近因工作需求,需要自绘CTreeCtrl。由于原来从来没有自绘过,开始在网上搜索资料,查询(因此本文有些知识可能不全面,或许还有更好的办法来实现,还请大家多多指教。)经过一段时间的编写,终于写好了。在此,感谢网友bunpkin提供的实例参考。
先贴上效果图,如果觉得还不错,那就继续往下看吧。如果觉得不行的,请飘过。
如何你看见这句话我会很高兴,因为至少我写的东西对你还是有一点点的吸引了。在此谢过!
很好,那现在让我们来说说为什么要自绘CTreeCtrl。我总结了以下2点需要自绘的情况。
1.当系统自带的树形控件已不满足我们的要求时,我们需要自绘。就像上图一样我们需要在后面显示我们额外的图标。
2.当你是一个追求界面美观的人时,我们需要自绘
我们需要自绘CTreeCtrl控件,我们就必须先了解一下自绘的方法,
CTreeCtrl自绘有2种方法可以实现。
第一种:通过从写NM_CUSTORMDRAW反射消息实现自绘。
第二种: 通过重写ON_PAINT实现自绘。
二种方法都是通过继承CTreeCtrl类,然后重写虚函数实现。
下面分别介绍每一种的方法:
第一种:通过从写NM_CUSTORMDRAW反射消息实现自绘。从这个消息的英文单词我们翻译过来就是自定义绘制。当CTreeCtrl控件需要绘制就会触发这个消息。需要注意的是这个函数被调用的时候只是绘制了当前的某一个节点,意思就是当我们的CTreeCtrl有10个节点需要绘制的时候这个函数就需要调用10次。
这个是函数原型
void CMyTreeCtrl::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult)
这个函数会给我们传入一个pNMHDR指针,这个指针有我们很关心的数据,如当前的HDC,RECT,和当前的节点信息,但是必须要通过转换。下面是转换语句。
NMTVCUSTOMDRAW *ptvTreeCtrl=(NMTVCUSTOMDRAW *)pNMHDR
可能有的朋友会问为什么需要类型转换了,这是由于在我们的程序中收到NM_CUSTORMDRAW消息的不止CTreeCtrl一个,其它的控件也能收到,这里我们我为了区分是哪个控件收到的消息所以我们需要对应的类型转换。下面是常见控件的类型转换类型。
Control
Structure
List view
NMLVCUSTOMDRAW
ToolTips
NMTTCUSTOMDRAW
Tree view
NMTVCUSTOMDRAW
All other supported controls
NMCUSTOMDRAW
很明显我们根据上面的图一眼就能看出CTreeCtrl对应的类型是NMTVCUSTOMDRAW。
下面我们在来看看我们最关心的NMTVCUSTOMDRAW 结构里面存的是什么数据。
NMTVCUSTOMDRAW结构定义:
typedef struct tagNMTVCUSTOMDRAW {
NMCUSTOMDRAW nmcd;//包含控件的基本信息(见下表)
COLORREF clrText;//节点的文本颜色
COLORREF clrTextBk;//文本背景色
}NMTVCUSTOMDRAW, *LPNMTVCUSTOMDRAW;
NMCUSTOMDRAW结构定义:
typedef struct tagNMCUSTOMDRAWINFO {
NMHDR hdr;//跟pNMHDR一样,我基本没用到
DWORD dwDrawStage;//绘画段,某项被檫出前,后,绘制前,后
HDC hdc;//控件的设备上下文句柄
RECT rc;//要绘制的区域
DWORD dwItemSpec;//树控件不需要这个变量
UINT uItemState;//项的状态,只要是点击选中
LPARAM lItemlParam //项关联的数据,通过SetItemData函数设置的。
}NMCUSTOMDRAW, FAR* LPNMCUSTOMDRAW;
uItemState项的状态(来自MSDN)
Specifies the current item state. It can be a combination of the following values.
Value Description
CDIS_CHECKED The item is checked. 项被核记了
CDIS_DEFAULT The item is in its default state. 默认状态
CDIS_DISABLED The item is disabled. 项被禁止了
CDIS_FOCUS The item is in focus. 项具有焦点
CDIS_GRAYED The item is grayed. 项为灰颜色,
CDIS_HOT The item is currently under the pointer (hot). 鼠标当前停留在这个项上
CDIS_SELECTED The item is selected. 项被选中了
以上就是我们自绘需要知道的数据结构,如果你了解这些数据结构所代表的意思,那下面我们就可以开始绘制了。
绘制方法
NMCUSTOMDRAW消息自绘,使你可以决定在什么绘画端(就是NMCUSTOMDRAW 中的DWORD dwDrawStage)来绘制,比较常用的是在绘制前的阶段来绘制,如果你只是用了这种方法来绘制画,那么恭喜你选择对了一半,但是绘制失败了,因为你将什么也看不见。不急,让我们慢慢给你说明原因,因为你在绘制前的阶段绘制了,紧接这系统还会调用一次默认绘制,那么你原来的绘制就被覆盖了。
正确的方法是在绘制前绘制,然后过滤点系统的默认绘制,使之不在调用。这样我们所绘制就能看见了。于是乎在我们的OnNMCustomdraw函数中多了一下几句代码。(过滤系统的默认绘制)
if (lpnmcd ->nmcd.dwDrawStage == CDDS_PREPAINT)
{
*pResult = CDRF_NOTIFYITEMDRAW;
return;
}
else if (lpnmcd->nmcd.dwDrawStage == CDDS_ITEMPREPAINT)
{
//自定义绘制
*pResult = CDRF_DODEFAULT;
return;
}
很好,现在你已经知道了绘制的基本方法了,那么接下来你就可以加上你自己的绘制了。
大致思路如下。
获取当前绘制节点的信息。如:节点状态,节点区域,节点文字等信息
需要掌握的函数:
CTreeCtrl::GetItemRect
BOOL GetItemRect( HTREEITEM hItem, LPRECT lpRect, BOOL bTextOnly );
返回值:
如果项是可视的则返回非零值,以及包含在lpRect中的边界矩形。否则,返回0和没有被初始化的lpRect。
参数:
hItem 一个tree view项的句柄。
lpRect 指向一个用来接收边界矩形的RECT结构的指针。其中的坐标是相对于该tree view控件的左上角的。
bTextOnly 如果这个参数是非零值,则边界矩形值包括项的文本。否则,它包括该项在tree view控件所占据的整个一行。
CTreeCtrl::GetItemText
CString GetItemText( HTREEITEM hItem ) const;
返回值:返回一个包含该项的文本的CString对象。
参数:
hItem 要获取其文本的项的句柄。
添加自己的绘制函数,如绘制图片,绘制文字等。
CTreeCtrl::ItemHasChildren
BOOL ItemHasChildren( HTREEITEM hItem );
返回值:
如果由hItem指定的tree项有子项则返回非零值;否则返回0。
参数:
hItem 一个tree项的句柄。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
如过你讨厌记住以上的这么多数据,那么恭喜你,你可以继续网下看,下面介绍另一种实现自绘的方法.通过重写ON_PAINT消息来实现自绘,也是我最终实现自绘的方法。因为我发现通过上面的方法来实现自绘解决不了我的问题。闪烁和热点,也许是我自己的能力有限。大家可以分享一下你们是如何解决我遇到的问题的。
原理:
我们获取树形控件的数据结构和DC,然后我们自己来定义绘制的规则。这里就可以发挥你的DIY兴趣了,想怎么画就怎么画。
很好,下面让我们进入另一种方法的介绍
首先我们要明白ON_PAINT消息在什么情况下触发,在win32程序中,当窗口需要重绘的时候会触发ON_PAINT消息,还有一种情况就是我们自己手动触发,手动触发的消息有两种,第一种是调用窗口无效函数Invalidate(FALSE),第2种是手动发送wm_paint消息。
明白了这个我们才知道我们以后需要窗口重绘的时候怎么处理,这里说点题外话,原来在做扫雷游戏的时候,鼠标单击之后要过20多毫秒才有反应,后来查了半天原因才发现是带用重绘的问题,因为我开始单机之后没有立即调用重绘,而是没隔50毫秒调用一次。
我们通过重写ON_PAINT消息就能获取当前窗口的DC,在这里我们就是整个树形控件的DC,不向上面那种方法一样只获取到树形控件某节点的DC,获取整个DC要比上面那种方法操作方便一些。
下面是onpaint方法:
void CMyCtreeCtrl::OnPaint()
{
CPaintDC dc(this); //这句就是获取绘制的DC
现在有了DC我们就可以绘制了,在这里我们为了让它绘制的时候不闪烁我们用双缓冲,于是乎我们有了下面的代码。
[cpp] view plaincopy
CPaintDC dc(this); // device context for painting
GetClientRect(&m_ClientRect);
CBitmap bitmap;
CDC MemeDc;
MemeDc.CreateCompatibleDC(&dc);
bitmap.CreateCompatibleBitmap(&dc, m_ClientRect.Width(), m_ClientRect.Height());
CBitmap *pOldBitmap = MemeDc.SelectObject(&bitmap);
DrawBack(&MemeDc);
DrawItem(&MemeDc);
[cpp] view plaincopy
dc.BitBlt( m_ClientRect.left, m_ClientRect.top, m_ClientRect.Width(), m_ClientRect.Height(), &MemeDc, 0, 0,SRCCOPY);
MemeDc.SelectObject(pOldBitmap);
MemeDc.DeleteDC();
[cpp] view plaincopy
可以看见我们在函数中首先绘制了背景然后再背景上绘制了控件。具体的函数实现请看下面的介绍。
在这里我们基本的东西已经具备了现在我们就差数据了,首先我们要实现我们最开始的那种效果,我们需要定义一个结构体来存储这些数据。
struct TREE_STRUCT
{
int s_FirstImage; //第一张图片的信息
int s_SecondImage; //第二张图片的信息
int s_ThreeImage; //第三张图片的信息
int s_FourImage; //第四张图片的信息
COLORREF s_TextColor; //文字的颜色
int s_PeopleNum; //人的数目
CString s_ItemStr; //每一项的文字
CString s_StrUrl; //每一项对应的URL地址
}
我们在定义一个map
map <HTREEITEM,TREE_STRUCT> m_mapTree; 这是为了我们以后的绘制和判断热点用。
有了数据结构,我们现在就可以插入数据使之成为一颗拥有节点的数,这个插入我们也需要自己重写,因为插入的数据是我们自己定义的。
[cpp] view plaincopy
HTREEITEM CMyCtreeCtrl::InsertItemEx(TREE_STRUCT pStruct,HTREEITEM lparent,HTREEITEM lpFont )//插入项
{
HTREEITEM tempTreeItem;
CString str;
str.Format("%s(%d人)",pStruct.s_ItemStr,pStruct.s_PeopleNum);
tempTreeItem = InsertItem(str,lparent,lpFont);
m_mapTree.insert(pair<HTREEITEM,TREE_STRUCT>(tempTreeItem,pStruct));
return tempTreeItem;
}
下面是绘制树形控件的具体实现
[cpp] view plaincopy
void CMyCtreeCtrl::DrawItem(CDC* pDc)
{
HTREEITEM currentItem,parentItem;//当前的句柄,和它的父节点的句柄
DWORD treeStyle;// 数的类型
CRect itemRect;//每一项的区域
int itemState;//某项的状态
//bool selected; //True:表示是需要高亮
ImageAttributes alphaAttribut;
alphaAttribut.SetColorKey(Color::Fuchsia,Color::Fuchsia);
treeStyle =:: GetWindowLong( m_hWnd, GWL_STYLE );
[cpp] view plaincopy
currentItem = GetFirstVisibleItem();//获取第一个课可见的项
do
{
if (GetItemRect(currentItem,itemRect,TRUE))
{
itemRect.left=itemRect.left-19;
CRect fillRect(0,itemRect.top,m_ClientRect.right,itemRect.bottom);
itemState = GetItemState(currentItem,TVIF_STATE);
if (itemRect.top>m_ClientRect.bottom) //说明这一项已超出窗口的边界,所以不绘制,很好理解吧!不用我说了撒
{
break;
}
//绘制鼠标热点
if (currentItem==m_MouseMoveItem&&ItemHasChildren(currentItem)==NULL)
{
m_Gdiplus.usFillRectangle(pDc->m_hDC,fillRect,0xB7F0FE,0xB7F0FE,edoVertical,true);
}
if(itemState&TVIS_SELECTED)
{
m_Gdiplus.usFillRectangle(pDc->m_hDC,fillRect,0xFF00BB,0xFF00BB,edoVertical,true);
}
//绘制展开图片
if (ItemHasChildren(currentItem))
{
CPoint point;
point.x = itemRect.left;
point.y = itemRect.top+(itemRect.Height()-m_OpenHigh)/2;
if (itemState & TVIS_EXPANDED)
{
m_IconList.Draw(pDc,1,point,ILD_TRANSPARENT);
}else
{
m_IconList.Draw(pDc,0,point,ILD_TRANSPARENT);
}
}
itemRect.left+=m_OpenWidth+2;
itemRect.right+=m_OpenWidth+8;
//绘制图标1
m_iter=m_mapTree.find(currentItem);
Graphics tempGraphics(pDc->m_hDC);
Rect rcDes;
if (m_iter->second.s_FirstImage>=0&&m_iter->second.s_FirstImage<m_IconNum)
{
rcDes=Rect(itemRect.left,fillRect.top+m_IconSpacing,m_IconWidt,m_IconHigh);
tempGraphics.DrawImag(m_IconBitmap,rcDes,m_iter>second.s_FirstImage*m_IconWidt,0,m_IconWidt,m_IconHigh,UnitPixel,&alphaAttribut);
itemRect.left =itemRect.left+m_IconWidt;
itemRect.right = itemRect.right+m_IconWidt;
}
[cpp] view plaincopy
//绘制文字
DrawItemText(pDc,currentItem,itemRect);
//绘制后面的第2,3,4项图标
CSize fontSize;
fontSize= pDc->GetTextExtent(GetItemText(currentItem));
itemRect.left+=fontSize.cx;
if (m_iter->second.s_SecondImage>=0&&m_iter->second.s_SecondImage<m_IconNum)
{
rcDes=Rect(itemRect.left,fillRect.top+m_IconSpacing,m_IconWidt,m_IconHigh);
tempGraphics.DrawImage(m_IconBitmap,rcDes,m_iter->second.s_SecondImage*m_IconWidt,0,m_IconWidt,m_IconHigh,UnitPixel,&alphaAttribut);
itemRect.left=itemRect.left+m_IconWidt+4;
}
}
} while ((currentItem=GetNextVisibleItem(currentItem)) != NULL);
}
函数的主要思路首先通过GetFirstVisibleItem();获取第一个可见的节点,如果节点存在则按照我们的绘制顺序绘制,然后调用GetNextVisibleItem获得下一个可见的节点,一直循环,直到下一个可见节点为NULL的时候退出绘制函数。
下面讲一下获取项热点的方法。同样我们需要重写消息,这次是ON_MOUSEMOVE消息。
我们首先定义一个数据来记录当前鼠标移动到的节点的句柄。具体看代码。
[cpp] view plaincopy
HTREEITEM m_MouseMoveItem; //鼠标移动到的项
void CMyCtreeCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_ptOldMouse = point;
HTREEITEM hItem = HitTest(point);
if ( hItem != NULL && hItem != m_MouseMoveItem )
{
m_MouseMoveItem = hItem;
Invalidate(FALSE);
}
//CTreeCtrl::OnMouseMove(nFlags, point);
}
接下来我们通过获取当前绘制的节点和m_MouseMoveItem比对,如果相同则设置当前的背景颜色,从而实现热点时间
//绘制鼠标热点
if (currentItem==m_MouseMoveItem&&ItemHasChildren(currentItem)==NULL)
{
m_Gdiplus.usFillRectangle(pDc->m_hDC,fillRect,0xB7F0FE,0xB7F0FE,edoVertical,true);
}
//通过获取当前状态来判断单击事件。
if(itemState&TVIS_SELECTED)
{
m_Gdiplus.usFillRectangle(pDc->m_hDC,fillRect,0xFF00BB,0xFF00BB,edoVertical,true);
}
<p>这2句代码都在DrawItem函数中。</p>
之后在我们的一些常见操作之后触发WN_PAINT就可以了。
这种重写ON_PAINT消息实现自绘的方法就是这些了,有没有觉得比我们前面的那种方法要简单好理解一些了。下面说说我为什么选择这种方法而不用上面一种方法的原因,首先ON_PAINT可以获取到树形控件的整个DC,感觉绘制的时候方便一些,我想绘制到什么地方就绘制到什么地方,容易控制。其次是这种方法需要掌握的数据结构比较的少就需要知道几个常见的函数就OK了。
需要掌握的函数:
CTreeCtrl::GetFirstVisibleItem
HTREEITEM GetFirstVisibleItem( );
返回值:如果成功则返回第一个可视项的句柄;否则返回NULL。
说明:
此成员函数用来获取该tree view控件中的第一个可视项的句柄。
CTreeCtrl::GetNextVisibleItem
HTREEITEM GetNextVisibleItem( HTREEITEM hItem );
返回值:返回下一个可视项的句柄;否则返回NULL。
参数:
hItem 一个tree项的句柄。
说明:此成员函数用来获取hItem的下一个可视项。
CTreeCtrl::ItemHasChildren
BOOL ItemHasChildren( HTREEITEM hItem );
返回值:
如果由hItem指定的tree项有子项则返回非零值;否则返回0。
参数:
hItem 一个tree项的句柄。
CTreeCtrl::GetItemText
CString GetItemText( HTREEITEM hItem ) const;
返回值:返回一个包含该项的文本的CString对象。
参数:
hItem 要获取其文本的项的句柄。
说明:
此成员函数返回由hItem指定的项的文本。
CTreeCtrl::GetItemRect
BOOL GetItemRect( HTREEITEM hItem, LPRECT lpRect, BOOL bTextOnly );
返回值:
如果项是可视的则返回非零值,以及包含在lpRect中的边界矩形。否则,返回0和没有被初始化的lpRect。
参数:
hItem 一个tree view项的句柄。
lpRect 指向一个用来接收边界矩形的RECT结构的指针。其中的坐标是相对于该tree view控件的左上角的。
bTextOnly 如果这个参数是非零值,则边界矩形值包括项的文本。否则,它包括该项在tree view控件所占据的整个一行。