问题
如果需要自定义控件的呈现效果,我们会使用两种不同的实现手法:OwnerDraw和CustomDraw。今天就来具体讲一讲它们的区别。
OwnerDraw
所谓OwnerDraw,这里的”Owner”可以理解为控件的父窗口,也可以是控件类的派生类。当控件的OwnerDraw属性被启用时,控件的父窗口负责控件的所有绘制工作。
如何启用控件的OwnerDraw属性
对于普通按钮,我们可以加上BS_OWNERDRAW属性。
对于ListBox控件,我们可以加上LBS_OWNERDRAWFIXED或者LBS_OWNERDRAWVARIABLE属性。
对于ListView控件,我们可以加上LVS_OWNERDRAWFIXED属性。
对于ComboBox控件,我们可以加上CBS_OWNERDRAWFIXED或者CBS_OWNERDRAWVARIABLE属性。
对于CStatic控件,我们可以加上SS_OWNERDRAW属性。
这里的XXXFIXED和XXXVARIABLE主要用在有列表项目的控件中,它们都是表明父窗口负责绘制控件,不同的是,XXXFIXED表明列表中的项目的高度都是一样的,而XXXVARIABLE表明项目高度可以不一样,是可变的。
OwnerDraw绘制原理
当OwnerDraw属性被启用,每当控件需要绘制的时候,系统会发送WM_DRAWITEM消息给父窗口。当我们不想在父窗口中处理这个消息时,我们可以从控件类中创建一个派上类,并在派上类中使用反射或者重写DrawItem虚函数实现WM_DRAWITEM消息的处理。
需要注意的是:如果控件的OwnerDraw属性没有被开启,则系统是不会发送WM_DRAWITEM消息的。
以下是我们通过重写DrawItem虚函数实现一个自定义按钮的例子,在这里,我们从CButton类派生出CMyButton类,并重写其DrawItem虚函数。
再次强调一下,我们需要先在资源管理器中启用按钮的OwnerDraw属性,DrawItem虚函数才能被调用。
CustomDraw
CustomDraw没有对应的属性可供开启。每当控件需要绘制的时候,系统都会发送NM_CUSTOMDRAW通知(借由WM_NOTIFY消息)给父窗口。控件的父窗口或者派生类可以选择处理或不处理此通知,如果处理了,则我们可以认为这个控件使用了CustomDraw机制。
CustomDraw绘制原理
在CustomDraw场景下,控件的大部分绘制工作还是由系统默认完成,我们只是在系统绘制之前或者之后对控件的呈现进行某种方式的”微调”。比如,我们可以使用CustomDraw机制来修改ListView控件中的项目背景色,文字的前景或背景色等。
在控件绘制的整个阶段中,系统划分了一系列不同的阶段,开发者设置了感兴趣的阶段后,系统就会在每个开发者感兴趣的阶段发送NM_CUSTOMDRAW通知。当我们收到NM_CUSTOMDRAW通知的时候,我们可以根据通知中的DrawStage知道当前绘制的阶段。
这里的感兴趣的阶段,可以这样理解:如果希望在控件绘制的各个阶段都收到NM_CUSTOMDRAW通知,我们需要手动设置NM_CUSTOMDRAW消息处理例程的第二个参数(pResult)。如果不对这个参数进行设置,则只会在CDDS_PREPAINT阶段收到NM_CUSTOMDRAW消息,其他所有绘制阶段将不会收到此消息。
可供使用的NM_CUSTOMDRAW返回标志
CDRF_DEFAULT Indicates that the control is to draw itself. This value—which should not be combined with any other value—is the default value.
CDRF_SKIPDEFAULT Used to specify that the control is not to do any drawing at all.
CDRF_NEWFONT Used if your code changes the font of an item/subitem being drawn.
CDRF_NOTIFYPOSTPAINT Results in notification messages being sent after the control or each item/subitem is drawn.
CDRF_NOTIFYITEMDRAW Indicates that an item (or subitem) is about to be drawn. Note that the underlying value for this is the same as CDRF_NOTIFYSUBITEMDRAW.
CDRF_NOTIFYSUBITEMDRAW Indicates that a subitem (or item) is about to be drawn. Note that the underlying value for this is the same as CDRF_NOTIFYITEMDRAW.
CDRF_NOTIFYPOSTERASE Used if your code needs to be notified after the control has been erased.
例如,我们希望ListView控件在绘制它的项目及子项目的时候能收到NM_CUSTOMDRAW消息,则我们需要在NM_CUSTOMDRAW处理例程中指定pResult。
当我们通过pResult设置我们感兴趣的绘制阶段后,我们可以通过在不同阶段编写不同的绘制代码,实现控件的自定义呈现效果。
控件绘制的不同阶段
CDDS_PREPAINT
CDDS_ITEM
CDDS_ITEMPREPAINT
CDDS_ITEMPOSTPAINT
CDDS_ITEMPREERASE
CDDS_ITEMPOSTERASE
CDDS_SUBITEM
CDDS_POSTPAINT
CDDS_PREERASE
CDDS_POSTERASE
以下是一个CustomDraw的例子:
在以上代码中,我们重写了控件的OnCustomDraw方法,当收到NM_CUSTOMDRAW消息的时候,此方法会被调用。我们使用了一个switch结构来对不同的绘制阶段进行处理。
例如,当我们想在项目绘制之前自定义文字的前景和背景色,我们可以在CDDS_ITEMPREPAINT和CDDS_SUBITEM阶段加入我们的绘制代码:
总结
1) OwnerDraw绘制机制需要开启控件对应的OWNERDRAW属性才能使用。
2) OwnerDraw需要”owner”负责控件的所有绘制工作,所以,这种方法可以适用于所有Windows界面控件,而CustomDraw只能适用于Common Controls。
3) 由于CustomDraw是在控件绘制的各个阶段加入我们自己的绘制代码,在大部分时候,控件的绘制还是有系统完成,所以,CustomDraw可以看做是一款”轻量级”OwnerDraw。