在您決定開(kāi)發(fā) Windows 提供的常規(guī)免費(fèi)自定義控件范圍之外的控件之后,您必需確定自己的控件將有多少獨(dú)到之處 — 在功能和外觀兩方面。例如,我們假定您正在創(chuàng)建一個(gè)類(lèi)似于計(jì)速表的控件。由于公共控件庫(kù) (ComCtrl32.dll) 中沒(méi)有類(lèi)似的控件,您完全需要自己進(jìn)行以下操作:編寫(xiě)所有控件功能需要的代碼,進(jìn)行繪制,默認(rèn)終端用戶(hù)的交互,以及控件與其父窗口之間需要的任意消息處理。
(#add 兩方面,公共控件庫(kù)中沒(méi)有類(lèi)似的 完全重寫(xiě); 只想調(diào)整公共控件功能,則可以部分修改)
另一方面,還包括一些您只想調(diào)整公共控件功能的情況。例如,我們假定您想創(chuàng)建一個(gè)屏蔽編輯控件,它只允許接受指定的字符。如果使用 MFC,通常涉及從 MFC 提供的類(lèi)派生一個(gè)類(lèi),該類(lèi)封裝了一個(gè)公共控件(在屏蔽編輯控件中,通常為 CEdit),重寫(xiě)必需的虛函數(shù)(或處理指定的消息),然后加入自定義的代碼。
本文討論的重點(diǎn)介于兩者之間 — 公共控件賦予您想要的大部分功能,但控件的外觀并不是您想要的。例如,列表視圖控件提供在許多視圖風(fēng)格中顯示數(shù)據(jù)列表的方式 — 小圖標(biāo)、大圖標(biāo)、列表和詳細(xì)列表(報(bào)告)。然而,如果您想要一個(gè)網(wǎng)格控件,那結(jié)果怎樣呢?盡管公共控件庫(kù)里沒(méi)有特別包含網(wǎng)格,但是列表視圖控件與它較為接近,它以行和列顯示數(shù)據(jù),并有一個(gè)相關(guān)的標(biāo)頭控件。因此,許多人以一個(gè)標(biāo)準(zhǔn)的列表視圖控件為起點(diǎn)創(chuàng)建自己的網(wǎng)格控件,然后重寫(xiě)該控件及其子項(xiàng)的呈現(xiàn)方式或繪制方式。
即使“只”進(jìn)行繪制,您仍然有至少四種選項(xiàng)可用,它們都具有鮮明的優(yōu)缺點(diǎn):
·處理 WM_PAINT
·所有者繪制(owner draw)
·自定義繪制(custom draw)
·處理 WM_CTLCOLOR
處理 WM_PAINT
最極端的選擇是執(zhí)行一個(gè) WM_PAINT 處理程序,并且自己完成所有的繪制。這意味著,您的代碼將需要進(jìn)行一些與呈現(xiàn)控件相關(guān)的瑣事 — 創(chuàng)建適當(dāng)?shù)脑O(shè)備上下文(一個(gè)或多個(gè)),決定控件的大小和位置,繪制控件等。在繪制過(guò)程中,很少需要這種級(jí)別的控件。
所有者繪制
控制控件繪制的另一種方法是利用所有者繪制。事實(shí)上,您也許聽(tīng)開(kāi)發(fā)人員提到過(guò)所有者繪制控件,因?yàn)樗怯糜陂_(kāi)發(fā)自定義控件最普通的技術(shù)。該技術(shù)普遍使用的主要原因在于,Windows 可為您提供很多幫助。在呈現(xiàn)控件的那一刻,Windows 就已經(jīng)創(chuàng)建并填寫(xiě)了設(shè)備上下文,決定了控件的大小和位置,并且向您傳遞信息以使您了解此刻繪制的需求。對(duì)于列表控件(例如,列表框和列表視圖),Windows 將為列表中的每一項(xiàng)調(diào)用繪制代碼,這意味著您只需繪制這些項(xiàng),而無(wú)需考慮控件的其他方面。注意,所有者繪制可用于大多數(shù)控件。然而,它不能用于編輯控件;并且考慮到列表控件,它只能用于報(bào)表視圖樣式。
自定義繪制
對(duì)于繪制自己的控件而言,這可能是最少為人所知的技術(shù)。事實(shí)上,許多技術(shù)能力較高的開(kāi)發(fā)人員也混淆了術(shù)語(yǔ)所有者繪制 (owner-draw) 和自定義繪制 (custom-draw)。關(guān)于自定義控件,首先需要了解,它僅針對(duì)于指定的公共控件:標(biāo)頭、列表視圖、rebar、工具欄、工具提示、跟蹤條和樹(shù)視圖。此外,盡管所有者繪制只允許繪制報(bào)告視圖風(fēng)格的列表視圖控件,而自定義繪制則使您能夠處理列表視圖控件所有視圖風(fēng)格的繪制。使用自定義繪制的另一個(gè)明顯優(yōu)勢(shì)是,您可以對(duì)希望繪制的內(nèi)容進(jìn)行嚴(yán)格挑選。實(shí)現(xiàn)方式是,在控件繪制的每個(gè)階段由 Windows 向代碼發(fā)送一個(gè)消息。這樣,您可以決定在每個(gè)階段是自己進(jìn)行所有的繪制工作,增加默認(rèn)的繪制,還是允許 Windows 為該階段執(zhí)行所有的繪制。(鑒于自定義繪制是本文的一個(gè)主題,因此您很快會(huì)看到它的工作方式。)
處理 WM_CTLCOLOR消息
這可能是幫助決定如何呈現(xiàn)控件最簡(jiǎn)單的方式。正如消息名所指,當(dāng)要繪制一個(gè)控件,并且它能讓您的代碼決定要使用的畫(huà)筆時(shí),發(fā)送 WM_CTLCOLOR 消息(#add 似乎不對(duì),應(yīng)該用消息反射)。通常情況下,如果您只想更改控件的顏色(#addSetTextColor SetBkColor),并且不提供除控件本身之外的更多功能,則使用該技術(shù)。此外,對(duì)于由 Internet Explorer 引入的公共控件(列表視圖、樹(shù)視圖、rebar 等),不發(fā)送該消息,并且它只與標(biāo)準(zhǔn)控件(編輯、列表框等)協(xié)同使用。
CTLCOLOR_STATIC Static control
CTLCOLOR_BTN Button control
CTLCOLOR_EDIT Edit control
CTLCOLOR_LISTBOX List-box control
CTLCOLOR_SCROLLBAR Scroll-bar control
CTLCOLOR_DLG Dialog box
CTLCOLOR_MSGBOX Message box
不會(huì)為組合框中的下拉列表框調(diào)用OnCtlColor函數(shù),因?yàn)橄吕斜砜驅(qū)嶋H上是組合框的子窗口,而不是窗口的子窗口。要改變下拉列表框的顏色,創(chuàng)建一個(gè)CComboBox,在重載的OnCtlColor中的nCtlColor參數(shù)中檢查CTLCOLOR_LISTBOX。在這個(gè)處理函數(shù)中,為設(shè)置文本的背景必須使用SetBkColor成員函數(shù)。
自定義繪制:
既然您已經(jīng)了解了繪制控件可用的各種選項(xiàng)(包括使用自定義繪制的好處),那么,讓我們來(lái)看看實(shí)現(xiàn)一個(gè)自定義繪制控件需要的三個(gè)主要步驟。
·執(zhí)行一個(gè) NM_CUSTOMDRAW 消息處理程序。
·指定處理所需的繪制階段。
·篩選特定的繪制階段(在這些階段中,您需要加入自己的特定于控件的繪制代碼)。
1,執(zhí)行一個(gè)NM_CUSTOMDRAW 消息處理程序
當(dāng)需要繪制一個(gè)公共控件時(shí),MFC 會(huì)將控件的自定義繪制通知消息(最初發(fā)送到控件的父窗口)以 NM_CUSTOMDRAW 消息的形式反饋給控件。以下是一個(gè) NM_CUSTOMDRAW 處理程序的示例。
void CMyCustomDrawControl::OnCustomDraw(NMHDR* pNMHDR, LRESULT*pResult)
{
LPNMCUSTOMDRAW pNMCD =reinterpret_cast(pNMHDR);
...
}
正如您所見(jiàn),NM_CUSTOMDRAW 處理程序?qū)⒁粋€(gè)指針傳遞給 NMHDR 類(lèi)型的結(jié)構(gòu)。然而,該值不足以用于象 NMHDR 這樣只包含三個(gè)成員(hwndFrom、idFrom 和 code)的結(jié)構(gòu)。
因此,您通常需要將該結(jié)構(gòu)指針轉(zhuǎn)換為信息量更大的結(jié)構(gòu) — LPNMCUSTOMDRAW。LPNMCUSTOMDRAW指向 NMCUSTOMDRAW,它包含諸如 dwDrawStage、dwItemSpec 和 uItemState 這樣的成員 — 它們是決定當(dāng)前繪制階段及確切繪制(例如,控件本身、或控件的一個(gè)項(xiàng)目或子項(xiàng))所必需的。
這里值得注意的是,還可以將 NMHDR 指針指向特定于正在繪制控件的類(lèi)型的結(jié)構(gòu)。表 1 顯示控件的一個(gè)列表及其相關(guān)的自定義繪制結(jié)構(gòu)類(lèi)型名。
表 1:控件及其相關(guān)的自定義繪制結(jié)構(gòu)
控件 | 結(jié)構(gòu)(在 commctrl.h 中定義) |
Rebar、Trackbar、AuthTicket、My.Resources、My.Settings、My.User 和 My.WebServices。 | NMCUSTOMDRAW |
List-view | NMLVCUSTOMDRAW |
Toolbar | NMTBCUSTOMDRAW |
Tooltip | NMTTCUSTOMDRAW |
Tree-view | NMTVCUSTOMDRAW |
2,指定處理所需的繪制階段
正如我在前面提到的,繪制一個(gè)控件存在一些“階段”。特別是,您可以將繪制過(guò)程理解為一系列階段,其中控件通知其父窗口需要繪制的內(nèi)容。事實(shí)上,控件甚至?xí)诶L制控件及其各項(xiàng)前后發(fā)送一個(gè)通知,從而讓編程人員更好地控制該過(guò)程。
在所有情況下,單一的 NM_CUSTOMDRAW 處理程序在每個(gè)繪制階段都進(jìn)行調(diào)用。然而,謹(jǐn)記:自定義繪制允許您在自己的繪制中合并默認(rèn)的控件繪制,您需要指定您將處理哪個(gè)繪制階段。這通過(guò)設(shè)置 NM_CUSTOMDRAW 處理程序的第二個(gè)參數(shù) (pResult) 完成。事實(shí)上,如果您從未設(shè)置該值,則用初始階段的 CDDS_PREPAINT 調(diào)用函數(shù)后,您的函數(shù)將不再被調(diào)用!
從技術(shù)上講,只有兩個(gè)階段指定需要的繪制階段(CDDS_PREPAINT 和 CDDS_ITEMPREPAINT),它們影響發(fā)送通知消息的內(nèi)容。然而,通常只在處理程序的最后指定代碼將處理的繪制階段。表 2 列出用于指定所需繪制階段(代碼關(guān)注的)的值。
表 2:自定義繪制返回標(biāo)志
自定義繪制返回標(biāo)志 | 含義 |
CDRF_DEFAULT | 指示控件自行繪制。該值為默認(rèn)值,不應(yīng)該將它與其他值組合在一起。 |
CDRF_SKIPDEFAULT | 用于指定控件根本不進(jìn)行任何繪制。 |
CDRF_NEWFONT | 當(dāng)代碼更改繪制項(xiàng)/子項(xiàng)的字體時(shí)使用。 |
CDRF_NOTIFYPOSTPAINT | 使通知信息在控件或每個(gè)項(xiàng)/子項(xiàng)繪制后發(fā)送。 |
CDRF_NOTIFYITEMDRAW | 指出項(xiàng)(或子項(xiàng))將進(jìn)行繪制。注意,它下面的值與 CDRF_NOTIFYSUBITEMDRAW 相同。 |
CDRF_NOTIFYSUBITEMDRAW | 指出子項(xiàng)(或項(xiàng))將進(jìn)行繪制。注意,它下面的值與 CDRF_NOTIFYITEMDRAW 相同。 |
CDRF_NOTIFYPOSTERASE | 當(dāng)刪除控件后需要通知代碼時(shí)使用。 |
以下為一個(gè)示例,其中的代碼指定,當(dāng)繪制控件的項(xiàng) (CDRF_NOTIFYITEMDRAW) 及子項(xiàng)(CDRF_NOTIFYPOSTPAINT),以及繪制完成時(shí),應(yīng)該調(diào)用 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;
}
3,篩選指定的繪制階段
一旦指定要關(guān)注的階段后,您需要處理這些階段。因?yàn)槔L制過(guò)程的每個(gè)階段只有一個(gè)消息要發(fā)送,慣例是執(zhí)行一個(gè) switch 語(yǔ)句以決定準(zhǔn)確的繪制階段。不同的繪制階段由以下標(biāo)志定義:
CDDS_PREPAINT
CDDS_ITEM
CDDS_ITEMPREPAINT
CDDS_ITEMPOSTPAINT
CDDS_ITEMPREERASE
CDDS_ITEMPOSTERASE
CDDS_SUBITEM
CDDS_POSTPAINT
CDDS_PREERASE
CDDS_POSTERASE
對(duì)于一個(gè) CListCtrl 派生的類(lèi),有一個(gè) NM_CUSTOMDRAW 處理程序的示例,其中您可以發(fā)現(xiàn),代碼決定當(dāng)前繪制階段的方式:
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;
}
注意,為了決定子項(xiàng)(例如,列表視圖控件)繪制的階段,您必需使用按位 or 操作符,它有兩個(gè)值:其中一個(gè)為 CDDS_ITEMPREPAINT 或者CDDS_ITEMPOSTPAINT,另一個(gè)為 CDDS_SUBITEM。
要說(shuō)明它,我們假定您想在繪制列表視圖項(xiàng)之前進(jìn)行一些處理。將編寫(xiě) switch 語(yǔ)句來(lái)處理 CDDS_ITEMPREPAINT。
case CDDS_ITEMPREPAINT:
...
break;
然而,如果是您所關(guān)注子項(xiàng)的預(yù)繪制階段,則將如下操作:
case CDDS_ITEMPREPAINT | CDDS_SUBITEM:
...
break;
示例:創(chuàng)建一個(gè)列表視圖控件自定義繪制控件
如前面提到的,您可以完全控制控件及其項(xiàng)的繪制,或者僅執(zhí)行一小部分特定于應(yīng)用程序的繪制,并讓控件繼續(xù)進(jìn)行。本文的焦點(diǎn)更多地偏重于控件繪制技術(shù)而非高級(jí)的繪制技術(shù),我們將演練一個(gè)簡(jiǎn)單的示例,其中列表視圖控件是一個(gè)自定義的繪制,因此項(xiàng)的文本將在創(chuàng)建拼接外觀的交替單元中顯示為不同的顏色。
·創(chuàng)建一個(gè)基于 Visual C++ 2005 對(duì)話框的項(xiàng)目,名為L(zhǎng)istCtrlColor。
·從 Class View 中選擇 Project 菜單選項(xiàng),并單擊 Add Class 調(diào)用 Add Class 對(duì)話框。
·從分類(lèi)列表中選擇 MFC,然后從模板列表中選擇 MFC Class。
·單擊 Add 按鈕,調(diào)用 MFC Class Wizard 對(duì)話框。
·對(duì)于 Class name,鍵入值 CListCtrlWithCustomDraw 并選擇 CListCtrl的 Base class。
·單擊 Finish 按鈕,生成類(lèi)的標(biāo)頭和執(zhí)行文件。
·對(duì)于 Class View,右鍵單擊 CListCtrlWithCustomDraw 類(lèi),并選擇Properties 上下文菜單選項(xiàng)。
·顯示 Properties 窗口時(shí),單擊頂部的 Messages 按鈕,顯示一個(gè)兩列的消息列表,您可以為其實(shí)現(xiàn)處理程序。
·在消息列表中單擊 NM_CUSTOMDRAW 項(xiàng),然后下拉第二列的組合框箭頭,并選擇值 OnNMCustomdraw。
·現(xiàn)在,處理繪制代碼。這里,我們只簡(jiǎn)單處理項(xiàng)和子項(xiàng)預(yù)繪制階段,指定基于當(dāng)前行(項(xiàng))和列(子項(xiàng))的文本和背景色。要進(jìn)行此操作,按如下所示修改 OnNMCustomdraw 函數(shù):
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;
}
現(xiàn)在,我們來(lái)測(cè)試新控件。要進(jìn)行此操作,您只需使用 CListCtrlWithCustomDraw 類(lèi)將列表視圖控件放在對(duì)話框中,并對(duì)其進(jìn)行子類(lèi)派生。下面是完成該操作的步驟。
·在 Resource 視圖中,打開(kāi)應(yīng)用程序的主對(duì)話框 (IDD_LISTCTRLCOLOR_DIALOG)。
·從 Toolbox 中,將一個(gè) List Control 拖放到該對(duì)話框。
·右鍵單擊列表控件,并選擇 Properties 上下文菜單選項(xiàng)。
·將 View 屬性設(shè)置為 Report。
·右鍵單擊控件,并選擇 Add Variable 上下文菜單選項(xiàng)。
·出現(xiàn) Add Member Variable Wizard 對(duì)話框時(shí),指定m_lstBooks 的 Variable name,并單擊 Finish 按鈕。
·這時(shí),您就有了一個(gè) CListCtrl 派生類(lèi) (m_lstBooks),它將對(duì)話框上的列表視圖控件進(jìn)行子類(lèi)派生。然而,m_lstBooks 需要從最新創(chuàng)建的 CListCtrlWithCustomDraw 派生,以便于調(diào)用您的繪制代碼。因此,打開(kāi)對(duì)話框的標(biāo)題文件 (ListCtrlColorDlg.h),將 m_lstBooks 更改為 CListCtrlWithCustomDraw 類(lèi)型。
·在 CListCtrlColorDlg 類(lèi)開(kāi)始之前,添加以下指令。
#include"ListCtrlWithCustomDraw.h"
·將下面的代碼添加到對(duì)話框的 OnInitDialog 成員函數(shù),這樣我們就能夠看到一些列表視圖行。
// 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);
}
·現(xiàn)在,建立并運(yùn)行應(yīng)用程序。圖 1 為應(yīng)用程序外觀的一個(gè)示例。

圖 1. 自定義繪制示例應(yīng)用程序
小結(jié)
當(dāng) Windows 首次作為“下一代”操作系統(tǒng)引入到應(yīng)用程序開(kāi)發(fā)之中時(shí),它作為新圖形用戶(hù)界面的一個(gè)主要論據(jù)就是其一致性。該論據(jù)的要點(diǎn)所在是其具有一個(gè)通用的外觀:統(tǒng)一的菜單項(xiàng)、通用控件等。這一通用性的感覺(jué)可能會(huì)一直延續(xù),直到有第二家公司想設(shè)計(jì)其自己的應(yīng)用程序。簡(jiǎn)單說(shuō),提供外觀與其他應(yīng)用程序雷同的應(yīng)用程序,任何公司都不會(huì)逃離這一怪圈。
要建立一個(gè)唯一的且讓人過(guò)目難忘的用戶(hù)界面,其中一種方式是為應(yīng)用程序設(shè)計(jì)并開(kāi)發(fā)自定義的控件。希望本文能對(duì)您有所幫助,現(xiàn)在,您了解到一種非常強(qiáng)大的技術(shù),它使您的應(yīng)用程序能從眾多競(jìng)爭(zhēng)對(duì)手的應(yīng)用程序中脫穎而出。
OWNER DRAW實(shí)現(xiàn)自繪按鈕
一、準(zhǔn)備工作
在開(kāi)始編碼之前,首先應(yīng)該確定好,更準(zhǔn)確的說(shuō)應(yīng)該是設(shè)計(jì)好按鈕在各種狀態(tài)下的外觀。按鈕控件的幾中基本狀態(tài)包括:
Normal狀態(tài),就是按鈕一開(kāi)始顯示時(shí)的樣子。
Over狀態(tài),鼠標(biāo)指針移動(dòng)到按鈕上面時(shí)按鈕顯示的樣子。
Down狀態(tài),按下按鈕時(shí)顯示的樣子。
Focus狀態(tài),按鈕按下后松開(kāi)的樣子,例如標(biāo)準(zhǔn)按鈕按下松開(kāi)之后會(huì)看到按鈕內(nèi)部有一個(gè)虛線框。
Disable狀態(tài),當(dāng)然就是按鈕被設(shè)置成無(wú)效的時(shí)候的樣子啦。
我參考了一下WindowsXP中普通按鈕的實(shí)際樣子,設(shè)計(jì)出XP按鈕各種狀態(tài)的外觀,如下圖所示:

