一.思路:
Windows 為控件提供了自畫(owner draw)的能力,程序員可以通過這一機制實現非常酷的控件外觀。WTL(Windows Template Library)提供了一個CownerDraw模板,用來對控件的自畫操作提供支持。
COwnerDraw 的聲明為如下形式:
template <class T>
class CownerDraw
{
……
};
從上面的代碼可以看出,它沒有從任何基類或模板派生,它並不是一個窗口類。它只為參數T(T必須是一個支持自畫的控件類)提供自畫支持。除了自畫以外,我們也許還想讓按鈕具有ToolTip功能,或者看起來象一個位圖按鈕,最好還能在位圖的背景下顯示文字,或者上面顯示位圖下面顯示文字。這些功能我們都可以通過自畫操作來實現,但是那樣會很麻煩,利用WTL提供的CbitmapButtonImpl模板,我們只需要簡單地繼承再加上自畫能力就可以實現上述功能。現在看一看自畫按鈕的聲明:
class CownerDrawButton : public CbitmapButtonImpl<CownerDrawButton>,
public CownerDraw<CownerDrawButton>
……
它采用多繼承的方式從兩個模板派生,從而不但具有了自畫的能力,而且也是一個位圖按鈕。
二、COwnerDraw 模板
CownerDraw模板提供了一組消息映射宏和相應的響應函數。如:
BEGIN_MSG_MAP(COwnerDraw< T >)
MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
……
ALT_MSG_MAP(1)
MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
……
END_MSG_MAP()
因為CownerDrawButton從兩個基類派生,在響應WM_DRAWITEM消息時,它需要把消息鏈接到CownerDraw模板,在響應其它消息(如WM_CREATE)時,需要把消息鏈接到CBitmapButtonImpl模板。ATL(注意是ATL而不WTL,WTL構建在ATL之上)不知道哪一個消息要對應到哪個父類的處理函數,如果兩個父類都響應相同的消息,那麼崎義就會產生,所以這一操作必須由程序員來完成。
使用基類鏈接(base class chaining)機制用CHAIN_MSG_MAP宏,雖然可以將消息導向父類,但是如果父類派生了多個子類,而每個子類對相同的消息又有不同的處理要求時, CHAIN_MSG_MAP就無能為力了。所以ATL又提供了另一個機制:消息分割(Alternate message maps),消息分割可以在父類的消息映射中,將相同的消息分割放置在不同的區域,CownerDraw模板就是采用了這一機制。
在CownerDraw模板的消息映射表裡,消息映射被分為兩個區域,0號和1號區域。子類要鏈接到不同區域,需要使用CHAIN_MSG_MAP_ALT宏。CownerDrawButton需要響應CownerDraw的1號區域中的OCM_DRAWITEM消息,就可以在它自己的消息映射表中加入這樣一條宏:CHAIN_MSG_MAP_ALT(COwnerDraw<CownerDrawButton>,1)
而其余的消息,它希望由CbitmapButtonImpl模板來處理,仍然可以使用CHAIN_MSG_MAP做基類鏈接(後面我會提到,實際上不能用CHAIN_MSG_MAP簡單地做基類鏈接)。
CownerDraw模板的這兩個消息映射區域唯一的不同是1號區域的消息是以OCM開頭的。這就涉及到了ATL的消息反射(Message Reflection)機制。所謂消息反射,就是指窗口類在收到消息時可以將消息反傳回去給發出消息的窗口類。比如對於一個自畫樣式的按鈕,它會發出WM_DRAWITEM消息通知父窗口,而父窗口並不處理這個消息而是將它反傳回去,讓按鈕自己處理。顯而易見,這種機制更符合面向對象的要求,減少了按鈕和父窗口之間的依賴關系。
被父窗口返回的消息代號都是以OCM開頭,當我們在父窗口的消息映射表中加入一條REFLECT_NOTIFICATIONS()宏時,父窗口就能夠將支持消息反射的控件所發出的消息反傳回去,如果控件類或其父類(前提是已經做了基類鏈接)的消息映射表中有相應消息的反射處理宏,那麼控件就會在自己或父類的消息響應函數中處理這條消息。下面讓我們來看一看消息分割及反射的具體實現方法。首先在CownerDrawButton的消息映射表中加入如下宏:
CHAIN_MSG_MAP_ALT(COwnerDraw<CownerDrawButton>,1)
然後在框架類的消息映射表中加入REFLECT_NOTIFICATIONS()宏,這樣就完成了消息映射。但是需要注意的是,REFLECT_NOTIFICATIONS必須放在消息映射表的最後,否則所有通知消息都將被返回,窗口本身得不任何通知消息,如果你在REFLECT_NOTIFICATIONS宏後面添加一條COMMAND_HANDLER(IDC_BUTTON1, BN_CLICKED, OnClickedButton1) ,那麼OnClickedButton1是永遠也不會被觸發的。當按鈕發出WM_DRAWITEM消息時,框架類接到後,先檢查自己的消息映射表裡是否有相對應的消息處理函數,如果沒有那麼REFLECT_NOTIFICATIONS就將消息反回給按鈕,按鈕在消息映射表中找到MESSAGE_HANDLER(OCM_DRAWITEM,OnDrawItem)這一項,宏會將消息映射到OnDrawItem函數,通過調用OnDrawItem函數,完成繪制工作。CownerDraw模板已經為我們實現了OnDrawItem函數,這個函數很簡單,代碼如下:
LRESULT OnDrawItem(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& bHandled)
{
T* pT = static_cast<T*>(this);
pT->SetMsgHandled(TRUE);
pT->DrawItem((LPDRAWITEMSTRUCT)lParam);
bHandled = pT->IsMsgHandled();
return (LRESULT)TRUE;
}
OnDrawItem函數通過static_cast運算符(靜態強制轉換)將基類指針轉換到派生類指針,然後調用派生的成員函數DrawItem來完成繪制任務。DrawItem是實現自畫的關鍵所在,CownerDraw並沒有提供DrawItem的實現,因為它沒有辦法知道派生類的具體繪制要求,所以DrawItem必須由派生類去實現。CownerDraw模板只提供了一個接口,如果你在派生類中不提供DrawItem的實現,那麼在調試的時候,將引發一ATL assert。
三、CBitmapButtonImpl模板
CbitmapButtonImpl為位圖按鈕提供了支持,使我們不必了解太多實現細節,就可以做漂亮的位圖按鈕。它還提供了對ToolTip的支持。我們還可以通過重載DoPaint函數來實現個性化。CbitmapButtonImpl定義了一個重要的成員變量m_ImageList,這個成員主要用於位圖或圖標的管理和繪制。我們將在例程中看到它的使用方法。
四、COwnerDrawButton 類
前面曾經說過,CownerDrawButton類可以通過基類鏈接的機制,將消息導向其基類,但是如果簡單地使用CHAIN_MSG_MAP(CbitmapButtonImpl<CownerDrawButton>) 宏,就會出現問題。因為我們實現是的“自畫”按鈕,所有的繪制工作都應該在DrawItem函數裡完成,但是CbitmapButtonImpl並不知道這種情況,所以它仍然響應WM_PAINT、WM_PRINTCLIENT 和WM_ERASEBKGND以及其它有關繪制操作的消息,並調用DoPaint等函數進行繪制工作,可想而知這會造成極大的混亂。因此我們必須屏蔽掉CownerDrawButton對這些消息的響應。拷貝CbitmapButtonImpl的所有消息映射表項到CownerDrawButton的消息消息映射表中,然後刪除這三行:
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MESSAGE_HANDLER(WM_PRINTCLIENT, OnPaint)
再添加CHAIN_MSG_MAP_ALT(COwnerDraw<COwnerDrawButton>,1)這一項,這樣窗口默認的繪制消息就不會被觸發,問題也得到了解決。但是不要忘記,CownerDrawButton也是一個位圖按鈕,而CbitmapButtonImpl是在DoPaint函數中實現位圖的顯示,如果不響應WM_PAINT或者WM_PAINTCLIENT消息,DoPaint是不會被調用的。顯然如果CownerDrawButton的m_ImageList成員包含圖片的話,我們就需要自己在DrawItem函數裡實現位圖的顯示,當然我們也可以在需要顯示位圖的時候簡單地調用DoPaint函數,只要為它傳遞一個CDCHandle,DoPaint就會非常好地完成任務,實際上我就是這麼做的。不過要想實現圖1所顯示的按鈕,CbitmapButtonImpl提供的DoPaint函數是沒有辦法辦到的。為CownerDrawButton聲明一個成員變量m_uBmpPosStyle,當m_ImageList包含圖像時,這個變量就被設置,用於存儲圖像的具體位置。圖像的位置被聲明為五個無符號的整型常量如下所示:
unsigned int const IMAGEPOS_TOP = 1 ;
unsigned int const IMAGEPOS_BOTTOM = 2 ;
unsigned int const IMAGEPOS_LEFT = 3 ;
unsigned int const IMAGEPOS_RIGHT = 4 ;
unsigned int const IMAGEPOS_CENTER = 5 ;
只有當m_ImageList包含圖像,圖像的擴展樣式不是自動尺寸(BMPBTN_AUTOSIZE),並且圖像尺寸小於按鈕客戶區域時,這些樣式才有效。還要聲明一個CRect類型的成員變量m_ClientRect,用於記錄客戶區域的尺寸,每當我們顯示完位圖之後,就對m_ClientRect區域進行裁剪,以便於文本的排布。為了應用這些樣式,以及對m_ClientRect成員進行修改,必須對DoPaint進行重載。把CbitmapButtonImpl模板的DoPaint源碼拷貝到CownerDrawButton的DoPaint中,然後此基礎上進行修改。修改後的DoPaint函數對位圖尺寸和客戶區域進行比較,如果圖片尺寸小於客戶區域,則再根據m_uBmpPosStyle設置的樣式繪制位圖,最後對m_ClIEntRect進行裁剪,以便於文本布局。在DrawItem函數中,通過對m_ImageList 是否包含圖片及是否設置主圖進行判斷,來決定是否調用DoPaint進行圖片的顯示。如果沒有圖片則執行缺省的繪制,並在客戶區域中央顯示文本。
為了便於位圖資源的導入,CownerDrawButton提供了一個LoadImageFromID函數,原型為:
BOOL LoadImageFromID(UINT IDBitmap ,UINT IDMask, const IMGINFOS & imgno);
其第三個參數是一個自定義類型的結構,包含了按鈕的圖像列表成員中包含的各個圖像的狀態信息、圖像的尺寸、圖像類型標志、圖像列表中初始圖像個數和最大圖像個數等。結構的聲明及函數實現如下:
typedef struct _imageinfo{
int Normal ;
int Pushed ;
int Hover ;
int Disabled ;
int cx;
int cy;
UINT flags;
int cInitial;
int cGrow;
}IMAGELISTINFOSTRUCT;
typedef IMAGELISTINFOSTRUCT IMGINFOS;
file://Load bitmap from resource Id
BOOL LoadImageFromID(UINT IDBitmap ,UINT IDMask, const IMGINFOS & imgno)
{
if(!m_ImageList.Create( imgno.cx,imgno.cy,imgno.flags ,imgno.cInitial,imgno.cGrow))
return FALSE;
CBitmap m_Mask,m_bbmp;
if(!m_bbmp.LoadBitmap(IDBitmap))
return FALSE;
if(!m_Mask.LoadBitmap(IDMask))
return FALSE;
if((m_ImageList.Add(m_bbmp,m_Mask) == -1))
return FALSE;
SetImages(imgno.Normal,imgno.Pushed ,imgno.Hover,imgno.Disabled);
return TRUE;
}
五、例程
啟動VC++6.0,創建一個基於WTL對話框的應用程序,工程名為OwnerDrawDemo創建完成後,打開ClassView,選擇CmainDlg,單擊鼠標右鍵,選擇Add Member Variable 為CmainDlg類添加一個CownerDrawButton成員變量。打開ResourceVIEw ,在對話框資源模板上添加一個按鈕,調整到合適尺寸,ID為IDC_BUTTON1,Caption 為Help 。導入一幅位圖和一幅相應的Mask圖,修改ID分別為:IDB_BUTTON、IDB_MASK。
打開FileVIEw,打開OwnerDrawDemo.cpp在其頂部依次添加#include <atlctrlx.h>、
#include <atlgdi.h> 和 #include <atlmisc.h>。打開maindlg.h文件,在OnInitDialog函數中添加如下代碼:
DWord style = BMPBTN_AUTO3D_SINGLE|BMPBTN_SHAREIMAGELISTS|
BMPBTN_HOVER;
IMGINFOS imgis = {0,1,1,-1,30,30,ILC_COLOR24|ILC_MASK,0,2};
if(m_Button.LoadImageFromID(IDB_BUTTON,IDB_MASK,imgis))
{
m_Button.SetBitmapButtonExtendedStyle(style);
m_Button.SetBitmapPosStyle(IMAGEPOS_TOP);
}
m_Button.SubclassWindow(GetDlgItem(IDC_BUTTON1));
m_Button.SetToolTipText(_T("WTL_OwnerDrawButton!"));
現在就可以按F7構建或者Ctrl + F5執行了。