AD
AD

Visual C++ 2005 中开发自定义的绘图控件

时间:2007-12-26 23:49:41  来源:  作者:
AD
摘要:TomArcher介绍自定义的绘图技术 — 开发自定义/com/">控件,使自己的应用/doc/">程序具有独特的外观。

字串8

下载相关的示例代码,download.microsoft.com/download/4/6/c/46ccef01-db01-4c5a-bcab-a0e22a4e74d0/CustomDraw.exe">CustomDraw.exe。

字串1

*
本页内容
您想变得有多与众不同? 您想变得有多与众不同?
主宰绘图操作 主宰绘图操作
实现自定义绘制的三步曲 实现自定义绘制的三步曲
示例:创建一个列表视图/com/"控件自定义绘制/com/">控件 hspace=4 src="/upimg/071226/11bCAX64910T1D.gif" width=7 vspace=2 border=0> 示例:创建一个列表视图/com/">控件自定义绘制/com/">控件
小结 小结
致谢 致谢
参考资料 参考资料

至今我仍然记得一次对话(回首 1995 年,那时我在 Peachtree Software 管理一支开发团队),话题是关于 Visual C++ 和 MFC 能为我们节约多少时间,从而使计帐系统的上市时间能加快多少。大概情况是这样的:

字串5

:Visual Studio 向导将使我们能够在几秒钟的时间内生成应用/doc/">程序的框架。我们基本上可以免费得到所有用户界面。从菜单、状态栏、完整的/doc/">文档/视图结构,到单独的数据和演示文稿、/tool/">工具栏等。甚至,它们还在其中生成了类似于文件打开、打印和打印预览的功能! 字串9

市场人员:听起来不错。那么,你们完成全部编码要用多长时间?

字串7

:鉴于我们得到的所有 UI 都是免费的,并且只需加入有关计帐的内容,所以会花 6 到 9 个月的时间完成编码。最棒的是,该应用/doc/">程序的外观会象一个 Microsoft Office 应用/doc/">程序一样!

字串6

市场人员:哦?

字串2

:真的。从具有 Microsoft 风格的应用/doc/">程序中,我们可以获得潜在的好处。这一点特别重要,如果我们的应用/doc/">程序象个 Office 产品,那么在其包装盒上打上 Windows 95 徽标会更容易。 字串6

市场人员:我们不能在市场上大喊“购买我们的产品吧,它多象 XXX 产品呀。”所有的计帐产品都具有相同的基本功能。我们的产品区别于其他产品的唯一方式就是用户界面。我们要雇佣图形设计师来设计一个完全自定义的用户界面,然后您的团队再进行编码。这要花多少时间? 字串8

:在我们没看到准确的/com/">控件前,很难说会花多长时间,但至少这样会加倍我们的工作。

字串5

市场人员:那么,你们最好尽快开始。

字串5

两年后,Peachtree Software 发布了自己第一个从零开始设计并创建的产品,并且作为其中的一份子,我也以此为而骄傲。经过 10 年的变迁,我也带头进行了一些知名产品(分别为 IBM、AT&T 和 VeriSign)的开发,这些产品在全球数百万的 PC 和电话上运转着,期间,我始终记得一个教训:不管应用/doc/">程序在内部运行时有多好,但如果它不能在纷繁的产品中脱颖而出并抓住用户的心,那么它也卖不出去。 字串5

因此,当我在 MSDN 发表第一篇文章时,我考虑最好着眼于一个我感兴趣的题目 — 过去我在 Peachtree 经常使用的一项技术,用来开发一些市场部门需要的奇妙的 UI 部件 — 开发自定义的绘图/com/">控件。

字串4

您想变得有多与众不同?

在您决定开发 Windows 提供的常规免费自定义/com/">控件范围之外的/com/">控件之后,您必需确定自己的/com/">控件将有多少独到之处 — 在功能和外观两方面。例如,我们假定您正在创建一个类似于计速表的/com/">控件。由于公共/com/">控件库 (ComCtrl32.dll) 中没有类似的/com/">控件,您完全需要自己进行以下操作:编写所有/com/">控件功能需要的代码,进行绘制,默认终端用户的交互,以及/com/">控件与其父窗口之间需要的任意消息处理。 字串4

