在windows平台,相比MFC,我更喜歡WTL,因其簡潔漂亮。所以陸續花了一年的時間學習之,這里總結一下(在學習Wtl/Atl之前,最好是對WinApi編程有一定的了解)。
安裝
Wtl主頁 http://sourceforge.net/projects/wtl/ ,整個庫就是一堆.h文件,官方沒有提供Installer,下載后解壓到某個目錄即可。
如果需要在VS中使用“工作導向”,可以點擊Appwiz目錄下的對應js文件來安裝之。雖然沒有直接對VS2010的支持,不過拿VS2008的改改即可,詳情Google下。
為了在VS中使用Wtl,可以將include目錄添加到全局Include Path。不過如果僅僅希望對單個項目有效,則在添加C++ Include Path的同時,可能還需要添加資源的Include Path。如果不希望以來第三方的庫,可以直接將源碼放在項目內。
Atl
學習Wtl,不可能跳過Atl,Wtl底層就是使用Atl。關於Atl這里就幾個點講一下:
- ATL-style 模板
- 窗口創建和初始化
- Thunk技術
- 回調和消息綁定
ATL-style 模板
template <class T> class B1 { public: void SayHi() { T* pT = static_cast<T*>(this); pT->PrintClassName(); } protected: void PrintClassName() { cout << "This is B1"; } }; class D1 : public B1<D1> { // No overridden functions at all };
使用這個模板形式有幾個好處:
- 不需要使用指向對象的指針。
- 節省內存,因為不需要虛函數表。
- 因為沒有虛函數表所以不會發生在運行時調用空指針指向的虛函數。
- 所有的函數調用在編譯時確定(譯者加:區別於C++的虛函數機制使用的動態編連),有利於編譯程序對代碼的優化。
http://www.winmsg.com/wtl/Part1.htm
窗口創建和初始化
有個哥們就這個流程分析了一下,地址在這里 http://blog.csdn.net/jznsmail/archive/2004/12/01/200947.aspx 。
在Windows下,任何窗口創建都是通過CreateWindowEx或者CreateWindow函數來實現之,Wtl/Atl也不例外,(不過Wtl和Atl的流程略有不同)。詳細的流程見上面鏈接,這里就幾個重點說一下(不僅僅是Atl,也包括Wtl的內容)。
對於Windows窗口,需要派生自CFrameWindowImpl類,創建和初始化窗口就在該類的CreateEx函數實現。通過前面講到的“ATL-style 模板”,CreateEx會調用Create函數,CFrameWindowImpl提供了默認的Create實現,不過也可以在派生類中定制。在CFrameWindowImpl類成員函數Create中會首先注冊窗口,然后才創建之。
在CFrameWindowImpl的Create函數中會通過CFrameWindowImplBase的成員函數Create創建實際的窗口,使用API函數CreateWindowEx。
在CFrameWindowImpl的Create函數中會調用CFrameWndClassInfo類成員函數Register注冊窗口,注冊用到的信息通過DECLARE_FRAME_WND_CLASS或DECLARE_FRAME_WND_CLASS_EX宏來指定。在這兩個宏中會指定靜態成員函數StartWindowProc 作為窗口回調。這個靜態成員在CWindowImplBaseT類中定義。在這個靜態成員中使用了下面提到的Thunk技術動態地修改回調參數,同時將回調重置為該類的另一個靜態成員WindowProc(見API函數SetWindowLongPtr和屬性GWLP_WNDPROC)。
以上提到的是Wtl流程的一個簡單概述,對於普通窗口(控件),我們一般使用Atl的那一套而非Wtl。
對於控件,一般派生自CWindowImpl窗口,窗口工作在該類的成員函數Create中完成。注冊窗口的信息使用DECLARE_WND_CLASS或DECLARE_WND_CLASS_EX宏。具體的注冊操作在ATL::CWndClassInfo類的成員函數Register中完成,窗口的創建在CWindowImplBaseT類的成員函數Create中完成。
Thunk技術
對於Windows API,從系統的消息回調出來的消息唯一的標識符就是HWND句柄,而當前隨便一個UI程序都有相當之多的控件,所以必須要有一種行之有效的方法來通過這個句柄定位控件。比較傻B的方法就是建立一個HWND到控件類實例的映射,然后在消息收到后查詢之,不過這種方法有很大的局限性。
Atl用Thunk技術來處理這個問題,這個所謂的“Thunk技術”干的事挺簡單,不過要理解代碼還真不容易(至少我是花了不少功夫)。有興趣可以研究下代碼:
(Microsoft Visual Studio 9.0\VC\atlmfc\include\atlstdthunk.h)
#pragma pack(push,1)
struct _stdcallthunk
{
DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
DWORD m_this; //
BYTE m_jmp; // jmp WndProc
DWORD m_relproc; // relative jmp
BOOL Init(DWORD_PTR proc, void* pThis)
{
m_mov = 0x042444C7; //C7 44 24 0C
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));
return TRUE;
}
//some thunks will dynamically allocate the memory for the code
void* GetCodeAddress()
{
return this;
}
void* operator new(size_t)
{
return __AllocStdCallThunk();
}
void operator delete(void* pThunk)
{
__FreeStdCallThunk(pThunk);
}
};
#pragma pack(pop)
這里我就不說什么原理了,我這水平也說不清:-)。
Thunk技術實際上就是用一段匯編代碼將函數的參數動態的替換,具體來說是將回調函數參數HWND動態替換成窗口類實例的指針。關於它推薦閱讀 http://www.cngr.cn/article/54/395/2006/2006071928301.shtml 。
這個過程在CWindowImplBaseT類的靜態成員StartWindowProc中實現。首先在注冊窗口時會指定它為回調,當第一次調用時,通過Thunk技術動態地修改回調參數,同時將回調重置為該類的另一個靜態成員WindowProc。具體的實現代碼如下:
pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
WNDPROC pProc = pThis->m_thunk.GetWNDPROC();
WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
那這個類實例在哪獲取呢?其實Wtl/Atl實現也並不美觀,通過CAtlWinModule類存儲這些指針,很顯然整個程序只應該包含一個CAtlWinModule類實例,這也是為什么我們經常看到這個實例前帶一個extern關鍵字。
在新的回調WindowProc中,Atl將參數HWND轉型為CWindowImplBaseT類指針,然后調用該類的成員函數ProcessWindowMessage來進一步派發消息。這個函數怎么定義看下一節內容。
回調和消息綁定
前面提到一個ProcessWindowMessage函數,它最初在CMessageMap類中以純虛函數的形式定義。在Atl中不管什么類,只要派生自CMessageMap就可以處理消息,這就提供了很大的靈活性。
ATL定義了一堆預處理宏來實現這個函數和分發邏輯,典型的如下:
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_CLOSE, OnClose)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
COMMAND_ID_HANDLER(IDC_ABOUT, OnAbout)
END_MSG_MAP()
前后兩個宏用於定義ProcessWindowMessage函數,中間則通過一個Switch結構來定義分發邏輯和回調綁定。
ATL這一套結構有幾個好處:
- 避免過多的純虛函數導致額外的開銷
- 容易將消息處理重定向,例如將消息處理重定向到一個非窗口類,實現UI和邏輯分離。也可以讓同一個消息處理類同時處理多個窗口的消息。具體見ATL定義的那堆宏。
Wtl在Atl基礎上針對特定消息定義了很多更加精確的宏,不過個人不是很喜歡,因為懶得去記。
Wtl入門
最基本的Wtl程序可以通過Appwiz來生成,很有必要理解下Wtl程序的流程。簡單地講,控件使用CButton等封裝類(atlctrls.h),窗口使用CFrameWindowImpl派生。如果希望自定義控件,可以派生自CWindowImpl(自繪還需要繼承其他自繪支持類,見后面說明),例如
class CBeepButton: public CWindowImpl< CBeepButton,CButton > { public: DECLARE_WND_CLASS( _T("CBeepButton")) BEGIN_MSG_MAP( CBeepButton ) END_MSG_MAP() };
不過上面的類封裝除了裝逼沒有任何用處!
發現幾個比較惡心的地方提一下。
如果在自定義控件有繪制字體時,繪出來的字很奇怪,和默認的字體相差較大,后來發現可以手動設置字體將其設置為UI默認字體。
m_widget_->SetFont(AtlGetStockFont(DEFAULT_GUI_FONT));
對於動態創建的Edit或者RichEdit,邊框都很奇怪,為了設置默認的邊框,需要
m_widget_->ModifyStyleEx(0, WS_EX_CLIENTEDGE, SWP_DRAWFRAME);
超類化
超類化(superclass )是一種生成新的窗口類的方法。它的中心思想是依靠現有的窗口類,克隆出另一個窗口類。被克隆的類可以是Windows預定義的窗口類,這些預定義的窗口類有按鈕或下拉框控制等等。也可以是一般的類。克隆的窗口類使用被克隆的類(基類)的窗口消息處理函數。
克隆類可以有自己的窗口消息處理函數,也可以使用基類的窗口處理函數。
(http://www.builder.com.cn/2007/1116/637833.shtml )
超類化是以類型為單位來設置,也就是被超類的類必須是已經存在的窗口(很顯然,如果想“超”自定義窗口類,必須保證該類至少被用過一次或者手動注冊過,如果沒注冊則超類會初始化失敗)。在消息處理時,Wtl首先會使用超類的處理函數,如果沒處理,則會調用“被超類”的消息處理函數來處理。
那這個“被超類”的消息處理函數在哪里呢?畢竟每次只能注冊一個回調函數。實際上在CWindowImplBaseT類中包含一個成員m_pfnSuperWindowProc用於在超類化時存儲“被超類”的原始回調。這個成員在CFrameWndClassInfo類或_ATL_WNDCLASSINFOW結構的成員函數Register調用時作為參數傳入實現初始化。
通過宏DECLARE_FRAME_WND_SUPERCLASS或DECLARE_WND_SUPERCLASS來聲明一個超類,兩者原理都是一致的,即定義一個OrigWndClassName。在注冊窗口類是首先獲得OrigWndClassName類的注冊信息,然后替換回調,並且將原來的回調保存在參數中,這個參數即前面提到的m_pfnSuperWindowProc。
關於這兩回調的配合,可以參考CWindowImplBaseT類靜態成員函數WindowProc(這個函數即通過Thunk技術重置后的回調函數,見前面說明)。Wtl首先會調用當前的回調,如果返回FALSE,則繼續調用原來的回調。
為了測試超類化自定義窗口類,我寫了如下代碼:
class CBeepButton1: public CWindowImpl< CBeepButton1,CButton > { public: DECLARE_WND_CLASS( _T("CBeepButton1")) BEGIN_MSG_MAP( CBeepButton1 ) MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLButtonDown ) MESSAGE_HANDLER( WM_LBUTTONUP, OnLButtonUp ) END_MSG_MAP() LRESULT OnLButtonDown( UINT, WPARAM, LPARAM, BOOL& bHandled ) { this->SetWindowText("Press"); bHandled = FALSE; // alternatively: DefWindowProc() return 0; } LRESULT OnLButtonUp( UINT, WPARAM, LPARAM, BOOL& bHandled ) { this->SetWindowText("Release"); bHandled = FALSE; // alternatively: DefWindowProc() return 0; } }; class CBeepButton: public CWindowImpl< CBeepButton > { public: DECLARE_WND_SUPERCLASS( _T("BeepButton"), _T("CBeepButton1") ) BEGIN_MSG_MAP( CBeepButton ) MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLButtonDown ) END_MSG_MAP() LRESULT OnLButtonDown( UINT, WPARAM, LPARAM, BOOL& bHandled ) { bHandled = FALSE; // alternatively: DefWindowProc() return 0; } };
經測試發現幾個問題,為了使用 CBeepButton,必須先實現一個 CBeepButton1(目的是為了注冊窗口類,主要是手動注冊不方便)。否則 CBeepButton1會注冊失敗。即便如此 CbeepButton還是無法運行,究其原因是因為Thunk技術未能重置回調。
前面說過,Atl在初始化一個窗口類時,會使用Thunk技術動態重置回調。這個兩個回調分別是CWindowImplBaseT類靜態成員函數StartWindowProc和WindowPro,Thunk技術在前者中被使用,是一個臨時的回調,具體的消息分發在后者進行。
當超類化一個自定義窗口類時,首先會獲得原窗口的回調,而這個回調很顯然是StartWindowProc,而新的窗口類並非處理了所有消息,所以總是有部分消息會發送到原窗口類的回調,即StartWindowProc。杯具就在這里,StartWindowProc是一個臨時的回調。實際中當消息派發到這里時會碰到斷點,因為無法從CAtlWinModule中獲得類實例的指針(這都不是該窗口類初始化過程,當然找不到了)。這不得不說是Wtl的一個Bug,不過就目前這種框架,要改好還真不容易。
子類化
子類化(subclass)是普遍采用的一種擴展窗口功能的方法。它的大致原理如下。
在一個窗口創建完了之后,將該窗口的窗口函數替換成新的窗口消息處理函數。這個新的窗口函數可以對某些需要處理的特定的消息進行處理,然后再將處理傳給原來的窗口函數。
注意它與superclass的區別。
Superclass是以一個類為原版,進行克隆。既在注冊新的窗口類時,使用的是基類窗口的窗口函數。
而subclass是在某一個窗口注冊並創建后,通過修改該窗口的窗口消息函數的地址而實現的。它是針對窗口實例。
(http://www.builder.com.cn/2007/1116/637833.shtml )
子類化的核心就是前面提到的Thunk技術,這里就不廢話了。
自繪
說的自繪,我真想吐血,很不明白微軟為什么要為了自繪搞出那么一套鳥毛東西,雖然所有的自繪都可以通過Paint來完成,不過既然微軟推出那么一套鳥毛,總是會要猶豫一下,”我用Paint來做會不會有什么不妥“。
並不是所有的內置控件都支持那一套鳥毛的自繪,Wtl通過COwnerDraw類和CCustomDraw 類來簡化這一套東西的開發,其實就是定義一堆宏來處理回調,提供一個相對簡單的接口給程序員。為了能夠讓控件收到這些自定義消息,需要在父窗口加入反射宏DEFAULT_REFLECTION_HANDLER()。
不過個人很少用這東西,即便要自繪Button,我也是通過WM_PAINT來做的(這種做法有什么不妥忘各位指出,不過我沒發現效率有多低,至少結構一致和美觀,我想誰都不想去理解那么多規則,簡單就是美)。
瀏覽器控件
如今越來越多的UI程序選擇瀏覽器控件來顯示網頁內容,這有很多好處,至少簡化了開發並且可以實現很復雜的樣式,更重要的是無需升級可保持最新。
Wtl官方並沒有內置瀏覽器控件,不過可以參考這里http://devel.openocr.org/svn/openocr/trunk/cuneiform/interface/icrashreport/wtl/samples/tabbrowser/browserview.h ,
----------------------------------------------WTL 方式對話框數據交換(DDX)
MFC程序員的WTL指南: Part IV - 對話框與控件(二)
http://www.cnblogs.com/procoder/archive/2009/06/11/1501044.html
---wtl 界面開發 http://blog.163.com/l1_jun/blog/static/14386388201052922725417/