至于Down狀態(tài)主要是在Over狀態(tài)的基礎(chǔ)上將文字往右下的方向稍微平移,以實(shí)現(xiàn)下壓的效果。
二、實(shí)現(xiàn)原理及難點(diǎn)
下面我們開(kāi)始類(lèi)的創(chuàng)建,在Workspace的ClassView頁(yè)中右擊列表樹(shù)的根結(jié)點(diǎn),選擇New Class…

在彈出窗口中進(jìn)行派生類(lèi)的定義,如下圖所示,注意,你需要填寫(xiě)的只有Name和Base class兩項(xiàng),其余的選項(xiàng)保持默認(rèn)值就可以了。

下面簡(jiǎn)要敘述一下按鈕的實(shí)現(xiàn)原理:
1. 在控件初始化時(shí)為按鈕添加Owner Draw的屬性。這是因?yàn)?span style="color: rgb(255, 0, 0);">在MFC中,要想激活控件的自繪功能,要求該控件的屬性中必須包含屬性值BS_OWNERDRAW,這一步我們可以通過(guò)類(lèi)向?qū)?CXPButton類(lèi)添加PreSubclassWindow()函數(shù),在該函數(shù)中完成屬性值的設(shè)置。當(dāng)激活控件的自繪功能之后,每次控件狀態(tài)改變的時(shí)候都會(huì)運(yùn)行函數(shù)DrawItem(),該函數(shù)的作用就是繪制控件在各種狀態(tài)下的外觀。
2. 添加WM_MOUSELEAVE消息函數(shù),當(dāng)鼠標(biāo)指針離開(kāi)按鈕時(shí),觸發(fā)該消息函數(shù),我們?cè)诤瘮?shù)中添加代碼,通知DrawItem函數(shù)鼠標(biāo)指針已經(jīng)離開(kāi)了,讓按鈕重繪。
3. 添加WM_MOUSEHOVER消息函數(shù),當(dāng)鼠標(biāo)指針位于按鈕之上時(shí),觸發(fā)該消息函數(shù),我們?cè)诤瘮?shù)重添加代碼,通知DrawItem函數(shù)鼠標(biāo)指針現(xiàn)在正在按鈕的上面,讓按鈕重繪。
4. 添加DrawItem函數(shù)。在DrawItem中根據(jù)按鈕當(dāng)前的狀態(tài)繪制按鈕的外觀??梢哉f(shuō)自繪控件的大部分功能都是在這個(gè)函數(shù)中實(shí)現(xiàn)的。DrawItem函數(shù)包含了一個(gè)LPDRAWITEMSTRUCT的指針,本篇會(huì)在稍后予以講解。
這里有兩個(gè)難點(diǎn),首先是WM_MOUSELEAVE和 WM_MOUSEHOVER不是標(biāo)準(zhǔn)的Windows消息函數(shù),它們不能通過(guò)類(lèi)向?qū)?lái)添加,所有的添加工作都需要通過(guò)手工輸入代碼來(lái)完成。另一個(gè)難點(diǎn)是 DrawItem中的LPDRAWITEMSTRUCT指針,它指向了一個(gè)DRAWITEMSTRUCT的結(jié)構(gòu),這個(gè)結(jié)構(gòu)中包含了控件的各種細(xì)節(jié),為我們提供了實(shí)現(xiàn)自繪功能的必要信息。
難點(diǎn)一:
事實(shí)上WM_MOUSELEAVE和WM_MOUSEHOVER兩個(gè)Windows消息是通過(guò)WM_MOUSEMOVE消息觸發(fā)的,而 WM_MOUSEMOVE是標(biāo)準(zhǔn)的Windows消息,因此我們可以通過(guò)類(lèi)向?qū)?lái)為CXPButton類(lèi)添加WM_MOUSEMOVE消息函數(shù)。

