wtl學習總結

在windows平臺,相比MFC,我更喜歡WTL,因其簡潔漂亮。因此陸續花了一年的時間學習之,這裏總結一下(在學習Wtl/Atl以前,最好是對WinApi編程有必定的瞭解)。html

安裝

        Wtl主頁 http://sourceforge.net/projects/wtl/ ,整個庫就是一堆.h文件,官方沒有提供Installer,下載後解壓到某個目錄便可。程序員

        若是須要在VS中使用「工做導向」,能夠點擊Appwiz目錄下的對應js文件來安裝之。雖然沒有直接對VS2010的支持,不過拿VS2008的改改便可,詳情Google下。編程

        爲了在VS中使用Wtl,能夠將include目錄添加到全局Include Path。不過若是僅僅但願對單個項目有效,則在添加C++ Include Path的同時,可能還須要添加資源的Include Path。若是不但願以來第三方的庫,能夠直接將源碼放在項目內。windows

 

Atl

        學習Wtl,不可能跳過Atl,Wtl底層就是使用Atl。關於Atl這裏就幾個點講一下:瀏覽器

  1. ATL-style 模板
  2. 窗口建立和初始化
  3. Thunk技術
  4. 回調和消息綁定

 

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 };

 

使用這個模板形式有幾個好處:框架

  1. 不須要使用指向對象的指針。
  2. 節省內存,由於不須要虛函數表。
  3. 由於沒有虛函數表因此不會發生在運行時調用空指針指向的虛函數。
  4. 全部的函數調用在編譯時肯定(譯者加:區別於C++的虛函數機制使用的動態編連),有利於編譯程序對代碼的優化。

http://www.winmsg.com/wtl/Part1.htmsvn

 

窗口建立和初始化

        有個哥們就這個流程分析了一下,地址在這裏 http://blog.csdn.net/jznsmail/archive/2004/12/01/200947.aspx 。函數

        在Windows下,任何窗口建立都是經過CreateWindowEx或者CreateWindow函數來實現之,Wtl/Atl也不例外,(不過WtlAtl的流程略有不一樣)。詳細的流程見上面連接,這裏就幾個重點說一下(不只僅是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這一套結構有幾個好處:

  1. 避免過多的純虛函數致使額外的開銷
  2. 容易將消息處理重定向,例如將消息處理重定向到一個非窗口類,實現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)

WTL 學習筆記 -- DDX 和 DDV

 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/

相關文章
相關標籤/搜索