深度理解:OwnerDraw和CustomDraw
如果需要自定义控件的呈现效果,我们会使用两种不同的实现手法: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虚函数。
void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { CDC dc; dc.Attach(lpDrawItemStruct->hDC); //Get device context object CRect rt; rt = lpDrawItemStruct->rcItem; //Get button rect dc.FillSolidRect(rt, RGB(0, 0, 255)); //Fill button with blue color UINT state = lpDrawItemStruct->itemState; //Get state of the button if ( (state & ODS_SELECTED) ) // If it is pressed { dc.DrawEdge(rt,EDGE_SUNKEN,BF_RECT); // Draw a sunken face } else { dc.DrawEdge(rt,EDGE_RAISED,BF_RECT); // Draw a raised face } dc.SetTextColor(RGB(255,255,120)); // Set the color of the caption to be yellow CString strTemp; GetWindowText(strTemp); // Get the caption which have been set dc.DrawText(strTemp,rt,DT_CENTER|DT_VCENTER|DT_SINGLELINE); // Draw out the caption if ( (state & ODS_FOCUS ) ) // If the button is focused { // Draw a focus rect which indicates the user // that the button is focused int iChange = 3; rt.top += iChange; rt.left += iChange; rt.right -= iChange; rt.bottom -= iChange; dc.DrawFocusRect(rt); } dc.Detach(); }
再次强调一下,我们需要先在资源管理器中启用按钮的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。
void CListCtrlWithCustomDraw::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR); ... *pResult = 0; // 初始值 *pResult |= CDRF_NOTIFYITEMDRAW; // 绘制项目时收到NM_CUSTOMDRAW消息 *pResult |= CDRF_NOTIFYSUBITEMDRAW; // 绘制子项目时收到NM_CUSTOMDRAW消息 }
当我们通过pResult设置我们感兴趣的绘制阶段后,我们可以通过在不同阶段编写不同的绘制代码,实现控件的自定义呈现效果。
控件绘制的不同阶段
CDDS_PREPAINT
CDDS_ITEM
CDDS_ITEMPREPAINT
CDDS_ITEMPOSTPAINT
CDDS_ITEMPREERASE
CDDS_ITEMPOSTERASE
CDDS_SUBITEM
CDDS_POSTPAINT
CDDS_PREERASE
CDDS_POSTERASE
以下是一个CustomDraw的例子:
void CMyCustomDrawControl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR); switch(pNMCD->dwDrawStage) { case CDDS_PREPAINT: ... break; case CDDS_ITEMPREPAINT: ... break; case CDDS_ITEMPREPAINT | CDDS_SUBITEM: ... break; ... } *pResult = 0; }
在以上代码中,我们重写了控件的OnCustomDraw方法,当收到NM_CUSTOMDRAW消息的时候,此方法会被调用。我们使用了一个switch结构来对不同的绘制阶段进行处理。
例如,当我们想在项目绘制之前自定义文字的前景和背景色,我们可以在CDDS_ITEMPREPAINT和CDDS_SUBITEM阶段加入我们的绘制代码:
void CListCtrlWithCustomDraw::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLVCUSTOMDRAW lpLVCustomDraw = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR); switch(lpLVCustomDraw->nmcd.dwDrawStage) { case CDDS_ITEMPREPAINT: case CDDS_ITEMPREPAINT | CDDS_SUBITEM: lpLVCustomDraw->clrText = RGB(255,255,255); // 设置文字前景色 lpLVCustomDraw->clrTextBk = RGB(0,0,0); // 设置文字背景色 break; default: break; } *pResult = 0; *pResult |= CDRF_NOTIFYPOSTPAINT; *pResult |= CDRF_NOTIFYITEMDRAW; *pResult |= CDRF_NOTIFYSUBITEMDRAW; }
总结
1) OwnerDraw绘制机制需要开启控件对应的OWNERDRAW属性才能使用。
2) OwnerDraw需要”owner”负责控件的所有绘制工作,所以,这种方法可以适用于所有Windows界面控件,而CustomDraw只能适用于Common Controls。
3) 由于CustomDraw是在控件绘制的各个阶段加入我们自己的绘制代码,在大部分时候,控件的绘制还是有系统完成,所以,CustomDraw可以看做是一款”轻量级”OwnerDraw。
相关推荐
- Visual Studio新版C++代码分析工具介绍
- Posted on 03月27日
- 错误信息:正被停用的激活上下文不是最近激活的
- Posted on 08月26日
- VS Code中的CMake工具新功能速览
- Posted on 06月17日
- 古老的新玩法:使用命令行做些简单的计算
- Posted on 08月04日
评论已关闭。