另一方面,还包括一些您只想调整公共/com/">控件功能的情况。例如,我们假定您想创建一个屏蔽编辑/com/">控件,它只允许接受指定的字符。如果使用 MFC,通常涉及从 MFC 提供的类派生一个类,该类封装了一个公共/com/">控件(在屏蔽编辑/com/">控件中,通常为 CEdit),重写必需的虚函数(或处理指定的消息),然后加入自定义的代码。

字串5

本文讨论的重点介于两者之间 — 公共/com/">控件赋予您想要的大部分功能,但/com/">控件的外观并不是您想要的。例如,列表视图/com/">控件提供在许多视图风格中显示数据列表的方式 — 小图标、大图标、列表和详细列表(报告)。然而,如果您想要一个网格/com/">控件,那结果怎样呢?尽管公共/com/">控件库里没有特别包含网格,但是列表视图/com/">控件与它较为接近,它以行和列显示数据,并有一个相关的标头/com/">控件。因此,许多人以一个标准的列表视图/com/">控件为起点创建自己的网格/com/">控件,然后重写该/com/">控件及其子项的呈现方式或绘制方式。

字串9

主宰绘图操作

即使“只”进行绘制,您仍然有至少四种选项可用,它们都具有鲜明的优缺点:

字串6

处理 WM_PAINT

字串6

所有者绘制 字串4

自定义绘制 字串7

处理 WM_CTLCOLOR

字串1

处理 WM_PAINT

字串1

最极端的选择是执行一个 WM_PAINT 处理/doc/">程序,并且自己完成所有的绘制。这意味着,您的代码将需要进行一些与呈现/com/">控件相关的琐事 — 创建适当的设备上下文(一个或多个),决定/com/">控件的大小和位置,绘制/com/">控件等。在绘制过程中,很少需要这种级别的/com/">控件。

字串6

所有者绘制

字串2

控制/com/">控件绘制的另一种方法是利用所有者绘制。事实上,您也许听开发人员提到过所有者绘制/com/">控件,因为它是用于开发自定义/com/">控件最普通的技术。该技术普遍使用的主要原因在于,Windows 可为您提供很多帮助。在呈现/com/">控件的那一刻,Windows 就已经创建并填写了设备上下文,决定了/com/">控件的大小和位置,并且向您传递信息以使您了解此刻绘制的需求。对于列表/com/">控件(例如,列表框和列表视图),Windows 将为列表中的每一项调用绘制代码,这意味着您只需绘制这些项,而无需考虑/com/">控件的其他方面。注意,所有者绘制可用于大多数/com/">控件。然而,它不能用于编辑/com/">控件;并且考虑到列表/com/">控件,它只能用于报表视图样式。 字串4

自定义绘制

字串8

对于绘制自己的/com/">控件而言,这可能是最少为人所知的技术。事实上,许多技术能力较高的开发人员也混淆了术语所有者绘制 (owner-draw) 和自定义绘制 (custom-draw)。关于自定义/com/">控件,首先需要了解,它仅针对于指定的公共/com/">控件:标头、列表视图、rebar、/tool/">工具栏、/tool/">工具提示、跟踪条和树视图。此外,尽管所有者绘制只允许绘制报告视图风格的列表视图/com/">控件,而自定义绘制则使您能够处理列表视图/com/">控件所有视图风格的绘制。使用自定义绘制的另一个明显优势是,您可以对希望绘制的内容进行严格挑选。实现方式是,在/com/">控件绘制的每个阶段由 Windows 向代码发送一个消息。这样,您可以决定在每个阶段是自己进行所有的绘制工作,增加默认的绘制,还是允许 Windows 为该阶段执行所有的绘制。(鉴于自定义绘制是本文的一个主题,因此您很快会看到它的工作方式。)

字串4

处理 WM_CTLCOLOR 字串5