函數(shù)的代碼見(jiàn)如下,這段代碼非常有用,在其它的自繪控件中,如果想觸發(fā)WM_MOUSELEAVE和WM_MOUSEHOVER消息,也是使用類(lèi)似的方法實(shí)現(xiàn)的。
voidCXPButton::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: Add your message handler codehere and/or call default
if (!m_bTracking)
{
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = m_hWnd;
tme.dwFlags = TME_LEAVE |TME_HOVER;
tme.dwHoverTime = 1;
m_bTracking = _TrackMouseEvent(&tme);
}
CButton::OnMouseMove(nFlags, point);
}
我們接著添加WM_MOUSELEAVE和WM_MOUSEHOVER消息消息函數(shù)。在CXPButton類(lèi)的聲明中(即在 XPButton.h文件中)找到afx_msg void OnMouseMove(UINT nFlags, CPoint point);的函數(shù)聲明,緊接其下輸入
afx_msgLRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam);
afx_msgLRESULT OnMouseHover(WPARAM wParam, LPARAM lParam);
然后在XPButton.cpp文件中找到ON_WM_MOUSEMOVE(),緊接其后輸入
ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave)
ON_MESSAGE(WM_MOUSEHOVER,OnMouseHover)
難點(diǎn)二:
下面我們看看DRAWITEMSTRUCE結(jié)構(gòu)為我們提供了哪些有用信息呢?
DRAWITEMSTRUCT結(jié)構(gòu)的定義如下:
typedefstruct tagDRAWITEMSTRUCT
{
UINT CtlType; //控件類(lèi)型
UINT CtlID; //控件ID
UINT itemID; //菜單項(xiàng)、列表框或組合框中某一項(xiàng)的索引值
UINT itemAction; //控件行為
UINT itemState; //控件狀態(tài)
HWND hwndItem; //父窗口句柄或菜單句柄
HDC hDC; //控件對(duì)應(yīng)的繪圖設(shè)備句柄
RECT rcItem; //控件所占據(jù)的矩形區(qū)域
DWORD itemData; //列表框或組合框中某一項(xiàng)的值
}DRAWITEMSTRUCT, *PDRAWITEMSTRUCT, *LPDRAWITEMSTRUCT;
其實(shí)不僅是按鈕控件,其它控件,如ComboBox、ListBox、StaticText等都是通過(guò)DRAWITEMSTRUCT來(lái)記錄控件信息的。關(guān)于這個(gè)結(jié)構(gòu)的詳細(xì)文檔可參考本篇的附錄。
也許你早已看到許多自繪按鈕的例子,實(shí)際上自繪按鈕本身的函數(shù)結(jié)構(gòu)都是差不多的,它們顯示效果的區(qū)別主要取決于代碼編寫(xiě)者對(duì)GDI作圖函數(shù)的運(yùn)用與掌握程度。有興趣的朋友可以研究一下CXPButton類(lèi)中DrawItem函數(shù)的數(shù)據(jù)結(jié)構(gòu),其實(shí)只要修改一下其中GDI繪圖函數(shù)的部分代碼,馬上又能做出另一個(gè)自繪按鈕控件了。
三、按鈕類(lèi)的使用
下面演示CXPButton類(lèi)的使用。往對(duì)話框中添加一個(gè)按鈕控件,假設(shè)它的ID值為IDC_BUTTON1。進(jìn)入類(lèi)向?qū)В–lass Wizard)的Member Variables屬性頁(yè),為IDC_BUTTON1添加一個(gè)變量m_btnNormal。確定退出后再進(jìn)行編譯,就可以看到重新定義過(guò)XP風(fēng)格按鈕了。

