ATL对窗口消息处理函数的封装
在本节开始部分谈到的封装窗口的两个难题,其中第一个问题是怎样解决将窗口函数的消息转发到HWND相对应的类的实例中的相应函数。
下面我们来看一下,ATL采用的是什么办法来实现的。
我们知道每个Windows的窗口类都有一个窗口函数。
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
在类CWindowImplBaseT中,定义了两个类的静态成员函数。
template <class TBase = CWindow, class TWinTraits = CControlWinTraits> class ATL_NO_VTABLE CWindowImplBaseT : public CWindowImplRoot< TBase > { public: … … static LRESULT CALLBACK StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); … … } |
它们都是窗口函数。之所以定义为静态成员函数,是因为每个类必须只有一个窗口函数,而且,窗口函数的申明必须是这样的。
在前面介绍的消息处理逻辑过程中,我们知道怎样通过宏生成虚函数ProcessWindowsMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID)。
现在的任务是怎样在窗口函数中把消息传递给某个实例(窗口)的ProcessWindowsMessage()。这是一个难题。窗口函数是类的静态成员函数,因此,它不象类的其它成员函数,参数中没有隐含this指针。
注意,之所以存在这个问题是因为ProcessWindowsMessage()是一个虚函数。而之所以用虚函数是考虑到类的派生及多态性。如果不需要实现窗口类的派生及多态性,是不存在这个问题的。
通常想到的解决办法是根据窗口函数的HWND参数,寻找与其对应的类的实例的指针。然后,通过该指针,调用该实例的消息逻辑处理函数ProcessWindowsMessage()。
这样就要求存储一个全局数组,将HWND和该类的实例的指针一一对应地存放在该数组中。
ATL解决这个问题的方法很巧妙。该方法并不存储这些对应关系,而是使窗口函数接收C++类指针作为参数来替代HWND作为参数。
具体步骤如下:
· 在注册窗口类时,指定一个起始窗口函数。
· 创建窗口类时,将this指针暂时保存在某处。
· Windows在创建该类的窗口时会调用起始窗口函数。它的作用是创建一系列二进制代码(thunk)。这些代码用this指针的物理地址来取代窗口函数的HWND参数,然后跳转到实际的窗口函数中。这是通过改变栈来实现的。
· 然后,用这些代码作为该窗口的窗口函数。这样,每次调用窗口函数时都对参数进行转换。
· 在实际的窗口函数中,只需要将该参数cast为窗口类指针类型。
详细看看ATL的封装代码。
1. 注册窗口类时,指定一个起始窗口函数。
在superclass中,我们分析到窗口注册时,指定的窗口函数是StartWindowProc()。
2. 创建窗口类时,将this指针暂时保存在某处。
template <class TBase, class TWinTraits> HWND CWindowImplBaseT< TBase, TWinTraits >::Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName, DWORD dwStyle, DWORD dwExStyle, UINT nID, ATOM atom, LPVOID lpCreateParam) { ATLASSERT(m_hWnd == NULL); if(atom == 0) return NULL; _Module.AddCreateWndData(&m_thunk.cd, this); if(nID == 0 && (dwStyle & WS_CHILD)) nID = (UINT)this; HWND hWnd = ::CreateWindowEx(dwExStyle, (LPCTSTR)MAKELONG(atom, 0), szWindowName, dwStyle, rcPos.left, rcPos.top, rcPos.right - rcPos.left, rcPos.bottom - rcPos.top, hWndParent, (HMENU)nID, _Module.GetModuleInstance(), lpCreateParam); ATLASSERT(m_hWnd == hWnd); return hWnd; } |
该函数用于创建一个窗口。它做了两件事。第一件就是通过_Module.AddCreateWndData(&m_thunk.cd, this);语句把this指针保存在_Module的某个地方。
第二件事就是创建一个Windows窗口。
3. 一段奇妙的二进制代码
下面我们来看一下一段关键的二进制代码。它的作用是将传递给实际窗口函数的HWND参数用类的实例指针来代替。
ATL定义了一个结构来代表这段代码:
#pragma pack(push,1) struct _WndProcThunk { 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 }; |
#pragma pack(pop)
#pragma pack(push,1)的意思是告诉编译器,该结构在内存中每个字段都紧紧挨着。因为它存放的是机器指令。
这段代码包含两条机器指令:
mov dword ptr [esp+4], pThis jmp WndProc |
MOV指令将堆栈中的HWND参数(esp+0x4)变成类的实例指针pThis。JMP指令完成一个相对跳转到实际的窗口函数WndProc的任务。注意,此时堆栈中的HWND参数已经变成了pThis,也就是说,WinProc得到的HWND参数实际上是pThis。
上面最关键的问题是计算出jmp WndProc的相对偏移量。
我们看一下ATL是怎样初始化这个结构的。
class CWndProcThunk { public: union { _AtlCreateWndData cd; _WndProcThunk thunk; }; void Init(WNDPROC proc, void* pThis) { thunk.m_mov = 0x042444C7; //C7 44 24 0C thunk.m_this = (DWORD)pThis; thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk)); // write block from data cache and // flush from instruction cache FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk)); } }; |
ATL包装了一个类并定义了一个Init()成员函数来设置初始值的。在语句thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk)); 用于把跳转指令的相对地址设置为(int)proc - ((int)this+sizeof(_WndProcThunk))。
上图是该窗口类的实例(对象)内存映象图,图中描述了各个指针及它们的关系。很容易计算出相对地址是(int)proc - ((int)this+sizeof(_WndProcThunk))。
4. StartWindowProc()的作用
template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData(); ATLASSERT(pThis != NULL); pThis->m_hWnd = hWnd; pThis->m_thunk.Init(pThis->GetWindowProc(), pThis); WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk); WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); #ifdef _DEBUG // check if somebody has subclassed us already since we discard it if(pOldProc != StartWindowProc) ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.n")); #else pOldProc; // avoid unused warning #endif return pProc(hWnd, uMsg, wParam, lParam); } |
该函数做了四件事:
一是调用_Module.ExtractCreateWndData()语句,从保存this指针的地方得到该this指针。
二是调用m_thunk.Init(pThis->GetWindowProc(), pThis)语句初始化thunk代码。
三是将thunk代码设置为该窗口类的窗口函数。
WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk); WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); |
这样,以后的消息处理首先调用的是thunk代码。它将HWND参数改为pThis指针,然后跳转到实际的窗口函数WindowProc()。
四是在完成上述工作后,调用上面的窗口函数。
由于StartWindowProc()在创建窗口时被Windows调用。在完成上述任务后它应该继续完成Windows要求完成的任务。因此在这里,就简单地调用实际的窗口函数来处理。
5. WindowProc()窗口函数
下面是该函数的定义:
template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd; // set a ptr to this message and save the old value MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } }; const MSG* pOldMsg = pThis->m_pCurrentMsg; pThis->m_pCurrentMsg = &msg; // pass to the message map to process LRESULT lRes; BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0); // restore saved value for the current message ATLASSERT(pThis->m_pCurrentMsg == &msg); pThis->m_pCurrentMsg = pOldMsg; // do the default processing if message was not handled if(!bRet) { if(uMsg != WM_NCDESTROY) lRes = pThis->DefWindowProc(uMsg, wParam, lParam); else { // unsubclass, if needed LONG pfnWndProc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC); lRes = pThis->DefWindowProc(uMsg, wParam, lParam); if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC) == pfnWndProc) ::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_pfnSuperWindowProc); // clear out window handle HWND hWnd = pThis->m_hWnd; pThis->m_hWnd = NULL; // clean up after window is destroyed pThis->OnFinalMessage(hWnd); } } return lRes; } |
首先,该函数把hWnd参数cast到一个类的实例指针pThis。
然后调用pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);语句,也就是调用消息逻辑处理,将具体的消息处理事务交给ProcessWindowMessage()。
接下来,如果ProcessWindowMessage()没有对任何消息进行处理,就调用缺省的消息处理。
注意这里处理WM_NCDESTROY的方法。这和subclass有关,最后恢复没有subclass以前的窗口函数。
WTL对subclass的封装
前面讲到过subclass的原理,这里看一下是怎么封装的。
template <class TBase, class TWinTraits> BOOL CWindowImplBaseT< TBase, TWinTraits >::SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd == NULL); ATLASSERT(::IsWindow(hWnd)); m_thunk.Init(GetWindowProc(), this); WNDPROC pProc = (WNDPROC)&(m_thunk.thunk); WNDPROC pfnWndProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc); if(pfnWndProc == NULL) return FALSE; m_pfnSuperWindowProc = pfnWndProc; m_hWnd = hWnd; return TRUE; } |
没什么好说的,它的工作就是初始化一段thunk代码,然后替换原先的窗口函数。
ATL仅仅是封装了窗口函数和提供了消息映射。实际应用中,需要各种种类的窗口,比如,每个界面线程所对应的框架窗口。WTL正是在ATL基础上,为我们提供了框架窗口和其他各种窗口。
所有的应用程序类型中,每个界面线程都有一个框架窗口(Frame)和一个视(View)。它们的概念和MFC中的一样。
图示是WTL的窗口类的继承图。
WTL框架窗口为我们提供了:
一个应用程序的标题,窗口框架,菜单,工具栏。
视的管理,包括视的大小的改变,以便与框架窗口同步。
提供对菜单,工具栏等的处理代码。
在状态栏显示帮助信息等等。
WTL视通常就是应用程序的客户区。它通常用于呈现内容给客户。
WTL提供的方法是在界面线程的逻辑中创建框架窗口,而视的创建由框架窗口负责。后面会介绍,框架窗口在处理WM_CREATE消息时创建视。
如果要创建一个框架窗口,需要:
从CFrameWindowImpl类派生你的框架窗口。
加入DECLARE_FRAME_WND_CLASS,指定菜单和工具栏的资源ID。
加入消息映射,同时把它与基类的消息映射联系起来。同时,加入消息处理函数。
下面是使用ATL/WTL App Wizard创建一个SDI应用程序的主框架窗口的申明。
class CMainFrame : public CFrameWindowImpl<CMainFrame>, public CUpdateUI<CMainFrame>, public CMessageFilter, public CIdleHandler { public: DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME) // 该框架窗口的视的实例 CView m_view; // 该框架窗口的命令工具行 CCommandBarCtrl m_CmdBar; virtual BOOL PreTranslateMessage(MSG* pMsg); virtual BOOL OnIdle(); BEGIN_UPDATE_UI_MAP(CMainFrame) UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP) UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP) END_UPDATE_UI_MAP() BEGIN_MSG_MAP(CMainFrame) MESSAGE_HANDLER(WM_CREATE, OnCreate) COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit) COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew) COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar) COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar) COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout) CHAIN_MSG_MAP(CUpdateUI<CMainFrame>) CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>) END_MSG_MAP() // Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) // LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) // LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/) LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/); LRESULT OnFileExit(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/); LRESULT OnFileNew(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/); LRESULT OnViewToolBar(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/); LRESULT OnViewStatusBar(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/); LRESULT OnAppAbout(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/); }; |
DECLARE_FRAME_WND_CLASS()宏是为框架窗口指定一个资源ID,可以通过这个ID和应用程序的资源联系起来,比如框架的图标,字符串表,菜单和工具栏等等。
WTL视
通常应用程序的显示区域分成两个部分。一是包含窗口标题,菜单,工具栏和状态栏的主框架窗口。另一部分就是被称为视的部分。这部分是客户区,用于呈现内容给客户。
视可以是包含HWND的任何东西。通过在框架窗口处理WM_CREATE时,将该HWND句柄赋植给主窗口的m_hWndClien成员来设置主窗口的视。
比如,在用ATL/WTL App Wizard创建了一个应用程序,会创建一个视,代码如下:
class CTestView : public CWindowImpl<CTestView> { public: DECLARE_WND_CLASS(NULL) BOOL PreTranslateMessage(MSG* pMsg); BEGIN_MSG_MAP(CTestView) MESSAGE_HANDLER(WM_PAINT, OnPaint) END_MSG_MAP() // Handler prototypes (uncomment arguments if needed): // LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) // LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) // LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/) LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/); }; |
这个视是一个从CWindowImpl派生的窗口。
在主窗口的创建函数中,将该视的HWND设置给主窗口的m_hWndClient成员。
LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { … … m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE); … … return 0; } |
上述代码为主窗口创建了视。
到此为止,我们已经从Win32模型开始,到了解windows界面程序封装以及WTL消息循环机制,详细分析了WTL。通过我们的分析,您是否对WTL有一个深入的理解,并能得心应手的开发出高质量的Windows应用程序?别急,随后,我们还将一起探讨开发WTL应用程序的技巧。
查看本文来源