这可能是帮助决定如何呈现/com/">控件最简单的方式。正如消息名所指,当要绘制一个/com/">控件,并且它能让您的代码决定要使用的画笔时,发送 WM_CTLCOLOR 消息。通常情况下,如果您只想更改/com/">控件的颜色,并且不提供除/com/">控件本身之外的更多功能,则使用该技术。此外,对于由 Internet Explorer 引入的公共/com/">控件(列表视图、树视图、rebar 等),不发送该消息,并且它只与标准/com/">控件(编辑、列表框等)协同使用。 字串2

实现自定义绘制的三步曲

既然您已经了解了绘制/com/">控件可用的各种选项(包括使用自定义绘制的好处),那么,让我们来看看实现一个自定义绘制/com/">控件需要的三个主要步骤。

字串9

执行一个 NM_CUSTOMDRAW 消息处理/doc/">程序。 字串9

指定处理所需的绘制阶段。

字串8

筛选特定的绘制阶段(在这些阶段中,您需要加入自己的特定于/com/">控件的绘制代码)。

字串8

执行一个NM_CUSTOMDRAW 消息处理/doc/">程序

字串9

当需要绘制一个公共/com/">控件时,MFC 会将/com/">控件的自定义绘制通知消息(最初发送到/com/">控件的父窗口)以 NM_CUSTOMDRAW 消息的形式反馈给/com/">控件。以下是一个 NM_CUSTOMDRAW 处理/doc/">程序的示例。 字串1

void CMyCustomDrawControl::OnCustomDraw(NMHDR* pNMHDR, 
                                        LRESULT* pResult)
{
  LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR);
  ...
}
 
字串7

正如您所见,NM_CUSTOMDRAW 处理/doc/">程序将一个指针传递给 NMHDR 类型的结构。然而,该值不足以用于象 NMHDR 这样只包含三个成员(hwndFromidFromcode)的结构。

字串6

因此,您通常需要将该结构指针转换为信息量更大的结构 — LPNMCUSTOMDRAWLPNMCUSTOMDRAW 指向 NMCUSTOMDRAW,它包含诸如 dwDrawStagedwItemSpecuItemState 这样的成员 — 它们是决定当前绘制阶段及确切绘制(例如,/com/">控件本身、或/com/">控件的一个项目或子项)所必需的。 字串7

这里值得注意的是,还可以将 NMHDR 指针指向特定于正在绘制/com/">控件的类型的结构。表 1 显示/com/">控件的一个列表及其相关的自定义绘制结构类型名。

字串7

表 1:/com/">控件及其相关的自定义绘制结构
/com/">控件 结构(在 commctrl.h 中定义)

Rebar、Trackbar、AuthTicket、My.Resources、My.Settings、My.User 和 My.WebServices。 字串6

NMCUSTOMDRAW

字串6

List-view

字串7

NMLVCUSTOMDRAW 字串1

Toolbar 字串8

NMTBCUSTOMDRAW 字串4

Tooltip 字串5

NMTTCUSTOMDRAW

字串4

Tree-view 字串5

NMTVCUSTOMDRAW 字串8

指定处理所需的绘制阶段

字串2

正如我在前面提到的,绘制一个/com/">控件存在一些“阶段”。特别是,您可以将绘制过程理解为一系列阶段,其中/com/">控件通知其父窗口需要绘制的内容。事实上,/com/">控件甚至会在绘制/com/">控件及其各项前后发送一个通知,从而让/">编程人员更好地控制该过程。

字串8

在所有情况下,单一的 NM_CUSTOMDRAW 处理/doc/">程序在每个绘制阶段都进行调用。然而,谨记:自定义绘制允许您在自己的绘制中合并默认的/com/">控件绘制,您需要指定您将处理哪个绘制阶段。这通过设置 NM_CUSTOMDRAW 处理/doc/">程序的第二个参数 (pResult) 完成。事实上,如果您从未设置该值,则用初始阶段的 CDDS_PREPAINT 调用函数后,您的函数将不再被调用!

字串2

从技术上讲,只有两个阶段指定需要的绘制阶段(CDDS_PREPAINTCDDS_ITEMPREPAINT),它们影响发送通知消息的内容。然而,通常只在处理/doc/">程序的最后指定代码将处理的绘制阶段。表 2 列出用于指定所需绘制阶段(代码关注的)的值。

字串2

表 2:自定义绘制返回标志
自定义绘制返回标志 含义

CDRF_DEFAULT 字串6

指示/com/">控件自行绘制。该值为默认值,不应该将它与其他值组合在一起。 字串3

CDRF_SKIPDEFAULT 字串7

用于指定/com/">控件根本不进行任何绘制。 字串1

CDRF_NEWFONT

字串1

当代码更改绘制项/子项的字体时使用。

字串6

CDRF_NOTIFYPOSTPAINT 字串5

使通知信息在/com/">控件或每个项/子项绘制后发送。

字串5

CDRF_NOTIFYITEMDRAW 字串9

指出项(或子项)将进行绘制。注意,它下面的值与 CDRF_NOTIFYSUBITEMDRAW 相同。

字串2

CDRF_NOTIFYSUBITEMDRAW

字串8

指出子项(或项)将进行绘制。注意,它下面的值与 CDRF_NOTIFYITEMDRAW 相同。

字串6

CDRF_NOTIFYPOSTERASE

字串6

当删除/com/">控件后需要通知代码时使用。

字串6

以下为一个示例,其中的代码指定,当绘制/com/">控件的项 (CDRF_NOTIFYITEMDRAW) 及子项 (CDRF_NOTIFYPOSTPAINT),以及绘制完成时,应该调用 NM_CUSTOMDRAW 处理/doc/">程序。

字串6

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;
}
 

