創建有個性的對話框之ATL/WTL篇

前記
    這幾個嵌入類其實很早之間就完成了,2003年的時候我在CodeProject上發佈了這些代碼,不過當時使用了紫色作爲按鈕的邊框,導致幾個無聊的 LY在哪裏爭吵關於Gays的問題,呵呵,看來不僅語言要接軌,顏色的認識也要和西方接軌喲。現在剛好趁這個機會整理一下代碼,寫篇文章紀念之。
    本文的目的是使用ATL/WTL做一個與“創建有個性的對話框之MFC篇”的個性對話框一樣的對話框。ATL/WTL一套模板庫,創建ATL/WTL應用 程序不可避免的要用到C++的模板與多繼承方面的知識,在看本文之前希望您對它們有所瞭解。本文結尾可以下載文中介紹的例子代碼,編譯這些代碼需要安裝 WTL庫,對於WTL的更詳細的信息請查看oRbIt翻譯的“WTL for MFC Programmers”系列文章,具體位置在:http://blog.51CTO.net/orbit
    ATL和WTL一起構建了一個輕量級的應用程序框架,ATL在設計時接口定義和實現是嚴格區分開的,這在窗口類的設計中是最明顯的,這一點類似於 COM,COM的接口定義和實現是完全分開的(或者可能有多個實現)。ATL有一個專門爲窗口設計的接口,可以做全部的窗口操作,這就是CWindow。 它實際上就是對HWND操作的包裝類,對幾乎所有以HWND句柄爲第一個參數的窗口API的進行了封裝,例如:SetWindowText() 和 DestroyWindow()。在ATL類中對窗口過程的實現是CWindowImpl。CWindowImpl 含有所有窗口實現代碼,例如:窗口類的註冊,窗口的子類化,消息映射以及基本的WindowProc()函數,可以看出這與MFC的設計有很大的不 同,MFC將所有的代碼都放在一個CWnd類中。
    由於ATL和MFC都是應用於Windows平臺的庫,所以他們都能夠響應和處理系統發送的窗口消息,只是ATL和MFC對消息的分派方式不同,從而造成 編寫代碼方面的差異。這些差異並不是不可逾越,ATL也定義了一些與MFC類似的宏來處理和分派消息,每個ATL的窗口類都用一個消息映射表或者稱其爲消 息映射鏈,將消息處理函數與特定的消息關聯起來,這和MFC的做法是類似的。少量的不同之處在於消息響應函數的參數,MFC對Windows的消息參數, 也就是wParam和lParam進行了內部解釋,傳遞給消息響應函數的參數比較友好,ATL的消息響應函數則是原原本本的將wParam和lParam 傳遞給了消息響應函數,對Windows的消息不太熟悉的程序員可能會很迷惑。如果你對C++的模板機制比較瞭解,並且願意不停的查閱MSDN,那就很容 易將MFC的窗口類“翻譯”成ATL/WTL的窗口類,比如本文用到的CWzButtonImpl類就是從本文的“MFC姊妹篇”中例子代碼的 CSMButton類翻譯過來的。
    在開始用ATL/WTL創建個性對話框之前,還要介紹一下ATL中常用的嵌入類(Mix-in class)。ATL的另一個顯著不同之處就是任何一個C++類都可以響應消息,而MFC只是將消息響應任務分給了CWnd類和CCmdTarget類, 外加幾個有PreTranslateMessage()方法的類。ATL的這種特性允許我們編寫所謂的“嵌入類”,爲我們的窗口添加特性只需將該類添加到 繼承列表中就行了,就這麼簡單!一個基本的帶有消息映射鏈的類通常是模板類,將派生類的類名作爲模板的參數,這樣它就可以訪問派生類中的成員,比如 m_hWnd(CWindow類中的HWND成員)。讓我們來看一個嵌入類的例子,這個嵌入類通過響應WM_ERASEBKGND消息來畫窗口的背景:
template <class T, COLORREF t_crBrushColor>
class CPaintBkgnd : public CMessageMap
{
public:
 CPaintBkgnd() { m_hbrBkgnd = CreateSolidBrush(t_crBrushColor); }
 ~CPaintBkgnd() { DeleteObject ( m_hbrBkgnd ); }
 BEGIN_MSG_MAP(CPaintBkgnd)
  MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
 END_MSG_MAP()
 
 LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
 {
  T*   pT = static_cast<T*>(this);
  HDC  dc = (HDC) wParam;
  RECT rcClient;
  pT->GetClientRect ( &rcClient );
  FillRect ( dc, &rcClient, m_hbrBkgnd );
  return 1;    // we painted the background
 }
protected:
 HBRUSH m_hbrBkgnd;
};
讓我們來研究一下這個新類。首先,CPaintBkgnd有兩個模板參數:使用CPaintBkgnd的派生類的名 字和用來畫窗口背景的顏色。(t_ 前綴通常用來作爲模板類的模板參數的前綴)CPaintBkgnd也是從CMessageMap派生的,這並不是必須的,因爲所有需要響應消息的類只需使 用BEGIN_MSG_MAP宏就足夠了,所以你可能看到其他的一些嵌入類的例子代碼,它們並不是從該基類派生的。
    構造函數和析構函數都相當簡單,只是創建和銷燬Windows畫刷,這個畫刷由參數t_crBrushColor決定顏色。接着是消息映射鏈,它響應 WM_ERASEBKGND消息,最後由響應函數OnEraseBkgnd()用構造函數創建的畫刷填充窗口的背景。要在我們的窗口中使用這個嵌入類需要 做兩件事:首先,將它加入到繼承列表:
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
                  public CPaintBkgnd<CMyWindow, RGB(0,0,255)>
其次,需要CMyWindow將消息傳遞給CPaintBkgnd,就是將其鏈入到消息映射鏈,在CMyWindow的消息映射鏈中加入CHAIN_MSG_MAP宏:
typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase;
BEGIN_MSG_MAP(CMyWindow)
 MESSAGE_HANDLER(WM_CLOSE, OnClose)
 MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
 COMMAND_HANDLER(IDC_ABOUT, OnAbout)
 CHAIN_MSG_MAP(CPaintBkgndBase)
END_MSG_MAP()
任何CMyWindow沒有處理的消息都被傳遞給CPaintBkgnd。應該注意的是 WM_CLOSE,WM_DESTROY和IDC_ABOUT消息將不會傳遞,因爲這些消息一旦被處理消息映射鏈的查找就會中止。你可以在繼承列表中使用 多個嵌入類,每一個嵌入類使用一個CHAIN_MSG_MAP宏,這樣消息映射鏈就會將消息傳遞給它。這與MFC不同,MFC的CWnd派生類只能有一個 基類,MFC自動將消息傳遞給基類。
    瞭解了ATL消息處理方式和嵌入類的知識之後,就可以開始創建ATL/WTL的彩色對話框了。在“創建有個性的對話框之MFC篇”中提到了一種最簡單的改 變對話框背景顏色的方法,就是調用CWinApp::SetDialogBkColor()函數,很不幸,ATL/WTL沒有提供這個方法,至少ATL 7.1和WTL 7.1是這樣的。不過沒關係,我們可以向MFC那樣通過處理一些特殊的窗口繪製消息來實現彩色的對話框,而且ATL/WTL的方式更加靈活,稍後就會看 到。
    首先是修改對話框的背景,這個只需要處理WM_ERASEBKGND消息就行了,在ATL的窗口中也可以這樣做,不過ATL可以使用更加靈活的嵌入類,前 面提到的CPaintBkgnd就是這樣的嵌入類。使用嵌入類將WM_ERASEBKGND消息的處理函數封裝到CPaintBkgnd類中,避免了 CMainDlg代碼的過度臃腫,同時也使得創建同樣背景的對話框更加用以,不需要拷貝OnEraseBkgnd中的代碼到新對話框類,只需將 CPaintBkgnd加入到新類的集成列表中就行了:
class CMainDlg : public CDialogImpl<CMainDlg>,
    public CPaintBkgnd<CMainDlg, RGB(0,0,255)>
這樣CMainDlg就擁有了藍色的背景,改變CPaintBkgnd的模板參數t_crBrushColor可以定製對話框不同的顏色,下面的代碼使得對話框擁有了淡綠色的背景:
class CMainDlg : public CDialogImpl<CMainDlg>,
    public CPaintBkgnd<CMainDlg, RGB(215,241,215)>

圖1.使用了CPaintBkgnd的對話框
和在MFC中處理WM_ERASEBKGND消息一樣,控件的背景顏色還沒有改變,下面本文就介紹在ATL/WTL框架下如何響應WM_CTLCOLORXXX消息。
    MFC將所有的WM_CTLCOLORXXX系列消息都映射爲WM_CTLCOLOR消息,只是通過OnCtlColor()函數的參數區分他們,不過 WM_CTLCOLORDLG消息並沒有被映射爲WM_CTLCOLOR,因爲MFC對爲WM_CTLCOLORDLG消息做了其他處理,如果你願意追蹤 一下CWinApp::SetDialogBkColor()的代碼你就會看到MFC是如何處理WM_CTLCOLORDLG消息的。也正式因爲MFC對 WM_CTLCOLORDLG“另有打算”,使得本文的“MFC姊妹篇”中的例子爲了改變對話框的背景色不得不處理了WM_ERASEBKGND消息。但 是使用ATL/WTL則不需要額外處理WM_ERASEBKGND消息,因爲處理WM_CTLCOLORDLG通知消息更簡單。ATL/WTL沒有對 WM_CTLCOLORXXX系列消息統一處理,不過如果集中處理這些消息能夠簡化代碼編寫,提高與MFC代碼的互換性,我們也可以將其映射到一個消息處 理函數中,事實上下面將要介紹的CCtrlColor就是這樣做的。
    下面就是使用了CCtrlColor之後的效果圖,這裏沒有繼續使用CPaintBkgnd,因爲CCtrlColor中處理了WM_CTLCOLORDLG通知消息。

圖2.使用了CCtrlColor的對話框
在對話框中使用位圖背景也很簡單,只要照着CPaintBkgnd樣子寫一個畫位圖的類就行了,以下是一個簡單的例子:
template <class T, UINT uBitmapID>
class CBitmapBkgnd : public CMessageMap
{
public:
 CBitmapBkgnd() { m_Bitmap.LoadBitmap(uBitmapID); }
 ~CBitmapBkgnd() { m_Bitmap.DeleteObject(); }
 BEGIN_MSG_MAP(CPaintBkgnd)
  MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
 END_MSG_MAP()
 
 LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
 {
  T*   pT = static_cast<T*>(this);
  HDC  hDC = (HDC) wParam;
  RECT rcClient;
  pT->GetClientRect ( &rcClient );
  
  BITMAP bm;
  m_Bitmap.GetBitmap(&bm);
  CDC memDC;
  memDC.CreateCompatibleDC(hDC);
  HBITMAP hOldBmp = memDC.SelectBitmap(m_Bitmap);
  StretchBlt(hDC,0,0,rcClient.right - rcClient.left,rcClient.bottom - rcClient.top,memDC,0,0,bm.bmWidth,bm.bmHeight,SRCCOPY);
  memDC.SelectBitmap(hOldBmp);
  memDC.DeleteDC();
  return 1;    // we painted the background
 }
protected:
 CBitmap m_Bitmap;
};
和CPaintBkgnd相比,CBitmapBkgnd只是修改了第二個模板參數和OnEraseBkgnd() 函數的代碼,由於使用位圖作爲對話框的背景,對話框中的控件在處理WM_CTLCOLORXXX系列消息時應該返回一個空畫刷而不是默認的背景顏色畫刷, 也就是需要一個定製的處理WM_CTLCOLORXXX系列消息的嵌入類,本文用了CCtrlHollowColor與CBitmapBkgnd配合使 用,但是CCtrlHollowColor過於簡單,它只能在本文的例子中使用,如果對話框中有更多的控件就需要修改CCtrlHollowColor類 的代碼。下面是使用位圖背景的效果:

圖3.使用位圖背景的對話框
    現在該考慮按鈕的問題了,和本文的MFC姊妹篇一樣,本文設計的WTL例子也使用了一個自畫按鈕類。前文已經提過,它其實就是從CSMButton“移 植”過來的,你肯定覺得還是有些不一樣,那是因爲CWzButtonImpl使用了ATL的特性,那就是使用WTL的嵌入類:COwnerDraw。在 MFC的按鈕類CSMButton中,WM_DRAWITEM消息被MFC做了內部映射,最終的按鈕繪製映工作交給了虛函數DrawItem(),這是利 用了C++的多態機制。ATL/WTL不使用虛函數,多繼承和嵌入類是它擁有比MFC更加靈活的方式避開了使用虛函數,這也就使得ATL/WTL的窗口對 象比MFC的窗口對象佔用更少的內存。只要將COwnerDraw類加入窗口類的集成列表,就可以原封不動的將 CSMButton::DrawItem()搬到CWzButtonImpl中,以下是CWzButtonImpl類的完整聲明,可以看到它是個窗口類, 具有CButton的接口:
class CWzButtonImpl : public CWindowImpl<CWzButtonImpl, CButton>,public COwnerDraw<CWzButtonImpl>
CWzButtonImpl的使用方法可在ATL中與MFC有較大的差異,它只能通過SubclassWindow()這個方法與控件關聯起來,不過有了WTL之後就和MFC一樣了,因爲ATL也實現了類似MFC的DDE/DDV。
    如果一個對話框中有很多個按鈕,一個一個添加關聯也是一件很麻煩的事情,尤其是當你需要從對話框中刪除某個按鈕的時候。在這裏我要介紹一個有用的輔助 類:CButtonHelp。CButtonHelp是一個模板類,它的作用就是在對話框初始化的時候枚舉對話框中所有的控件,找出其中的按鈕控件並用模 板參數指定的按鈕類子類化(Subclassing)這個控件。以下是CButtonHelp類的聲明:
template <class T, class t_ButtonClass>
class CButtonHelp
第一個模板參數是窗口類,第二個模板參數就是按鈕類,通過替換模板參數t_ButtonClass可以在不同的對話框中使用不同風格的按鈕,下面的例子就是本文使用的例子代碼中主對話框的聲明,他使用CWzButtonImpl來子類化按鈕控件:
class CMainDlg : public CDialogImpl<CMainDlg>, public CCtrlColor<CMainDlg>,
  public CButtonHelp<CMainDlg,CWzButtonImpl>
假設還有一個自畫按鈕類CBitmapButtonImpl,下面的聲明將使CBmpDlg對話框擁有了另一種風格的自畫按鈕:
class CBmpDlg : public CDialogImpl<CBmpDlg>, public CCtrlColor<CBmpDlg>,
  public CButtonHelp<CBmpDlg,CBitmapButtonImpl>
CButtonHelp類有三個主要的函數,一個是SubclassAllButtons(),這個函數遍歷對話框 所有的子控件,子類化其中具有pushbutton風格的按鈕,SubclassButton()子類化指定的按 鈕,UnSubclassButton()則是SubclassButton()的逆過程。下面就是使用了CButtonHelp和 CWzButtonImpl後的效果:

圖4.使用自畫按鈕後的對話框

    至此,本文已經使用ATL/WTL實現了MFC例子中的全部功能,通過對比可以看到這兩個FrameWork各有各的優點,對同一個問題的解決方案也是一樣的,只是編碼的方式不同。本文使用的例子代碼可以在文後的鏈接中下載。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章