深度理解:OwnerDraw和CustomDraw

深度理解:OwnerDraw和CustomDraw

作者:BlogUpdater |  时间:2018-06-10 |  浏览:7617 |  评论已关闭 条评论

如果需要自定义控件的呈现效果,我们会使用两种不同的实现手法: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。

标签:

评论已关闭。