字串4

筛选指定的绘制阶段

字串4

一旦指定要关注的阶段后,您需要处理这些阶段。因为绘制过程的每个阶段只有一个消息要发送,惯例是执行一个 switch 语句以决定准确的绘制阶段。不同的绘制阶段由以下标志定义:

字串5

CDDS_PREPAINT
CDDS_ITEM
CDDS_ITEMPREPAINT
CDDS_ITEMPOSTPAINT
CDDS_ITEMPREERASE
CDDS_ITEMPOSTERASE
CDDS_SUBITEM
CDDS_POSTPAINT
CDDS_PREERASE
CDDS_POSTERASE
 
字串6

对于一个 CListCtrl 派生的类,有一个 NM_CUSTOMDRAW 处理/doc/">程序的示例,其中您可以发现,代码决定当前绘制阶段的方式:

字串2

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;
}
 
字串7

注意,为了决定子项(例如,列表视图/com/">控件)绘制的阶段,您必需使用按位 or 操作符,它有两个值:其中一个为 CDDS_ITEMPREPAINT 或者 CDDS_ITEMPOSTPAINT,另一个为 CDDS_SUBITEM

字串2

要说明它,我们假定您想在绘制列表视图项之前进行一些处理。将编写 switch 语句来处理 CDDS_ITEMPREPAINT字串3

case CDDS_ITEMPREPAINT:
...
break;
 字串8 

然而,如果是您所关注子项的预绘制阶段,则将如下操作:

字串3

case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
...
break;
 

字串9

示例:创建一个列表视图/com/">控件自定义绘制/com/">控件

如前面提到的,您可以完全控制/com/">控件及其项的绘制,或者仅执行一小部分特定于应用/doc/">程序的绘制,并让/com/">控件继续进行。本文的焦点更多地偏重于/com/">控件绘制技术而非高级的绘制技术,我们将演练一个简单的示例,其中列表视图/com/">控件是一个自定义的绘制,因此项的文本将在创建拼接外观的交替单元中显示为不同的颜色。 字串1

创建一个基于 Visual C++ 2005 对话框的项目,名为 ListCtrlColor

字串3

Class View 中选择 Project 菜单选项,并单击 Add Class 调用 Add Class 对话框。

字串2

从分类列表中选择 MFC,然后从模板列表中选择 MFC Class

字串1

单击 Add 按钮,调用 MFC Class Wizard 对话框。 字串2

对于 Class name,键入值 CListCtrlWithCustomDraw 并选择 CListCtrlBase class字串9

单击 Finish 按钮,生成类的标头和执行文件。

字串4

对于 Class View,右键单击 CListCtrlWithCustomDraw 类,并选择 Properties 上下文菜单选项。

字串1

显示 Properties 窗口时,单击顶部的 Messages 按钮,显示一个两列的消息列表,您可以为其实现处理/doc/">程序。 字串6

在消息列表中单击 NM_CUSTOMDRAW 项,然后下拉第二列的组合框箭头,并选择值 OnNMCustomdraw字串5

现在,处理绘制代码。这里,我们只简单处理项和子项预绘制阶段,指定基于当前行(项)和列(子项)的文本和背景色。要进行此操作,按如下所示修改 OnNMCustomdraw 函数: 字串4

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;
}
 
