Visual Studio 2005:在 Visual C++ 中開發自定義的繪圖控件
發布日期: 2006-4-14 | 更新日期: 2006-4-14
Tom Archer
Program Manager, Microsoft
適用于:
Win32 API
Microsoft 基礎類
Visual C++ 2005
摘要:TomArcher介紹自定義的繪圖技術 — 開發自定義控件,使自己的應用程序具有獨特的外觀。
下載相關的示例代碼,CustomDraw.exe。
本頁內容
至今我仍然記得一次對話(回首 1995 年,那時我在 Peachtree Software 管理一支開發團隊),話題是關于 Visual C++ 和 MFC 能為我們節約多少時間,從而使計帳系統的上市時間能加快多少。大概情況是這樣的:
我:Visual Studio 向導將使我們能夠在幾秒鐘的時間內生成應用程序的框架。我們基本上可以免費得到所有用戶界面。從菜單、狀態欄、完整的文檔/視圖結構,到單獨的數據和演示文稿、工具欄等。甚至,它們還在其中生成了類似于文件打開、打印和打印預覽的功能!
市場人員:聽起來不錯。那么,你們完成全部編碼要用多長時間?
我:鑒于我們得到的所有 UI 都是免費的,并且只需加入有關計帳的內容,所以會花 6 到 9 個月的時間完成編碼。最棒的是,該應用程序的外觀會象一個 Microsoft Office 應用程序一樣!
市場人員:哦?
我:真的。從具有 Microsoft 風格的應用程序中,我們可以獲得潛在的好處。這一點特別重要,如果我們的應用程序象個 Office 產品,那么在其包裝盒上打上 Windows 95 徽標會更容易。
市場人員:我們不能在市場上大喊“購買我們的產品吧,它多象 XXX 產品呀。”所有的計帳產品都具有相同的基本功能。我們的產品區別于其他產品的唯一方式就是用戶界面。我們要雇傭圖形設計師來設計一個完全自定義的用戶界面,然后您的團隊再進行編碼。這要花多少時間?
我:在我們沒看到準確的控件前,很難說會花多長時間,但至少這樣會加倍我們的工作。
市場人員:那么,你們最好盡快開始。
兩年后,Peachtree Software 發布了自己第一個從零開始設計并創建的產品,并且作為其中的一份子,我也以此為而驕傲。經過 10 年的變遷,我也帶頭進行了一些知名產品(分別為 IBM、AT&T 和 VeriSign)的開發,這些產品在全球數百萬的 PC 和電話上運轉著,期間,我始終記得一個教訓:不管應用程序在內部運行時有多好,但如果它不能在紛繁的產品中脫穎而出并抓住用戶的心,那么它也賣不出去。
因此,當我在 MSDN 發表第一篇文章時,我考慮最好著眼于一個我感興趣的題目 — 過去我在 Peachtree 經常使用的一項技術,用來開發一些市場部門需要的奇妙的 UI 部件 — 開發自定義的繪圖控件。
您想變得有多與眾不同?
在您決定開發 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);
...
}
正如您所見,NM_CUSTOMDRAW 處理程序將一個指針傳遞給 NMHDR 類型的結構。然而,該值不足以用于象 NMHDR 這樣只包含三個成員(hwndFrom、idFrom 和 code)的結構。
因此,您通常需要將該結構指針轉換為信息量更大的結構 — LPNMCUSTOMDRAW。LPNMCUSTOMDRAW 指向 NMCUSTOMDRAW,它包含諸如 dwDrawStage、dwItemSpec 和 uItemState 這樣的成員 — 它們是決定當前繪制階段及確切繪制(例如,控件本身、或控件的一個項目或子項)所必需的。
這里值得注意的是,還可以將 NMHDR 指針指向特定于正在繪制控件的類型的結構。表 1 顯示控件的一個列表及其相關的自定義繪制結構類型名。
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 列出用于指定所需繪制階段(代碼關注的)的值。
CDRF_DEFAULT
|
指示控件自行繪制。該值為默認值,不應該將它與其他值組合在一起。
|
CDRF_SKIPDEFAULT
|
用于指定控件根本不進行任何繪制。
|
CDRF_NEWFONT
|
當代碼更改繪制項/子項的字體時使用。
|
CDRF_NOTIFYPOSTPAINT
|
使通知信息在控件或每個項/子項繪制后發送。
|
CDRF_NOTIFYITEMDRAW
|
指出項(或子項)將進行繪制。注意,它下面的值與 CDRF_NOTIFYSUBITEMDRAW 相同。
|
CDRF_NOTIFYSUBITEMDRAW
|
指出子項(或項)將進行繪制。注意,它下面的值與 CDRF_NOTIFYITEMDRAW 相同。
|
CDRF_NOTIFYPOSTERASE
|
當刪除控件后需要通知代碼時使用。
|
以下為一個示例,其中的代碼指定,當繪制控件的項 (CDRF_NOTIFYITEMDRAW) 及子項 (CDRF_NOTIFYPOSTPAINT),以及繪制完成時,應該調用 NM_CUSTOMDRAW 處理程序。
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;
}
篩選指定的繪制階段
一旦指定要關注的階段后,您需要處理這些階段。因為繪制過程的每個階段只有一個消息要發送,慣例是執行一個 switch 語句以決定準確的繪制階段。不同的繪制階段由以下標志定義:
CDDS_PREPAINT
CDDS_ITEM
CDDS_ITEMPREPAINT
CDDS_ITEMPOSTPAINT
CDDS_ITEMPREERASE
CDDS_ITEMPOSTERASE
CDDS_SUBITEM
CDDS_POSTPAINT
CDDS_PREERASE
CDDS_POSTERASE
對于一個 CListCtrl 派生的類,有一個 NM_CUSTOMDRAW 處理程序的示例,其中您可以發現,代碼決定當前繪制階段的方式:
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;
}
注意,為了決定子項(例如,列表視圖控件)繪制的階段,您必需使用按位 or 操作符,它有兩個值:其中一個為 CDDS_ITEMPREPAINT 或者 CDDS_ITEMPOSTPAINT,另一個為 CDDS_SUBITEM。
要說明它,我們假定您想在繪制列表視圖項之前進行一些處理。將編寫 switch 語句來處理 CDDS_ITEMPREPAINT。
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;
}
|
現在,我們來測試新控件。要進行此操作,您只需使用 CListCtrlWithCustomDraw 類將列表視圖控件放在對話框中,并對其進行子類派生。下面是完成該操作的步驟。
? |
在 Resource 視圖中,打開應用程序的主對話框 (IDD_LISTCTRLCOLOR_DIALOG)。
|
? |
從 Toolbox 中,將一個 List Control 拖放到該對話框。
|
? |
右鍵單擊列表控件,并選擇 Properties 上下文菜單選項。
|
? |
將 View 屬性設置為 Report。
|
? |
右鍵單擊控件,并選擇 Add Variable 上下文菜單選項。
|
? |
出現 Add Member Variable Wizard 對話框時,指定 m_lstBooks 的 Variable name,并單擊 Finish 按鈕。
|
? |
這時,您就有了一個 CListCtrl 派生類 (m_lstBooks),它將對話框上的列表視圖控件進行子類派生。然而,m_lstBooks 需要從最新創建的 CListCtrlWithCustomDraw 派生,以便于調用您的繪制代碼。因此,打開對話框的標題文件 (ListCtrlColorDlg.h),將 m_lstBooks 更改為 CListCtrlWithCustomDraw 類型。
|
? |
在 CListCtrlColorDlg 類開始之前,添加以下指令。
#include "ListCtrlWithCustomDraw.h"
|
? |
將下面的代碼添加到對話框的 OnInitDialog 成員函數,這樣我們就能夠看到一些列表視圖行。
// 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 為應用程序外觀的一個示例。
圖 1. 自定義繪制示例應用程序
|
小結
當 Windows 首次作為“下一代”操作系統引入到應用程序開發之中時,它作為新圖形用戶界面的一個主要論據就是其一致性。該論據的要點所在是其具有一個通用的外觀:統一的菜單項、通用控件等。這一通用性的感覺可能會一直延續,直到有第二家公司想設計其自己的應用程序。簡單說,提供外觀與其他應用程序雷同的應用程序,任何公司都不會逃離這一怪圈。
要建立一個唯一的且讓人過目難忘的用戶界面,其中一種方式是為應用程序設計并開發自定義的控件。希望本文能對您有所幫助,現在,您了解到一種非常強大的技術,它使您的應用程序能從眾多競爭對手的應用程序中脫穎而出。
致謝
我要感謝 Microsoft 的項目經理 Andrew Whitechapel,我們合著有兩本書籍(Inside C#, Second Edition 和 Visual C++.NET Bible)。幾年來,我從 Andrew 那里學到很多東西,包括本文中我寫到的一些內容。
參考資料
關于作者
Tom Archer 是 Microsoft 的一位項目經理。他曾經是 C++ MVP,Tom 現負責 Visual C++、Windows Vista 以及 MSDN 中的 Windows SDK Developer Centers。