本文源代码下载:CustomDraw.exe。
在您决定开发 Windows 提供的常规免费自定义控件范围之外的控件之后,您必需确定自己的控件将有多少独到之处 — 在功能和外观两方面。例如,我们假定您正在创建一个类似于计速表的控件。由于公共控件库 (ComCtrl32.dll) 中没有类似的控件,您完全需要自己进行以下操作:编写所有控件功能需要的代码,进行绘制,默认终端用户的交互,以及控件与其父窗口之间需要的任意消息处 理。
另一方面,还包括一些您只想调整公共控件功能的情况。例如,我们假定您想创建一个屏蔽编辑控件,它只允许接受指定的字符。如果使用 MFC,通常涉及从 MFC 提供的类派生一个类,该类封装了一个公共控件(在屏蔽编辑控件中,通常为 CEdit),重写必需的虚函数(或处理指定的消息),然后加入自定义的代码。
本文讨论的重点介于两者之间 — 公共控件赋予您想要的大部分功能,但控件的外观并不是您想要的。例如,列表视图控件提供在许多视图风格中显示数据列表的方式 — 小图标、大图标、列表和详细列表(报告)。然而,如果您想要一个网格控件,那结果怎样呢?尽管公共控件库里没有特别包含网格,但是列表视图控件与它较为接 近,它以行和列显示数据,并有一个相关的标头控件。因此,许多人以一个标准的列表视图控件为起点创建自己的网格控件,然后重写该控件及其子项的呈现方式或 绘制方式。
主宰绘图操作
即使“只”进行绘制,您仍然有至少四种选项可用,它们都具有鲜明的优缺点:
·处理 WM_PAINT
·所有者绘制
·自定义绘制
·处理 WM_CTLCOLOR
处理 WM_PAINT
最极端的选择是执行一个 WM_PAINT 处理程序,并且自己完成所有的绘制。这意味着,您的代码将需要进行一些与呈现控件相关的琐事 — 创建适当的设备上下文(一个或多个),决定控件的大小和位置,绘制控件等。在绘制过程中,很少需要这种级别的控件。
所有者绘制
控制控件绘制的另一种方法是利用所有者绘制。事实上,您也许听开发人员提到过所有者绘制控件,因为它是用于开发自定义控件最普通的技术。该技术普遍使 用的主要原因在于,Windows 可为您提供很多帮助。在呈现控件的那一刻,Windows 就已经创建并填写了设备上下文,决定了控件的大小和位置,并且向您传递信息以使您了解此刻绘制的需求。对于列表控件(例如,列表框和列表视 图),Windows 将为列表中的每一项调用绘制代码,这意味着您只需绘制这些项,而无需考虑控件的其他方面。注意,所有者绘制可用于大多数控件。然而,它不能用于编辑控件; 并且考虑到列表控件,它只能用于报表视图样式。
自定义绘制
对于绘制自己的控件而言,这可能是最少为人所知的技术。事实上,许多技术能力较高的开发人员也混淆了术语所有者绘制 (owner-draw) 和自定义绘制 (custom-draw)。关于自定义控件,首先需要了解,它仅针对于指定的公共控件:标头、列表视图、rebar、工具栏、工具提示、跟踪条和树视 图。此外,尽管所有者绘制只允许绘制报告视图风格的列表视图控件,而自定义绘制则使您能够处理列表视图控件所有视图风格的绘制。使用自定义绘制的另一个明 显优势是,您可以对希望绘制的内容进行严格挑选。实现方式是,在控件绘制的每个阶段由 Windows 向代码发送一个消息。这样,您可以决定在每个阶段是自己进行所有的绘制工作,增加默认的绘制,还是允许 Windows 为该阶段执行所有的绘制。(鉴于自定义绘制是本文的一个主题,因此您很快会看到它的工作方式。)
处理 WM_CTLCOLOR
这可能是帮助决定如何呈现控件最简单的方式。正如消息名所指,当要绘制一个控件,并且它能让您的代码决定要使用的画笔时,发送 WM_CTLCOLOR 消息。通常情况下,如果您只想更改控件的颜色,并且不提供除控件本身之外的更多功能,则使用该技术。此外,对于由 Internet Explorer 引入的公共控件(列表视图、树视图、rebar 等),不发送该消息,并且它只与标准控件(编辑、列表框等)协同使用。
实现自定义绘制的三步曲
既然您已经了解了绘制控件可用的各种选项(包括使用自定义绘制的好处),那么,让我们来看看实现一个自定义绘制控件需要的三个主要步骤。
·执行一个 NM_CUSTOMDRAW 消息处理程序。
·指定处理所需的绘制阶段。
·筛选特定的绘制阶段(在这些阶段中,您需要加入自己的特定于控件的绘制代码)。
执行一个NM_CUSTOMDRAW 消息处理程序
当需要绘制一个公共控件时,MFC 会将控件的自定义绘制通知消息(最初发送到控件的父窗口)以 NM_CUSTOMDRAW 消息的形式反馈给控件。以下是一个 NM_CUSTOMDRAW 处理程序的示例。
void CMyCustomDrawControl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); ... } |
控件 | 结构(在 commctrl.h 中定义) |
Rebar、Trackbar、AuthTicket、My.Resources、My.Settings、My.User 和 My.WebServices。 | NMCUSTOMDRAW |
List-view | NMLVCUSTOMDRAW |
Toolbar | NMTBCUSTOMDRAW |
Tooltip | NMTTCUSTOMDRAW |
Tree-view | NMTVCUSTOMDRAW |
指定处理所需的绘制阶段
正如我在前面提到的,绘制一个控件存在一些“阶段”。特别是,您可以将绘制过程理解为一系列阶段,其中控件通知其父窗口需要绘制的内容。事实上,控件甚至会在绘制控件及其各项前后发送一个通知,从而让编程人员更好地控制该过程。
在所有情况下,单一的 NM_CUSTOMDRAW 处理程序在每个绘制阶段都进行调用。然而,谨记:自定义绘制允许您在自己的绘制中合并默认的控件绘制,您需要指定您将处理哪个绘制阶段。这通过设置 NM_CUSTOMDRAW 处理程序的第二个参数 (pResult) 完成。事实上,如果您从未设置该值,则用初始阶段的 CDDS_PREPAINT 调用函数后,您的函数将不再被调用!
从技术上讲,只有两个阶段指定需要的绘制阶段(CDDS_PREPAINT 和 CDDS_ITEMPREPAINT),它们影响发送通知消息的内容。然而,通常只在处理程序的最后指定代码将处理的绘制阶段。表 2 列出用于指定所需绘制阶段(代码关注的)的值。
表 2:自定义绘制返回标志
自定义绘制返回标志 | 含义 |
CDRF_DEFAULT | 指示控件自行绘制。该值为默认值,不应该将它与其他值组合在一起。 |
CDRF_SKIPDEFAULT | 用于指定控件根本不进行任何绘制。 |
CDRF_NEWFONT | 当代码更改绘制项/子项的字体时使用。 |
CDRF_NOTIFYPOSTPAINT | 使通知信息在控件或每个项/子项绘制后发送。 |
CDRF_NOTIFYITEMDRAW | 指出项(或子项)将进行绘制。注意,它下面的值与 CDRF_NOTIFYSUBITEMDRAW 相同。 |
CDRF_NOTIFYSUBITEMDRAW | 指出子项(或项)将进行绘制。注意,它下面的值与 CDRF_NOTIFYITEMDRAW 相同。 |
CDRF_NOTIFYPOSTERASE | 当删除控件后需要通知代码时使用。 |
void CListCtrlWithCustomDraw::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); ... *pResult = 0; // Initialize value *pResult |= CDRF_NOTIFYITEMDRAW; *pResult |= CDRF_NOTIFYSUBITEMDRAW; *pResult |= CDRF_NOTIFYPOSTPAINT; } |
CDDS_PREPAINT CDDS_ITEM CDDS_ITEMPREPAINT CDDS_ITEMPOSTPAINT CDDS_ITEMPREERASE CDDS_ITEMPOSTERASE CDDS_SUBITEM CDDS_POSTPAINT CDDS_PREERASE CDDS_POSTERASE |
void CMyCustomDrawControl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); switch(pNMCD->dwDrawStage) { case CDDS_PREPAINT: ... break; case CDDS_ITEMPREPAINT: ... break; case CDDS_ITEMPREPAINT | CDDS_SUBITEM: ... break; ... } *pResult = 0; } |
case CDDS_ITEMPREPAINT: ... break; |
case CDDS_ITEMPREPAINT | CDDS_SUBITEM: ... break; |
示例:创建一个列表视图控件自定义绘制控件
如前面提到的,您可以完全控制控件及其项的绘制,或者仅执行一小部分特定于应用程序的绘制,并让控件继续进行。本文的焦点更多地偏 重于控件绘制技术而非高级的绘制技术,我们将演练一个简单的示例,其中列表视图控件是一个自定义的绘制,因此项的文本将在创建拼接外观的交替单元中显示为 不同的颜色。
·创建一个基于 Visual C++ 2005 对话框的项目,名为 ListCtrlColor。
·从 Class View 中选择 Project 菜单选项,并单击 Add Class 调用 Add Class 对话框。
·从分类列表中选择 MFC,然后从模板列表中选择 MFC Class。
·单击 Add 按钮,调用 MFC Class Wizard 对话框。
·对于 Class name,键入值 CListCtrlWithCustomDraw 并选择 CListCtrl 的 Base class。
·单击 Finish 按钮,生成类的标头和执行文件。
·对于 Class View,右键单击 CListCtrlWithCustomDraw 类,并选择 Properties 上下文菜单选项。
·显示 Properties 窗口时,单击顶部的 Messages 按钮,显示一个两列的消息列表,您可以为其实现处理程序。
·在消息列表中单击 NM_CUSTOMDRAW 项,然后下拉第二列的组合框箭头,并选择值 OnNMCustomdraw。
·现在,处理绘制代码。这里,我们只简单处理项和子项预绘制阶段,指定基于当前行(项)和列(子项)的文本和背景色。要进行此操作,按如下所示修改 OnNMCustomdraw 函数:
void CListCtrlWithCustomDraw::OnNMCustomdraw(NMHDR *pNMHDR, LRESULT *pResult) { LPNMLVCUSTOMDRAW lpLVCustomDraw = reinterpret_cast(pNMHDR); switch(lpLVCustomDraw->nmcd.dwDrawStage) { case CDDS_ITEMPREPAINT: case CDDS_ITEMPREPAINT | CDDS_SUBITEM: if (0 == ((lpLVCustomDraw->nmcd.dwItemSpec + lpLVCustomDraw->iSubItem) % 2)) { lpLVCustomDraw->clrText = RGB(255,255,255); // white text lpLVCustomDraw->clrTextBk = RGB(0,0,0); // black background } else { lpLVCustomDraw->clrText = CLR_DEFAULT; lpLVCustomDraw->clrTextBk = CLR_DEFAULT; } break; default: break; } *pResult = 0; *pResult |= CDRF_NOTIFYPOSTPAINT; *pResult |= CDRF_NOTIFYITEMDRAW; *pResult |= CDRF_NOTIFYSUBITEMDRAW; } |
#include "ListCtrlWithCustomDraw.h" |
// Insert the columns m_lstBooks.InsertColumn(0, _T("Author")); m_lstBooks.InsertColumn(1, _T("Book")); // Define the data static struct { TCHAR m_szAuthor[50]; TCHAR m_szTitle[100]; } BOOK_INFO[] = { _T("Tom Archer"), _T("Visual C++.NET Bible"), _T("Tom Archer"), _T("Extending MFC with the .NET Framework"), _T("Brian Johnson"), _T("XBox 360 For Dummies") }; // Insert the data int idx; for (int i = 0; i < sizeof BOOK_INFO / sizeof BOOK_INFO[0]; i++) { idx = m_lstBooks.InsertItem(i, BOOK_INFO[i].m_szAuthor); m_lstBooks.SetItemText(i, 1, BOOK_INFO[i].m_szTitle); } |
图 1. 自定义绘制示例应用程序 |