字串5

现在,我们来测试新/com/">控件。要进行此操作,您只需使用 CListCtrlWithCustomDraw 类将列表视图/com/">控件放在对话框中,并对其进行子类派生。下面是完成该操作的步骤。 字串2

Resource 视图中,打开应用/doc/">程序的主对话框 (IDD_LISTCTRLCOLOR_DIALOG)。

字串7

Toolbox 中,将一个 List Control 拖放到该对话框。

字串7

右键单击列表/com/">控件,并选择 Properties 上下文菜单选项。 字串6

View 属性设置为 Report字串8

右键单击/com/">控件,并选择 Add Variable 上下文菜单选项。

字串5

出现 Add Member Variable Wizard 对话框时,指定 m_lstBooksVariable name,并单击 Finish 按钮。 字串9

这时,您就有了一个 CListCtrl 派生类 (m_lstBooks),它将对话框上的列表视图/com/">控件进行子类派生。然而,m_lstBooks 需要从最新创建的 CListCtrlWithCustomDraw 派生,以便于调用您的绘制代码。因此,打开对话框的标题文件 (ListCtrlColorDlg.h),将 m_lstBooks 更改为 CListCtrlWithCustomDraw 类型。 字串3

CListCtrlColorDlg 类开始之前,添加以下指令。

字串1

#include "ListCtrlWithCustomDraw.h"
 字串4 

将下面的代码添加到对话框的 OnInitDialog 成员函数,这样我们就能够看到一些列表视图行。 字串5

// 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);
}
 
字串6

现在,建立并运行应用/doc/">程序。图 1 为应用/doc/">程序外观的一个示例。

字串8


图 1. 自定义绘制示例应用/doc/">程序 字串7

小结

当 Windows 首次作为“下一代”操作系统引入到应用/doc/">程序开发之中时,它作为新图形用户界面的一个主要论据就是其一致性。该论据的要点所在是其具有一个通用的外观:统一的菜单项、通用/com/">控件等。这一通用性的感觉可能会一直延续,直到有第二家公司想设计其自己的应用/doc/">程序。简单说,提供外观与其他应用/doc/">程序雷同的应用/doc/">程序,任何公司都不会逃离这一怪圈。 字串6

要建立一个唯一的且让人过目难忘的用户界面,其中一种方式是为应用/doc/">程序设计并开发自定义的/com/">控件。希望本文能对您有所帮助,现在,您了解到一种非常强大的技术,它使您的应用/doc/">程序能从众多竞争对手的应用/doc/">程序中脱颖而出。 字串5

致谢

我要感谢 Microsoft 的项目经理 Andrew Whitechapel,我们合著有两本书籍(Inside C#, Second EditionVisual C++.NET Bible)。几年来,我从 Andrew 那里学到很多东西,包括本文中我写到的一些内容。 字串2

参考资料

msdn.microsoft.com/library/en-us/shellcc/platform/commctls/custdraw/custdraw.asp">Custom Draw Overview

字串3

msdn.microsoft.com/library/en-us/shellcc/platform/commctls/custdraw/refs.asp">Custom Draw Reference 字串8


文章评论

共有 0位编程爱好者发表了评论 查看完整内容

    评论加载中…
忒好程序员:www.teihao.com

推荐信息

     
忒好程序员
AD