Neat Stuff to Do in List Controls Using Custom Draw

 

微软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

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值