http://www.brucesky.com/articles/242
新产品在不紧不慢的进行中,这应该是有史以来开发比较“自由”的一个项目。在折腾完一个功能服务器的demo之后,开始折腾起PC客户端。Leader说客户端界面需用ATL来实现。这时候可以满足一下客户端界面开发的兴趣,于是开始学习ATL界面开发,有人说做界面是个累人加无趣的体力活,但对于做界面的新手来说自得其乐,君子懂得善假于物的道理,短期不允许重新造轮子,于是使用了kui做铺垫。现在要讨论的与kui无关,只讨论MFC和ATL对窗口消息封装的实现手法。一切还是从调试源码过程中寻找答案,进入正题。
我们知道,一个标准Windows App的主要逻辑有:1.注册一个窗口类/窗口过程;2.创建该类窗口;3.显示、激活窗口;4.消息循环;5.处理该窗口感兴趣的消息。
我们又知道,Win32 API是面向过程的(虽然可以说Windows是一个OO系统),而我们希望可以利用Win32 API进行快乐的OOP(不需要重复上面的逻辑)编程,于是,我们需要包装API,封装Windows窗口。从上面的逻辑可以看出,要封装窗口主要需解决怎样封装窗口消息处理机制。由于交给Windows调用的标准窗口过程是全局/静态的,此时,将面临两个问题:
1.怎么知道将窗口过程中的消息转发给对应封装好的窗口类实例?(也就是HWND到对应窗口类实例的转换)
2.假设第1个问题解决了,怎样将消息传递给相应窗口类的实例?
重点是解决第1个问题,下面来看MFC和ATL分别是怎么来解决这两个问题。
一、MFC窗口消息封装机制
我们通过一个手工产生(not by wizzard)的最简单的MFC程序(基于CWinApp和CWnd的两个类,以省掉不必要的代码和麻烦)开始调试分析。
01 | BOOL CMFCApp::InitInstance() |
03 | CMFCWin* pMainWnd = new CMFCWin; |
08 | strWndClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW, |
09 | ::LoadCursor(NULL, IDC_ARROW), HBRUSH (COLOR_WINDOW+1), NULL); |
11 | HMENU hMenu = ::LoadMenu(NULL, MAKEINTRESOURCE(IDR_MAINFRAME)); |
13 | if (!pMainWnd->CreateEx(WS_EX_APPWINDOW, strWndClass, |
14 | _T( "MFCBased without Wizzard" ), WS_OVERLAPPEDWINDOW, |
15 | CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, |
18 | AfxMessageBox(_T( "CreateEx Failed!" )); |
22 | m_pMainWnd = pMainWnd; |
23 | pMainWnd->ShowWindow(m_nCmdShow); |
24 | pMainWnd->UpdateWindow(); |
进入pMainWnd->CreateEx:
01 | BOOL CWnd::CreateEx( DWORD dwExStyle, LPCTSTR lpszClassName, |
02 | LPCTSTR lpszWindowName, DWORD dwStyle, |
03 | int x, int y, int nWidth, int nHeight, |
04 | HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam) |
08 | AfxHookWindowCreate( this ); |
09 | HWND hWnd = ::AfxCtxCreateWindowEx(cs.dwExStyle, cs.lpszClass, |
10 | cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy, |
11 | cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams); |
AfxHookWindowCreate,看上去是装了个Hook,进去看。
01 | void AFXAPI AfxHookWindowCreate(CWnd* pWnd) |
04 | _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); |
05 | if (pThreadState->m_pWndInit == pWnd) |
08 | if (pThreadState->m_hHookOldCbtFilter == NULL) |
11 | pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, |
12 | _AfxCbtFilterHook, NULL, ::GetCurrentThreadId()); |
13 | if (pThreadState->m_hHookOldCbtFilter == NULL) |
14 | AfxThrowMemoryException(); |
17 | pThreadState->m_pWndInit = pWnd; |
先来说_afxThreadState.GetData(),_afxThreadState是一个全局CThreadLocal模板对象,是对TLS的封装,记录了线程相关的私有数据,_afxThreadState后面还会看到。接下来我们看到安装了一个WH_CBT钩子,_AfxCbtFilterHook是hook procedure,用来监视窗口的激活,创建,销毁等消息,也就是在窗口被激活,创建,销毁的时候系统会先调用这个函数。下面来看_AfxCbtFilterHook做了些什么。
02 | _AfxCbtFilterHook( int code, WPARAM wParam, LPARAM lParam) |
04 | _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); |
05 | if (code != HCBT_CREATEWND) |
08 | return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, |
12 | LPCREATESTRUCT lpcs = ((LPCBT_CREATEWND)lParam)->lpcs; |
14 | HWND hWnd = ( HWND )wParam; |
18 | AFX_MANAGE_STATE(pWndInit->m_pModuleState); |
21 | ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL); |
24 | pWndInit->Attach(hWnd); |
26 | pWndInit->PreSubclassWindow(); |
28 | WNDPROC *pOldWndProc = pWndInit->GetSuperWndProcAddr(); |
29 | ASSERT(pOldWndProc != NULL); |
32 | WNDPROC afxWndProc = AfxGetAfxWndProc(); |
33 | oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, |
34 | ( DWORD_PTR )afxWndProc); |
35 | ASSERT(oldWndProc != NULL); |
36 | if (oldWndProc != afxWndProc) |
37 | *pOldWndProc = oldWndProc; |
39 | pThreadState->m_pWndInit = NULL; |
这里可以看到,钩子函数仅监视窗口的创建,通过FromHandlePermanent/Attach完成了HWND到CWnd的映射,并重新设置了窗口过程。这个窗口过程视链接MFC的方式不同要么是AfxWndProcBase或AfxWndProc,即Windows想要的标准窗口过程。
01 | CWnd* PASCAL CWnd::FromHandlePermanent( HWND hWnd) |
03 | CHandleMap* pMap = afxMapHWND(); |
08 | pWnd = (CWnd*)pMap->LookupPermanent(hWnd); |
09 | ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd); |
14 | BOOL CWnd::Attach( HWND hWndNew) |
16 | ASSERT(m_hWnd == NULL); |
17 | ASSERT(FromHandlePermanent(hWndNew) == NULL); |
23 | CHandleMap* pMap = afxMapHWND(TRUE); |
26 | pMap->SetPermanent(m_hWnd = hWndNew, this ); |
通过查看FromHandlePermanent/Attach的实现,可以发现HWND到CWnd的映射建立在Map的机制上,另外,查看afxMapHWND()的实现,可以发现,这个映射关系也保存在线程的TLS中,因此有了MFC的一个先天限制,不能把一个MFC对象从某线程手上交给另一线程,也不能够在线程之间传递MFC对象指针(了解更多可以参考《Win32多线程程序设计》MFC多线程一章)。下面再看看窗口过程。
01 | WNDPROC AFXAPI AfxGetAfxWndProc() |
03 | #ifdef _AFXDLL // Bruce:Use MFC in a Shared DLL |
04 | return AfxGetModuleState()->m_pfnAfxWndProc; |
05 | #else // Bruce:Use MFC in a Static Library |
11 | AfxWndProcBase( HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) |
13 | AFX_MANAGE_STATE(_afxBaseModuleState.GetData()); |
14 | return AfxWndProc(hWnd, nMsg, wParam, lParam); |
18 | AfxWndProc( HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) |
21 | if (nMsg == WM_QUERYAFXWNDPROC) |
25 | CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); |
27 | ASSERT(pWnd==NULL || pWnd->m_hWnd == hWnd); |
28 | if (pWnd == NULL || pWnd->m_hWnd != hWnd) |
29 | return ::DefWindowProc(hWnd, nMsg, wParam, lParam); |
30 | return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam); |
33 | LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg, |
34 | WPARAM wParam = 0, LPARAM lParam = 0) |
36 | _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); |
37 | MSG oldState = pThreadState->m_lastSentMsg; |
38 | pThreadState->m_lastSentMsg.hwnd = hWnd; |
39 | pThreadState->m_lastSentMsg.message = nMsg; |
40 | pThreadState->m_lastSentMsg.wParam = wParam; |
41 | pThreadState->m_lastSentMsg.lParam = lParam; |
44 | _AfxTraceMsg(_T( "WndProc" ), &pThreadState->m_lastSentMsg); |
54 | lResult = pWnd->WindowProc(nMsg, wParam, lParam); |
到此,我们已经完整的看到了MFC封装窗口消息机制的过程,简单总结一下:
通过Hook技术安装一个WH_CBT钩子,在窗口创建时(必须在这个时候,这样才不会有漏网的窗口消息)建立HWND到窗口类实例(如CWnd实例)的映射关系,并保存在创建该窗口的线程的TLS中,然后在需要的时候从TLS中取出数据查找关系Map。MFC就是通过这样的方式解决了开始提出的第1个问题,而第2个问题则通过虚函数来实现。
二、ATL窗口消息封装机制
这里也手工写了一个简单的ATL窗口程序,基于CWindowImpl窗口类。开始调试。
01 | int APIENTRY _tWinMain( |
07 | _Module.Init(0, hInst); |
09 | HMENU hMenu = ::LoadMenu(_Module.GetResourceInstance(), |
10 | MAKEINTRESOURCE(IDR_MENU)); |
14 | if (!mainWnd.Create(NULL, CWindow::rcDefault, |
15 | _T( "ATLBased without Wizzard" ), 0, 0, ( UINT )hMenu)) |
17 | ::MessageBox(NULL, _T( "Create Failed" ), _T( "ATLBased Error" ), MB_OK); |
21 | mainWnd.CenterWindow(); |
22 | mainWnd.ShowWindow(nCmdShow); |
23 | mainWnd.UpdateWindow(); |
26 | while (::GetMessage(&msg, NULL, 0, 0)) |
28 | ::TranslateMessage(&msg); |
29 | ::DispatchMessage(&msg); |
依旧是从创建窗口开始,看看CWindowImpl::Create做了什么。
01 | HWND Create( HWND hWndParent, _U_RECT rect = NULL, |
02 | LPCTSTR szWindowName = NULL, |
03 | DWORD dwStyle = 0, DWORD dwExStyle = 0, |
04 | _U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL) |
06 | if (T::GetWndClassInfo().m_lpszOrigName == NULL) |
07 | T::GetWndClassInfo().m_lpszOrigName = GetWndClassName(); |
08 | ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc); |
10 | dwStyle = T::GetWndStyle(dwStyle); |
11 | dwExStyle = T::GetWndExStyle(dwExStyle); |
15 | return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rect, |
16 | szWindowName, dwStyle, dwExStyle, MenuOrID, atom, lpCreateParam); |
函数一开始通过GetWndClassInfo获取默认的WndClass并注册,GetWndClassInfo通过宏DECLARE_WND_CLASS实现,展开可以发现默认的窗口过程是StartWindowProc,继续看CWindowImplBaseT< TBase, TWinTraits >::Create。
02 | HWND CWindowImplBaseT< TBase, TWinTraits >::Create( |
03 | HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName, |
04 | DWORD dwStyle, DWORD dwExStyle, _U_MENUorID MenuOrID, |
05 | ATOM atom, LPVOID lpCreateParam) |
09 | result = m_thunk.Init(NULL,NULL); |
11 | _AtlWinModule.AddCreateWndData(&m_thunk.cd, this ); |
14 | HWND hWnd = ::CreateWindowEx(dwExStyle, MAKEINTATOM(atom), |
15 | szWindowName,dwStyle, rect.m_lpRect->left, rect.m_lpRect->top, |
16 | rect.m_lpRect->right - rect.m_lpRect->left, |
17 | rect.m_lpRect->bottom - rect.m_lpRect->top, |
18 | hWndParent, MenuOrID.m_hMenu, |
19 | _AtlBaseModule.GetModuleInstance(), lpCreateParam); |
21 | ATLASSUME(m_hWnd == hWnd); |
我们看到这里开始使用了thunk成员变量m_thunk,thunk一般可以理解为转换,在这里是一组ASM指令。每个CWindowImpl实例有自己的m_thunk,这里暂不管thunk的技术实现,只需留意窗口this指针和ThreadId被安全的放进一个_AtlCreateWndData全局链表结构中。下面就来看窗口过程StartWindowProc。
02 | LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >:: |
03 | StartWindowProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
06 | CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData(); |
12 | pThis->m_thunk.Init(pThis->GetWindowProc(), pThis); |
14 | WNDPROC pProc = pThis->m_thunk.GetWNDPROC(); |
17 | return pProc(hWnd, uMsg, wParam, lParam); |
继续看CWindowImplBaseT::WindowProc。
02 | LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc( |
03 | HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
06 | CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd; |
10 | BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0); |
到这里,大致看完了ATL为了解决第1个问题进行的转换过程,总结起来就是:通过thunk的作用,对hWnd做缓存后将Windows提供的hWnd替换成this指针,接着thunk把整个调用栈传递给真正的窗口过程(如果GetWindowProc没被改写,也就是CWindowImplBaseT::WindowProc)。
最后再来看有趣的thunk是怎么实现的。
07 | BOOL Init(WNDPROC proc, void * pThis) |
09 | return thunk.Init(( DWORD_PTR )proc, pThis); |
13 | return (WNDPROC)thunk.GetCodeAddress(); |
不管内存字节对齐方式,这里假设CStdCallThunk为_stdcallthunk,在_M_IX86平台下。
02 | PVOID __stdcall __AllocStdCallThunk( VOID ); |
03 | VOID __stdcall __FreeStdCallThunk( PVOID ); |
05 | #pragma pack(push,1) // Bruce:1字节内存对齐方式 |
12 | BOOL Init( DWORD_PTR proc, void * pThis) |
15 | m_this = PtrToUlong(pThis); |
17 | m_relproc = DWORD (( INT_PTR )proc - (( INT_PTR ) this + sizeof (_stdcallthunk))); |
20 | FlushInstructionCache(GetCurrentProcess(), this , sizeof (_stdcallthunk)); |
24 | void * GetCodeAddress() |
28 | void * operator new ( size_t ) |
30 | return __AllocStdCallThunk(); |
32 | void operator delete ( void * pThunk) |
34 | __FreeStdCallThunk(pThunk); |
三、其他实现
除了MFC、ATL中使用的手法,还有另外一种简单的封装方式。且看代码,主要是窗口过程。
01 | LRESULT CALLBACK XWindow::WndProc( HWND hWnd, UINT uMsg, |
02 | WPARAM wParam, LPARAM lParam) |
04 | XWindow* pThis = NULL; |
05 | if (WM_NCCREATE == uMsg) |
07 | assert (!::IsBadReadPtr(( void *)lParam, sizeof (CREATESTRUCT))); |
08 | LPCREATESTRUCT lpcs = reinterpret_cast (lParam); |
09 | pThis = static_cast (lpcs->lpCreateParams); |
12 | assert (!::IsBadReadPtr(pThis, sizeof (XWindow))); |
13 | ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast (pThis)); |
16 | pThis = reinterpret_cast (::GetWindowLongPtr(hWnd, GWLP_USERDATA)); |
19 | return pThis->MsgProc(hWnd, uMsg, wParam, lParam); |
21 | return DefWindowProc(hWnd, uMsg, wParam, lParam); |
这种方式通过在收到 WM_NCCREATE消息时,将this指针保存在lParam参数中,lParam事实上是一个LPCREATESTRUCT结构指针。成功保存后,WM_NCCREATE之后的消息就可以取出this指针,调用对应的消息处理函数。一些UI库使用的是这种实现方式。
到此,分析结束,由于MFC、ATL的设计目标不同(MFC在于简单易用,ATL则是短小精悍),MFC、ATL采用了不同的实现手法,且各有优劣。MFC采用全局映射表的方式,由于需要查找,损耗一定的时间,并随着窗口数的增多而增长。ATL在效率上占优势,但增加了一些复杂性。
本文调试源码:brucesky_window_bind_comp