一步一步教你实现CTreeCtrl自绘 源代码下载兼送分。。

wojiushi3344 2012-04-16 12:40:54
加精
一步一步教你实现CTreeCtrl 自绘

源代码下载

blog:http://blog.csdn.net/wojiushi3344/article/details/7463942 最近因工作需求,需要自绘CTreeCtrl。由于原来从来没有自绘过,开始在网上搜索资料,查询(因此本文有些知识不可能不全面,或许还有更好的办法来实现,还请大家多多指教。)经过一段时间的编写,终于写好了。在此,感谢网友everbeing提供的实例参考。

先贴上效果图,如果觉得还不错,那就继续往下看吧。如果觉得不行的,请飘过。




如何你看见这句话我会很高兴,因为至少我写的东西对你还是有一点点的吸引了。在此谢过。
很好,那现在让我们来说说为什么要自绘CTreeCtrl。我总结了以下2点需要自绘的情况。
1.当系统自带的树形控件已不满足我们的要求时,我们需要自绘。就像上图一样我们需要在后面显示我们额外的图标。
2.当你是一个追求界面美观的人时,我们需要自绘

我们需要自绘CTreeCtrl控件,我们就必须先了解一下自绘的方法,
CTreeCtrl自绘有2种方法可以实现。

第一种:通过从写NM_CUSTORMDRAW反射消息实现自绘。
第二种 通过重写 ON_PAINT实现自绘。

两中方法的实现原理:
第一种:通过从写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;



很好,现在你已经知道了绘制的基本方法了,那么接下来你就可以加上你自己的绘制了。

大致思路如下。

获取当前绘制节点的信息。如:节点状态,节点区域,节点文字等信息



------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------如过你讨厌记住以上的这么多数据,那么恭喜你,你可以继续网下看,下面介绍另一种实现自绘的方法通过重写ON_PAINT消息来实现自绘,也是我最终实现自绘的方法。因为我发现通过上面的方法来实现自绘解决不了我的问题。闪烁和热点,也许是我自己的能力有限。
原理:

我们获取树形控件的数据结构和DC,然后我们自己来定义绘制的规则。这里就可以发挥你的DIY兴趣了,想怎么画就怎么画。

很好很好,下面进入另一种方法的介绍

首先我们要明白ON_PAINT消息在什么情况下触发,在win32程序中,当窗口需要重绘的时候会触发ON_PAINT消息,还有一种情况就是我们自己手动触发,手动触发的消息有两种,第一种是调用窗口无效函数Invalidate(FALSE),第2种是手动发送wm_paint消息。

明白了这个我们才知道我们以后需要窗口重绘的时候怎么处理,这里说点题外话,原来在做扫雷游戏的时候,鼠标单击之后要过20多毫秒才有反应,后来查了半天原因才发现是带用重绘的问题,因为我开始单机之后没有立即调用重绘,而是没隔50毫秒调用一次。

我们通过重写ON_PAINT消息就能获取当当前窗口的DC,在这里我们就是整个树形控件的DC,不向上面那种方法一样只获取到树形控件某节点的DC,获取整个DC要比上面那种方法操作方便一些。

void CMyCtreeCtrl::OnPaint()



CPaintDC dc(this); //这句就是获取绘制的DC

现在有了DC我们就可以绘制了,在这里我们为了让它绘制的时候不闪烁我们决定用双缓冲,于是乎我们有了下面的代码。

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);

//dc.BitBlt( m_ClientRect.left, m_ClientRect.top, m_ClientRect.Width(), m_ClientRect.Height(), &MemeDc, 0, 0, SRCAND);
dc.BitBlt( m_ClientRect.left, m_ClientRect.top, m_ClientRect.Width(), m_ClientRect.Height(), &MemeDc, 0, 0,SRCCOPY);
//dc.BitBlt( m_rect.left, m_rect.top, m_rect.Width(), m_rect.Height(), &MemeDc, 0, 0, SRCCOPY);

MemeDc.SelectObject(pOldBitmap);
MemeDc.DeleteDC();



在这里我们基本的东西已经具备了现在我们就差数据了,首先我们要实现我们最开始的那种图,我们需要定义一个结构体来存储这些数据。

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; 这是为了我们以后的绘制和判断热点用。

有了数据结构,我们现在就可以插入数据使之成为一颗拥有节点的数,这个插入我们也需要自己重写,因为插入的数据是我们自己定义的。

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 );//获取数的类型
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.DrawImage(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;

}

//绘制文字
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消息。

我们首先定义一个数据来记录当前鼠标移动到的节点的句柄。具体看代码。

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);

}

这2句代码都在DrawItem函数中。
这种重写ON_PAINT消息实现自绘的方法就是这些了,有没有觉得比我们前面的那种方法要简单好理解一些了。下面说说我为什么选择这种方法而不用上面一种方法的原因,首先ON_PAINT可以获取到树形控件的整个DC,感觉绘制的时候方便一些,我想绘制到什么地方就绘制到什么地方,容易控制。其次是这种方法需要掌握的数据结构比较的少就需要知道几个常见的函数就OK了。


...全文
6415 196 打赏 收藏 转发到动态 举报
写回复
用AI写文章
196 条回复
切换为时间正序
请发表友善的回复…
发表回复
悠悠球霸 2014-04-01
  • 打赏
  • 举报
回复
怎么不自绘QQ的好友列表
asdfe121 2014-04-01
  • 打赏
  • 举报
回复
牛X good
shuzhongxunyu 2014-03-30
  • 打赏
  • 举报
回复
您好,请问这个源码下载下来怎么运行不了啊
alexmayer 2014-03-10
  • 打赏
  • 举报
回复
感谢愿意分享的朋友!
zou170016 2014-02-26
  • 打赏
  • 举报
回复
好东西,谢谢楼主分享~
HELLO大家拿 2013-12-28
  • 打赏
  • 举报
回复
非常感谢你的代码。很经典的例子太多了
qw410wm 2013-12-07
  • 打赏
  • 举报
回复
感谢楼主分享,好好学习下
随风随恨 2013-04-19
  • 打赏
  • 举报
回复
果然大牛啊,强大!
chenzhao064 2013-04-08
  • 打赏
  • 举报
回复
效果不错!顶楼主了
huangjujiu 2013-04-02
  • 打赏
  • 举报
回复
谢谢楼主,好教程
lgbgoingdown 2013-03-07
  • 打赏
  • 举报
回复
正愁ctreeCtrl的自绘呢,太好了
「已注销」 2012-12-08
  • 打赏
  • 举报
回复
楼主啊,用的过程中发现一个问题,就是将继承的TreeCtrl的border属性修改为false结果就显示不出来了!这是什么原因啊?求解,我是一个新菜鸟,所有也不是很明白为什么!不过这仍然给我很大的帮助,再次感谢。
Luo_Bryant 2012-12-05
  • 打赏
  • 举报
回复
运行不了,我用的是vc 6.0,解压之后怎么没有.dsw的文件的啊??这样的话应该打开那个文件来运行的??菜鸟求教ing。。。。急。。。
pchong2000 2012-12-05
  • 打赏
  • 举报
回复
正用得上,很好的参考啊。
redeyerabbit 2012-12-03
  • 打赏
  • 举报
回复
感谢楼主,不错,看着挺顺的,不知道有没有word格式的,打印下来,收藏了。
lanling2008 2012-11-28
  • 打赏
  • 举报
回复
哈哈,把路径改变了,图片就找不到了呗:) 建议把图片导入比较好
lanling2008 2012-11-28
  • 打赏
  • 举报
回复
void CCustomCTreeCtrlDlg::OnBnClickedButton1() { 。。。。。。 Sleep(20); m_tree.FlashTree(); ////////////////////////////////只要调用本段代码列表就异常!!!!!!//////////////////////////////////////// TCHAR szFilePath[MAX_PATH + 1]; GetModuleFileName(NULL, szFilePath, MAX_PATH); (_tcsrchr(szFilePath, _T('\\')))[1] = 0; CString strtemp=szFilePath; ::SetCurrentDirectory(strtemp); }
NachoZhong 2012-09-24
  • 打赏
  • 举报
回复
学习学习
giordano000 2012-09-06
  • 打赏
  • 举报
回复
感谢分享!呵呵
midongwen 2012-08-11
  • 打赏
  • 举报
回复
很好的,东西!
加载更多回复(131)

15,979

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC 界面
社区管理员
  • 界面
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