實現自訂繪製的三步曲
既然您已經瞭解了繪製控制項可用的各種選項(包括使用自訂繪製的好處),那麼,讓我們來看看實現一個自訂繪製控制項需要的三個主要步驟。
執行一個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 顯示控制項的一個列表及其相關的自訂繪製結構類型名。
表 1:控制項及其相關的自訂繪製結構
控制項 |
結構(在 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 |
當刪除控制項後需要通知代碼時使用。 |
以下為一個樣本,其中的代碼指定,當繪製控制項的項 (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_PREPAINTCDDS_ITEMCDDS_ITEMPREPAINTCDDS_ITEMPOSTPAINTCDDS_ITEMPREERASECDDS_ITEMPOSTERASECDDS_SUBITEMCDDS_POSTPAINTCDDS_PREERASECDDS_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 columnsm_lstBooks.InsertColumn(0, _T("Author"));m_lstBooks.InsertColumn(1, _T("Book"));// Define the datastatic 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 dataint 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 首次作為“下一代”作業系統引入到應用程式開發之中時,它作為新圖形化使用者介面的一個主要論據就是其一致性。該論據的要點所在是其具有一個通用的外觀:統一的功能表項目、通用控制項等。這一通用性的感覺可能會一直延續,直到有第二家公司想設計其自己的應用程式。簡單說,提供外觀與其他應用程式雷同的應用程式,任何公司都不會逃離這一怪圈。
要建立一個唯一的且讓人過目難忘的使用者介面,其中一種方式是為應用程式設計並開發自訂的控制項。希望本文能對您有所協助,現在,您瞭解到一種非常強大的技術,它使您的應用程式能從眾多競爭者的應用程式中脫穎而出。