微软4.70 版本的common controls 提供了一个叫custom draw的特性。这个名字给了你一个模糊的
提示关于custom draw是干什么。MSDN文档给了冗长的解释和例子,但是它没告诉你你想要的东西
。也就是,简单的,custom draw的好处在哪。Custom draw 可以被看成是一种轻量级的,容易使
用的Owner draw.容易使用的原因是因为:custom draw 只有一个消息(NM_CUSTOMDRAW)需要处理,
你可以让Windows Os 为你做一些工作。所以你没必要把部分owner-drawing的所有工作全部去做一
遍。
文章会把注意力放在列表控件上的custom draw.不尽因为我已经在工作中做了部分 custom draw
列表控件,我熟悉这个流程;而且可以用少量代码来取得不错的效果。Custom draw 的代码能取代
一些The Code Porject 上的老的关于Custom Draw的列表控件文章。
这篇文章的代码能够在装有 Microsoft Visual C++ 6 SP2,common control版本为version 5.0的
Windows 98上运行良好。我也在Unicode on NT 4上运行过改代码,但这是最少需要4.71的common
control.尽管这样,因为IE4(标榜着VC6),所以,这个不是问题。
Custom draw 基础
我尽可能的概述一下这里的custom draw 流程,不会再重申这些文档了。这些例子,都是假设我们
已经有了一个列表控件,这个控件放在对话框上,而且是报表模式,含有很多列。
填写custom draw消息映射项
Custom draw 和回调差不多。Windows在绘制列表控件过程的某个时间通过发送消息来通知你的程
序。你可以选择全部忽略这些消息(这样的话,你将看到标准的控件),可以选择一部分进行绘制
(一些简单的例子)或者干脆自己把控件全部画了。你可以只画部分你需要的,而让window去做其
它剩余的。
假设你想往一个控件中加一些闪光。假设你已经有了合适的common controls dll,当Windows已经
发送 NM_CUSTOMDRAW 消息给你,你只要加上以下消息处理函数就可以利用custom draw了。处理函
数会像这样子:
ON_NOTIFY ( NM_CUSTOMDRAW, IDC_MY_LIST, OnCustomdrawMyList )
原型会是像这样:
afx_msg void OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult );
这个告诉MFC,你想处理由IDC_MY_LIST发送通知代码是NM_CUSTOMDRAW的WM_NOTIFY消息。处理函数
名字为OnCustomDrawMylist.如果你为一个派生clistctrl增加custom draw功能,你可以用
ON_NOTIFY_REFLECT instead:
ON_NOTIFY_REFLECT ( NM_CUSTOMDRAW, OnCustomdraw )
这个消息处理函数和上面的具有相同的原型,但是用派生类代替。
Custom Draw Stages:
Custom Draw 可以分两个绘制过程:擦除和绘制。Windows 在每部分的的开始都会发送
NM_CUSTOMDRAW 消息。所以这里总共有4个消息。但是,根据你返回给Windows的值,你的应用程序
会收到1个或者比4个还多。Windows发送NM_CUSTOMDRAW的那个时间点叫做"draw stages".你需要很
好的理解这个概念,因为它会贯穿整个Custom draw.所以,总结一下,就是,你需要了解以下四个
阶段点:
1 列表项被绘制前阶段;
2 列表项被绘制后阶段;
3 列表项被擦除前阶段;
4 列表项被擦除后阶段;
并不是所有以上状态都有用。在实际中,我没有在多于两个的阶段中响应过消息。事实上,我在写
这篇文章前做过实验,Windows并不会在列表被擦除后和列表擦除前发送消息。所以,不要被这段
把你给吓着了。
响应 NM_CUSTOMDRAW 消息
你从custom draw 消息处理函数返回的值是一个至关重要的信息,它会告诉Widwows你已经在绘制
过程中做了多少事,所以也间接的告诉了Windows,它应该为你做些什么。你可以在你的消息处理函
数中返回5种值,他们是:
1 我不想做任何事;Windows 需要做所有事情来绘制这个控件或者是控件的项。这就好像你根
本没有参与Custom draw。
2 我改变被控件使用的字体;Windows需要重新计算被绘制项的面积。
3 我绘制整个控件或者列表项。Windows不要做任何事。
4 在列表项被绘制的过程中,我想接收另外的NM_CUSTOMDRAW消息
5 在列表项的子项(及列)被绘制时,我想接收另外的NM_CUSTOMDRAW消 息。
你发现没,“控件或者项”在这里经常出现.我说过,你可能会接收到4个或者更多的
NM_CUSTOMDRAW 消息。以上5项就是发生这个现象的原因。你收到的第一个NM_CUSTOMDRAW适用于怎
个控件。如果你响应以上第四个消息(在每个列表项中请求消息),随着每一项的绘制,你将会收
到很多消息。如果你响应第5个,随着子项的绘制,你会收到更加多的消息。
根据你想到达的效果不同,在报表模式的控件中,你可以使用任何那些响应消息。稍后,我会
展现一些例子来说明怎样响应NM_CUSTOMDRAW.
NM_CUSTOMDRAW 消息提供的信息
NM_CUSTOMDRAW 消息给你的消息处理函数传输了一个 NMLVCUSTOMDRAW 结构体的指针,它包含了以
下信息:
1 控件的窗口句柄;
2 控件的ID;
3 这个控件目前的绘制进度;
4 你可以用来绘制图像的设备上下文的句柄;
5 被绘制的控件,列表项,子项的面积(即RECT);
6 被绘制的列表子项的索引;
7 指示被绘制列表状态的标志位;
8 被绘制列表项的lLPARAM 参数,它是有setitemdata()函数设置的。
根据你想要的效果,以上信息的作用也会不同。但绘制阶段和设备上下文是你经常要用到的,项
索引和1lparam也非常有用。
一个简单的例子:
在陈述了一下枯燥的细节后,让我们看几段代码。第一个例子非常简单,我们会去改变列表里的文
字的颜色。这些颜色在红,绿,蓝三种颜色上切换。这个涉及到以下四个步骤:
1 在控件的绘制前这个阶段处理NM_CUSTOMDRAW消息;
2 告诉Windows我们想为每个列表项获得NM_CUSTOMDRAW消息;
3 处理随后的为每个列表项发送的NM_CUSTOMDRAW消息;
4 为每项设置文字的颜色。
看下处理函数:
void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
//默认处理除非我们把它设为其它值;
*pResult = CDRF_DODEFAULT;
//首先,检查绘制阶段。如果是控件绘制前的阶段,就告诉Windows我们希望接受到每个列表
//项的NM_CUSTOMDRAW 消息;
if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
// 这是每项绘制前的阶段。这里我们可以设置每项的文字颜色。我们的返回值告诉
//Windows自己绘制每一项;
// 但Windows会用我们在这里设置的颜色;颜色会在红,绿,蓝之间循环;
COLORREF crText;
if ( (pLVCD->nmcd.dwItemSpec % 3) == 0 )
crText = RGB(255,0,0);
else if ( (pLVCD->nmcd.dwItemSpec % 3) == 1 )
crText = RGB(0,255,0);
else
crText = RGB(128,128,255);
//把颜色保存在NMLVCUSTOMDRAW结构体中;
pLVCD->clrText = crText;
// 告诉 Windows 自己绘制控件;
*pResult = CDRF_DODEFAULT;
}
}
上面代码的结果如下图所示,
看看每行Windows用的颜色,很酷吧。这只用了十几条语句;
我们需要记住一点就是:在我们做任何事情时必须总是先去检查绘制阶段。因为你的处理函数会接
受到很多消息,绘制阶段决定你的代码怎么写。
一个复杂一点的例子
下面的例子展示怎样自定义绘制列表项子项(也就是列)。我们的处理函数会设置每个方格的文字
和背景颜色,但不会比前一个复杂多少,只多了一个if语句。在处理项子项时涉及到的步骤有:
1 在绘制整个控件前时处理NM_CUSTOMDRAW消息;
2 告诉Window我们想获得每个项的NM_CUSTOMDRAW消息;
3 当接受到以上消息时,在绘制项子项前告诉Windows我们想获得每个项子项的NM_CUSTOMDRAW消
息;
4 当每个项子项的NM_CUSTOMDRAW消息到来时,设置文字和背景的颜色。
发现我们会获得每一项的NM_CUSTOMDRAW消息和每个项子项的NM_CUSTOMDRAW。以下是代码.
void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
//默认处理除非我们把它设为其它值;
*pResult = CDRF_DODEFAULT;
//首先,检查绘制阶段。如果是控件绘制前阶段,告诉Windows我们想获得每项的消息;
if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
//这是列表项的子项通知信息。我们会在每个项子项的绘制前阶段请求通知消息
*pResult = CDRF_NOTIFYSUBITEMDRAW;
}
else if ( (CDDS_ITEMPREPAINT | CDDS_SUBITEM) == pLVCD->nmcd.dwDrawStage )
{
//这是项子项的绘制前的阶段。在这里我们设置文字和背景的颜色。我们的返回值告诉
//Windows自己绘制项子项,但它会用我们在这里设置的新颜色。文字的颜色会在红,绿
//,蓝循环,第一列的颜色为蓝,第二列颜色为红,第三列为黑。
COLORREF crText, crBkgnd;
if ( 0 == pLVCD->iSubItem )
{
crText = RGB(255,0,0);
crBkgnd = RGB(128,128,255);
}
else if ( 1 == pLVCD->iSubItem )
{
crText = RGB(0,255,0);
crBkgnd = RGB(255,0,0);
}
else
{
crText = RGB(128,128,255);
crBkgnd = RGB(0,0,0);
}
//把颜色保存在NMLVCUSTOMDRAW结构体中;
pLVCD->clrText = crText;
pLVCD->clrTextBk = crBkgnd;
// 告诉 Windows 自己绘制控件;
*pResult = CDRF_DODEFAULT;
}
}
以上代码的结果如图所示:
需要注意的一些地方:
1 背景颜色只绘制在一列中。右边的列和下方的行的背景色依然是控件的 背景色。
2 当我回看这篇文章时,看到标题“NM_CUSTOMDRAW(列表视图)”,这篇 文章说你能够在
第一个自定义绘制消息时返回标志 DRF_NOTIFYSUBITEMDRAW,而没有处理CDDS_ITEMPREPAINT绘
制阶段。我 测试过,但是,这个不行。你必须要处理CDDS_ITEMPREPAINT这个绘制 阶段
。
处理绘制后阶段:
到次为止,例子都是处理绘制前阶段,来改变列表项的展现情况。但是,在绘制前阶段,你的能力
只能限制在改变一下文字或者背景的颜色上。如果你想改变一下图标是怎样绘制的,你可以在绘制
前阶段绘制整个控件(矫枉过正),或者在绘制后阶段绘制。如果你在绘制后阶段做自定义绘制,
自定义绘制函数会在windows绘制好整个项或子项时被调用,你可以做任何额外的绘制。
在这个例子中,我会创建一个列表项被选择后,但图标颜色不会改变的控件。涉及到的步骤为:
1 对整个控件在绘制前阶段处理NM_CUSTOMDRAW消息;
2 告诉Windwos我们想为每个列表项获取NM_CUSTOMDRAW消息;
3 当列表项消息到来时,告诉windows我们想在绘制后阶段为每个列表项获取NM_CUSTOMDRAW消
息。
4 当每个列表项的NM_CUSTOMDRAW消息来时,在必要时重绘图标。
代码如下:
void CMyDlg::OnCustomdrawMyList ( NMHDR* pNMHDR, LRESULT* pResult )
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( pNMHDR );
*pResult = 0;
//如果是绘制控件周期的开始时,为每个列表项请求NM_CUSTOMDRAW消息;
if ( CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage )
{
*pResult = CDRF_NOTIFYITEMDRAW;
}
else if ( CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage )
{
//这是在列表项绘制前阶段。我们需要请求一个在绘制后的NM_CUSTOMDRAW消息;
*pResult = CDRF_NOTIFYPOSTPAINT;
}
else if ( CDDS_ITEMPOSTPAINT == pLVCD->nmcd.dwDrawStage )
{
// 如果列表项被选择,用正常的颜色重绘图标(效果是不会高亮起来)
LVITEM rItem;
int nItem = static_cast<int>( pLVCD->nmcd.dwItemSpec );
//获取图像的索引和项的状态。我们需要人工检查列表项是否被选择。msdn文档说
//列表项的状态在 pLVCD->nmcd.uItemState上,但在我的测试中,他一直等于 0x0201,
//没有任何意义,因为在commctrl.h中最大的 CDIS_*常量为 0x0100.
ZeroMemory ( &rItem, sizeof(LVITEM) );
rItem.mask = LVIF_IMAGE | LVIF_STATE;
rItem.iItem = nItem;
rItem.stateMask = LVIS_SELECTED;
m_list.GetItem ( &rItem );
//如果这个列表项被选择,用正常的颜色重绘图标。
if ( rItem.state & LVIS_SELECTED )
{
CDC* pDC = CDC::FromHandle ( pLVCD->nmcd.hdc );
CRect rcIcon;
// Get the rect that holds the item's icon.
m_list.GetItemRect ( nItem, &rcIcon, LVIR_ICON );
// Draw the icon.
m_imglist.Draw ( pDC, rItem.iImage, rcIcon.TopLeft(),
ILD_TRANSPARENT );
*pResult = CDRF_SKIPDEFAULT;
}
}
}
再次,自定义绘制能够让我们尽可能的做少点的事情,以上例子是让Windows做一切绘制工作,然
后我们重绘被选择项的图标。这样的效果
就是用户看到的是我们重绘的图标。效果见下图,被选择项的图标和未被选择的图标一致。
用自定义绘制代替自己绘制
另一个比较好的事就是你可以用custom draw 代替owner draw.这两者的区别在于实现相同的效果
,custom draw 编写的代码更加少,也更加容易理解。还有一个优点就是如果你使用custom draw
绘制,你可以选择只绘制其中几行,其它行就交给windows来绘制。如果你使用owner draw,所有事
情都是你自己做,尽管你不想在某些行中实现特殊效果。你在每项绘制前阶段处理NM_CUSTOMDRAW
消息,做好所有的绘制工作,然后返回 CDRF_SKIPDEFAULT标志。这个和我们前面做的都不相同。
CDRF_SKIPDEFAULT告诉Windows不要再做任何绘制了,因为我们已经做好了绘制工作。
我不会把这些代码贴出来,因为有点长。但是你可以一步一步的试一下,看下结果如何。如果你把
代码下下来,你会看到演示程序的对话框,并同时看到代码。你将会一步一步的看到绘制过程。列
表控件很简单,只有一列,但没有头部,如图:
其它一些你能做的事(也许)
用点想象力,你能用custom draw绘制一些其它的效果来。在最近的项目中,我绘制过这种控件,如
下图:
我不会把这个产品的名字说出来,以免有人说我在做广告,但是你能指出来,呵呵。注意到当文本
只有一行的数量时,它和正常的list control相同,但当文本多了,它变会分行。这样,所有的文
本都能看到,用户不需要向前或者向后拉动来看清所有的文字。我在控件绘制前阶段绘制所有东西
从而来实现该效果。
为什么我把标题说成是也许呢。正如我前面提到的一样,MSDN指出能在擦除前阶段和擦除后阶段绘
制。我从来没有在这些阶段绘制过图形。所以,为了写这篇文章,我想做个例子在列表项被擦除后
绘制图案。但是,我不能在列表项被擦除前或者擦除后获得NM_CUSTOMDRAW消息。在我的处理函数
中,我试了很多,也实验了不少,最终,我放弃了。对于这点,我很怀疑文档的正确性,因为在标
题为“NM_CUSTOMDRAW(list view)”的那页中,它列出了CDRF_NOTIFYITEMERASE的值,但这个值并
没有在 commctrl.h头文件中出现。当这么一个重要的信息都错了,我开始怀疑周围的一些文档的准确性。
在任何事件中,如果你在擦除前阶段或者擦除后阶段做了相关处理,你必须在绘制前阶段做相应处
理。否则,Windwos默认的绘制行为会被你在擦除阶段做的相应处理给消灭掉。基于上述几点,我
想不出你在擦除阶段处理消息的任何理由。任何特殊的显示效果都在绘制前阶段轻松的完成。
演示工程
演示程序了包含了我上面提到的四个列表控件。程序里包含了这个控件的完整的代码和custom
draw 绘制处理函数。这些代码能够帮助你更好的感受一下custom draw.这些代码也可以用到你自
己的程序中。
原文见:http://www.codeproject.com/KB/list/lvcustomdraw.aspx