如果你是之間把CXPButton的源文件引入自己的工程中的,那么在上圖的Variable type中是看不到CXPButton選項(xiàng)的。但是可以通過(guò)以下方法加入:
1. 首先保存工程后退出。
2. 在工程的目錄下找到一個(gè)后綴名為.clw的文件,將其刪除。但是為了以防萬(wàn)一還是建議你實(shí)現(xiàn)備份一下。
3. 重新打開(kāi)工程,進(jìn)入類(lèi)向?qū)?,此時(shí)會(huì)看到一下一個(gè)彈出對(duì)話框,我們選擇“是(Yes)”。

4. 再選擇“Add All”,這樣我們就可以在類(lèi)向?qū)е惺褂肅XPButton的變量類(lèi)型了。
四、小結(jié)與提示
對(duì)于按鈕來(lái)說(shuō),當(dāng)按鈕上面任何可見(jiàn)的部分發(fā)生變換的時(shí)候,都要調(diào)用DrawItem函數(shù)進(jìn)行重繪。自繪制按鈕必須設(shè)定BS_OWNERDRAW的屬性,設(shè)置的代碼在PreSubclassWindows函數(shù)中完成。另外為了防止系統(tǒng)字體設(shè)置的變化影響控件的表達(dá)效果,還可以在該函數(shù)中為控件指定某種固定的字體。但是要注意的是這個(gè)
讓我們來(lái)回顧一下實(shí)現(xiàn)自繪按鈕的基本步驟:
a. 確定設(shè)計(jì)方案;
b. 初始化,但是記得要在函數(shù)退出前恢復(fù)先前的GDI對(duì)象,并釋放所占領(lǐng)的資源;
c. 添加相應(yīng)消息函數(shù);
d. 添加繪圖函數(shù)DrawItem,在DrawItem中作圖的順序一般是先畫(huà)外邊框,再上底色,接著寫(xiě)文字,最后是畫(huà)內(nèi)邊框。不過(guò)有些人也喜歡把邊框放到最后畫(huà),這問(wèn)題不大。
五、附錄
DRAWITEMSTRUCT結(jié)構(gòu)文檔 (根據(jù)Msdn翻譯)
DRAWITEMSTRUCT
DRAWITEMSTRUCT為需要自繪的控件或者菜單項(xiàng)提供了必要的信息。在需要繪制的控件或者菜單項(xiàng)對(duì)應(yīng)的WM_DRAWITEM消息函數(shù)中得到一個(gè)指向該結(jié)構(gòu)的指針。 DRAWITEMSTRUCT結(jié)構(gòu)的定義如下:
typedef struct tagDRAWITEMSTRUCT
{
UINTCtlType ;
UINTCtlID ;
UINTitemID ;
UINTitemAction ;
UINTitemState ;
HWNDhwndItem ;
HDC hDC;
RECTrcItem ;
ULONG_PTRitemData ;
} DRAWITEMSTRUCT;
結(jié)構(gòu)成員:
CtlType
指定了控件的類(lèi)型,其取值如下表所示。
取值 | 描述 |
ODT_STATIC | 靜態(tài)文本控件 |
ODT_BUTTON | 按鈕控件 |
ODT_COMBOBOX | 組合框控件 |
ODT_LISTBOX | 列表框控件 |
ODT_LISTVIEW | 列表視圖控件 |
ODT_MENU | 菜單項(xiàng) |
ODT_TAB | Tab控件 |
CtlID
指定了自繪控件的ID值,而對(duì)于菜單項(xiàng)則不需要使用該成員
itemID
表示菜單項(xiàng)ID,也可以表示列表框或者組合框中某項(xiàng)的索引值。對(duì)于一個(gè)空的列表框或組合框,該成員的值為–1。這時(shí)應(yīng)用程序只繪制焦點(diǎn)矩形(該矩形的坐標(biāo)由rcItem 成員給出)雖然此時(shí)控件中沒(méi)有需要顯示的項(xiàng),但是繪制焦點(diǎn)矩形還是很有必要的,因?yàn)檫@樣做能夠提示用戶(hù)該控件是否具有輸入焦點(diǎn)。當(dāng)然也可以設(shè)置itemAction 成員為合適值,使得無(wú)需繪制焦點(diǎn)。
itemAction
指定繪制行為,其取值可以為下表中所示值的一個(gè)或者多個(gè)的聯(lián)合。
取值 | 描述 |
ODA_DRAWENTIRE | 當(dāng)整個(gè)控件都需要被繪制時(shí),設(shè)置該值 |
ODA_FOCUS | 如果控件需要在獲得或失去焦點(diǎn)時(shí)被繪制,則設(shè)置該值。此時(shí)應(yīng)該檢查itemState成員,以確定控件是否具有輸入焦點(diǎn)。 |
ODA_SELECT | 如果控件需要在選中狀態(tài)改變時(shí)被繪制,則設(shè)置該值。此時(shí)應(yīng)該檢查itemState 成員,以確定控件是否處于選中狀態(tài)。 |
itemState
指定了當(dāng)前繪制操作完成后,所繪項(xiàng)的可見(jiàn)狀態(tài)。例如,如果菜單項(xiàng)應(yīng)該被灰色顯示,則可以指定ODS_GRAYED狀態(tài)標(biāo)志。其取值可以為下表中所示值的一個(gè)或者多個(gè)的聯(lián)合。
取值 | 描述 |
ODS_CHECKED | 如果菜單項(xiàng)將被選中,則可設(shè)置該值。該值只對(duì)菜單項(xiàng)有用。 |
ODS_COMBOBOXEDIT | 在自繪組合框控件中只繪制選擇區(qū)域。 |
ODS_DEFAULT | 默認(rèn)值。 |
ODS_DISABLED | 如果控件將被禁止,則設(shè)置該值。 |
ODS_FOCUS | 如果控件需要輸入焦點(diǎn),則設(shè)置該值。 |
ODS_GRAYED | 如果控件需要被灰色顯示,則設(shè)置該值。該值只在繪制菜單時(shí)使用。 |
ODS_HOTLIGHT | Windows 98/Me, Windows 2000/XP: 如果鼠標(biāo)指針位于控件之上,則設(shè)置該值,這時(shí)控件會(huì)顯示高亮顏色。 |
ODS_INACTIVE | Windows 98/Me, Windows 2000/XP: 表示沒(méi)有激活的菜單項(xiàng)。 |
ODS_NOACCEL | Windows 2000/XP: 控件是否有快速鍵盤(pán)。 |
ODS_NOFOCUSRECT | Windows 2000/XP: 不繪制捕獲焦點(diǎn)的效果。 |
ODS_SELECTED | 選中的菜單項(xiàng)。 |
hwndItem
指定了組合框、列表框和按鈕等自繪控件的窗口句柄;如果自繪的對(duì)象時(shí)菜單項(xiàng),則表示包含該菜單項(xiàng)的菜單句柄。
hDC
指定了繪制操作所使用的設(shè)備環(huán)境。
rcItem
指定了將被繪制的矩形區(qū)域。這個(gè)矩形區(qū)域就是上面hDC的作用范圍。系統(tǒng)會(huì)自動(dòng)裁剪組合框、列表框或按鈕等控件的自繪制區(qū)域以外的部分。也就是說(shuō) rcItem中的坐標(biāo)點(diǎn)(0,0)指的就是控件的左上角。但是系統(tǒng)不裁剪菜單項(xiàng),所以在繪制菜單項(xiàng)的時(shí)候,必須先通過(guò)一定的換算得到該菜單項(xiàng)的位置,以保證繪制操作在我們希望的區(qū)域中進(jìn)行。
itemData
對(duì)于菜單項(xiàng),該成員的取值可以是由
CMenu::AppendMenu、
CMenu::InsertMenu或者
CMenu::ModifyMenu
等函數(shù)傳遞給菜單的值。
對(duì)于列表框或這組合框,該成員的值可以為由
ComboBox::AddString、
CComboBox::InsertString、
CListBox::AddString或者
CListBox::InsertString
等傳遞給控件的值。
如果ctlType 的取值是ODT_BUTTON或者ODT_STATIC, itemData的取值為0。