wtl學習總結


在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這里就幾個點講一下:

  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.htm

 

窗口創建和初始化

        有個哥們就這個流程分析了一下,地址在這里 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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM