我们又知道,Win32 API是面向过程的(虽然可以说Win是一个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() |
02 | { |
03 | CMFCWin* pMainWnd = new CMFCWin; |
04 | if (NULL == pMainWnd) |
05 | return FALSE; |
06
07 | CString strWndClass; |
08 | strWndClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW, |
09 | ::LoadCursor(NULL, IDC_ARROW), HBRUSH(COLOR_WINDOW+1), NULL); |
10
11 | HMENU hMenu = ::LoadMenu(NULL, MAKEINTRESOURCE(IDR_MAINFRAME)); |
12 | // Bruce:开始创建主窗口 |
13 | if (!pMainWnd->CreateEx(WS_EX_APPWINDOW, strWndClass, |
14 | _T("MFCBased without Wizzard"), WS_OVERLAPPEDWINDOW, |
15 | CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, |
16 | NULL, hMenu, NULL)) |
17 | { |
18 | AfxMessageBox(_T("CreateEx Failed!")); |
19 | return FALSE; |
20 | } |
21
22 | m_pMainWnd = pMainWnd; |
23 | pMainWnd->ShowWindow(m_nCmdShow); |
24 | pMainWnd->UpdateWindow(); |
25
26 | return TRUE; |
进入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) |
05 | { |
06 | // . . . |
07 | // Bruce:Hook? 好戏来了。 |
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); |
12
13 | } |
AfxHookWindowCreate,看上去是装了个Hook,进去看。
01 | void AFXAPI AfxHookWindowCreate(CWnd* pWnd) |
02 | { |
03 | // Bruce:这里取的什么数据? |
04 | _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); |
05 | if (pThreadState->m_pWndInit == pWnd) |
06 | return; |
07
08 | if (pThreadState->m_hHookOldCbtFilter == NULL) |
09 | { |
10 | // Bruce:果然,这里装了个WH_CBT钩子 |
11 | pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, |
12 | _AfxCbtFilterHook, NULL, ::GetCurrentThreadId()); |
13 | if (pThreadState->m_hHookOldCbtFilter == NULL) |
14 | AfxThrowMemoryException(); |
15 | } |
16 | // . . . |
17 | pThreadState->m_pWndInit = pWnd; |
18 | } |
先来说_afxThreadState.GetData(),_afxThreadState是一个全局CThreadLocal模板对象,是对TLS的封装,记录了线程相关的私有数据,_afxThreadState后面还会看到。接下来我们看到安装了一个WH_CBT钩子,_AfxCbtFilterHook是hook procedure,用来监视窗口的激活,创建,销毁等消息,也就是在窗口被激活,创建,销毁的时候系统会先调用这个函数。下面来看_AfxCbtFilterHook做了些什么。
01 | LRESULT CALLBACK |
02 | _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam) |
03 | { |
04 | _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); |
05 | if (code != HCBT_CREATEWND) // Bruce:只关心窗口创建消息,其他跳过 |
06 | { |
07 | // wait for HCBT_CREATEWND just pass others on... |
08 | return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, |
09 | wParam, lParam); |
10 | } |
11 | // . . . |
12 | LPCREATESTRUCT lpcs = ((LPCBT_CREATEWND)lParam)->lpcs; |
13 | // . . . |
14 | HWND hWnd = (HWND)wParam; |
15 | WNDPROC oldWndProc; |
16 | if (pWndInit != NULL) |
17 | { |
18 | AFX_MANAGE_STATE(pWndInit->m_pModuleState); |
19
20 | // Bruce:检查该窗口映射是否存在?后面说明这个函数 |
21 | ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL); |
22
23 | // Bruce:添加窗口映射(HWND CWnd) |
24 | pWndInit->Attach(hWnd); |
25 | // allow other subclassing to occur first |
26 | pWndInit->PreSubclassWindow(); |
27
28 | WNDPROC *pOldWndProc = pWndInit->GetSuperWndProcAddr(); |
29 | ASSERT(pOldWndProc != NULL); |
30
31 | // Bruce:subclass,重新设置窗口过程 |
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; |
38
39 | pThreadState->m_pWndInit = NULL; |
40 | } |
41 | // . . . |
42 | } |
这里可以看到,钩子函数仅监视窗口的创建,通过FromHandlePermanent/Attach完成了HWND到CWnd的映射,并重新设置了窗口过程。这个窗口过程视链接MFC的方式不同要么是AfxWndProcBase或AfxWndProc,即Windows想要的标准窗口过程。
01 | CWnd* PASCAL CWnd::FromHandlePermanent(HWND hWnd) |
02 | { |
03 | CHandleMap* pMap = afxMapHWND(); |
04 | CWnd* pWnd = NULL; |
05 | if (pMap != NULL) |
06 | { |
07 | // only look in the permanent map - does no allocations |
08 | pWnd = (CWnd*)pMap->LookupPermanent(hWnd); |
09 | ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd); |
10 | } |
11 | return pWnd; |
12 | } |
13
14 | BOOL CWnd::Attach(HWND hWndNew) |
15 | { |
16 | ASSERT(m_hWnd == NULL); // only attach once, detach on destroy |
17 | ASSERT(FromHandlePermanent(hWndNew) == NULL); |
18 | // must not already be in permanent map |
19
20 | if (hWndNew == NULL) |
21 | return FALSE; |
22
23 | CHandleMap* pMap = afxMapHWND(TRUE); // create map if not exist |
24 | ASSERT(pMap != NULL); |
25
26 | pMap->SetPermanent(m_hWnd = hWndNew, this); |
27 | // . . . |
28 | } |
通过查看FromHandlePermanent/Attach的实现,可以发现HWND到CWnd的映射建立在Map的机制上,另外,查看afxMapHWND()的实现,可以发现,这个映射关系也保存在线程的TLS中,因此有了MFC的一个先天限制,不能把一个MFC对象从某线程手上交给另一线程,也不能够在线程之间传递MFC对象指针(了解更多可以参考《Win32多线程程序设计》MFC多线程一章)。下面再看看窗口过程。
01 | WNDPROC AFXAPI AfxGetAfxWndProc() |
02 | { |
03 | #ifdef _AFXDLL // Bruce:Use MFC in a Shared DLL |
04 | return AfxGetModuleState()->m_pfnAfxWndProc; // Bruce:也就是AfxWndProcBase |
05 | #else // Bruce:Use MFC in a Static Library |
06 | return &AfxWndProc; |
07 | #endif |
08 | } |
09
10 | LRESULT CALLBACK |
11 | AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) |
12 | { |
13 | AFX_MANAGE_STATE(_afxBaseModuleState.GetData()); |
14 | return AfxWndProc(hWnd, nMsg, wParam, lParam); |
15 | } |
16
17 | LRESULT CALLBACK |
18 | AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) |
19 | { |
20 | // special message which identifies the window as using AfxWndProc |
21 | if (nMsg == WM_QUERYAFXWNDPROC) |
22 | return 1; |
23
24 | // Bruce:查找Map,找到HWND对应的CWnd实例 |
25 | CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); |
26 | ASSERT(pWnd != NULL); |
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); // Bruce:有了CWnd,可以调用窗口实例对应的窗口过程 |
31 | } |
32
33 | LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg, |
34 | WPARAM wParam = 0, LPARAM lParam = 0) |
35 | { |
36 | _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); |
37 | MSG oldState = pThreadState->m_lastSentMsg; // save for nesting |
38 | pThreadState->m_lastSentMsg.hwnd = hWnd; |
39 | pThreadState->m_lastSentMsg.message = nMsg; |
40 | pThreadState->m_lastSentMsg.wParam = wParam; |
41 | pThreadState->m_lastSentMsg.lParam = lParam; |
42
43 | #ifdef _DEBUG |
44 | _AfxTraceMsg(_T("WndProc"), &pThreadState->m_lastSentMsg); |
45 | #endif |
46
47 | // Catch exceptions thrown outside the scope of a callback |
48 | // in debug builds and warn the user. |
49 | LRESULT lResult; |
50 | TRY |
51 | { |
52 | // . . . |
53 | // Bruce:“终于找到属于我的窗口过程了!”,WindowProc是一个virtual function,注意与AfxWndProc的区别 |
54 | lResult = pWnd->WindowProc(nMsg, wParam, lParam); |
55 | } |
56 | // . . . |
57 | } |
到此,我们已经完整的看到了MFC封装窗口消息机制的过程,简单总结一下:
通过Hook技术安装一个WH_CBT钩子,在窗口创建时(必须在这个时候,这样才不会有漏网的窗口消息)建立HWND到窗口类实例(如CWnd实例)的映射关系,并保存在创建该窗口的线程的TLS中,然后在需要的时候从TLS中取出数据查找关系Map。MFC就是通过这样的方式解决了开始提出的第1个问题,而第2个问题则通过虚函数来实现。
二、ATL窗口消息封装机制
这里也手工写了一个简单的ATL窗口程序,基于CWindowImpl窗口类。开始调试。
01 | int APIENTRY _tWinMain( |
02 | HINSTANCE hInst, |
03 | HINSTANCE /*hInstPrev*/, |
04 | LPTSTR pszCmdLine, |
05 | int nCmdShow) |
06 | { |
07 | _Module.Init(0, hInst); |
08
09 | HMENU hMenu = ::LoadMenu(_Module.GetResourceInstance(), |
10 | MAKEINTRESOURCE(IDR_MENU)); |
11
12 | CATLWin mainWnd; |
13 | // Bruce:开始创建主窗口 |
14 | if (!mainWnd.Create(NULL, CWindow::rcDefault, |
15 | _T("ATLBased without Wizzard"), 0, 0, (UINT)hMenu)) |
16 | { |
17 | ::MessageBox(NULL, _T("Create Failed"), _T("ATLBased Error"), MB_OK); |
18 | return -1; |
19 | } |
20
21 | mainWnd.CenterWindow(); |
22 | mainWnd.ShowWindow(nCmdShow); |
23 | mainWnd.UpdateWindow(); |
24
25 | MSG msg; |
26 | while (::GetMessage(&msg, NULL, 0, 0)) |
27 | { |
28 | ::TranslateMessage(&msg); |
29 | ::DispatchMessage(&msg); |
30 | } |
31
32 | _Module.Term(); |
33 | return msg.wParam; |
34 | } |
依旧是从创建窗口开始,看看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) |
05 | { |
06 | if (T::GetWndClassInfo().m_lpszOrigName == NULL) |
07 | T::GetWndClassInfo().m_lpszOrigName = GetWndClassName(); |
08 | ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc); |
09
10 | dwStyle = T::GetWndStyle(dwStyle); |
11 | dwExStyle = T::GetWndExStyle(dwExStyle); |
12
13 | // . . . |
14
15 | return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rect, |
16 | szWindowName, dwStyle, dwExStyle, MenuOrID, atom, lpCreateParam); |
17 | } |
函数一开始通过GetWndClassInfo获取默认的WndClass并注册,GetWndClassInfo通过宏DECLARE_WND_CLASS实现,展开可以发现默认的窗口过程是StartWindowProc,继续看CWindowImplBaseT< TBase,TWinTraits >::Create。
01 | template |
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) |
06 | { |
07 | // . . . |
08 | // Allocate the thunk structure here, where we can fail gracefully. |
09 | result = m_thunk.Init(NULL,NULL); |
10 | // . . . |
11 | _AtlWinModule.AddCreateWndData(&m_thunk.cd, this); |
12 | // . . . |
13
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); |
20
21 | ATLASSUME(m_hWnd == hWnd); |
22
23 | return hWnd; |
24 | } |
我们看到这里开始使用了thunk成员变量m_thunk,thunk一般可以理解为转换,在这里是一组ASM指令。每个CWindowImpl实例有自己的m_thunk,这里暂不管thunk的技术实现,只需留意窗口this指针和ThreadId被安全的放进一个_AtlCreateWndData全局链表结构中。下面就来看窗口过程StartWindowProc。
01 | template |
02 | LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >:: |
03 | StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
04 | { |
05 | // Bruce:通过当前的ThreadId遍历_AtlCreateWndData链表,取得先前保存的窗口this指针 |
06 | CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData(); |
07 | // . . . |
08 | // Bruce:这里保存HWND以作后用,thunking过程中将被覆盖掉 |
09 | pThis->m_hWnd = hWnd; |
10
11 | // Bruce:将静态窗口过程(如果GetWindowProc没被改写,那么就是CWindowImplBaseT::WindowProc)和this指针初始化到thunk中, m_thunk.Init之后,hWnd被替换成this指针 |
12 | pThis->m_thunk.Init(pThis->GetWindowProc(), pThis); |
13 | // Bruce:这里返回的其实是一个_stdcallthunk结构体的首地址,后面会提及 |
14 | WNDPROC pProc = pThis->m_thunk.GetWNDPROC(); |
15 | // . . . |
16 | // Bruce:进入静态窗口过程 |
17 | return pProc(hWnd, uMsg, wParam, lParam); |
18 | } |
继续看CWindowImplBaseT::WindowProc。
01 | template |
02 | LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc( |
03 | HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) |
04 | { |
05 | // Bruce:hWnd变回this(thunk的功劳) |
06 | CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd; |
07 | // . . . |
08
09 | // Bruce:转换过程到这里结束,找到this的virtual成员函数处理窗口消息 |
10 | BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0); |
11 | // . . . |
12 | } |
到这里,大致看完了ATL为了解决第1个问题进行的转换过程,总结起来就是:通过thunk的作用,对hWnd做缓存后将Windows提供的hWnd替换成this指针,接着thunk把整个调用栈传递给真正的窗口过程(如果GetWindowProc没被改写,也就是CWindowImplBaseT::WindowProc)。
最后再来看有趣的thunk是怎么实现的。
01 | class CWndProcThunk |
02 | { |
03 | public: |
04 | _AtlCreateWndData cd; |
05 | CStdCallThunk thunk; |
06
07 | BOOL Init(WNDPROC proc, void* pThis) |
08 | { |
09 | return thunk.Init((DWORD_PTR)proc, pThis); |
10 | } |
11 | WNDPROC GetWNDPROC() |
12 | { |
13 | return (WNDPROC)thunk.GetCodeAddress(); |
14 | } |
15 | }; |
不管内存字节对齐方式,这里假设CStdCallThunk为_stdcallthunk,在_M_IX86平台下。
01 | #if defined(_M_IX86) |
02 | PVOID __stdcall __AllocStdCallThunk(VOID); |
03 | VOID __stdcall __FreeStdCallThunk(PVOID); |
04
05 | #pragma pack(push,1) // Bruce:1字节内存对齐方式 |
06 | struct _stdcallthunk |
07 | { |
08 | DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd) |
09 | DWORD m_this; // |
10 | BYTE m_jmp; // jmp WndProc |
11 | DWORD m_relproc; // relative jmp |
12 | BOOL Init(DWORD_PTR proc, void* pThis) |
13 | { |
14 | m_mov = 0x042444C7; //C7 44 24 0C |
15 | m_this = PtrToUlong(pThis); |
16 | m_jmp = 0xe9; |
17 | m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk))); |
18 | // write block from data cache and |
19 | // flush from instruction cache |
20 | FlushInstructionCache(GetCurrentProcess(), this,sizeof(_stdcallthunk)); |
21 | return TRUE; |
22 | } |
23 | //some thunks will dynamically allocate the memory for the code |
24 | void* GetCodeAddress() |
25 | { |
26 | return this; |
27 | } |
28 | void* operator new(size_t) |
29 | { |
30 | return __AllocStdCallThunk(); |
31 | } |
32 | void operator delete(void* pThunk) |
33 | { |
34 | __FreeStdCallThunk(pThunk); |
35 | } |
36 | }; |
37 | #pragma pack(pop) |
三、其他实现
除了MFC、ATL中使用的手法,还有另外一种简单的封装方式。且看代码,主要是窗口过程。
01 | LRESULT CALLBACK XWindow::WndProc(HWND hWnd, UINT uMsg, |
02 | WPARAM wParam, LPARAM lParam) |
03 | { |
04 | XWindow* pThis = NULL; |
05 | if (WM_NCCREATE == uMsg) |
06 | { |
07 | assert(!::IsBadReadPtr((void*)lParam, sizeof(CREATESTRUCT))); |
08 | LPCREATESTRUCT lpcs = reinterpret_cast(lParam); |
09 | pThis = static_cast(lpcs->lpCreateParams); |
10 | pThis->m_hWnd = hWnd; |
11
12 | assert(!::IsBadReadPtr(pThis, sizeof(XWindow))); |
13 | ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(pThis)); |
14 | } |
15 | else |
16 | pThis = reinterpret_cast(::GetWindowLongPtr(hWnd, GWLP_USERDATA)); |
17
18 | if (pThis) |
19 | return pThis->MsgProc(hWnd, uMsg, wParam, lParam); |
20 | else |
21 | return DefWindowProc(hWnd, uMsg, wParam, lParam); |
22 | } |
这种方式通过在收到 WM_NCCREATE消息时,将this指针保存在lParam参数中,lParam事实上是一个LPCREATESTRUCT结构指针。成功保存后,WM_NCCREATE之后的消息就可以取出this指针,调用对应的消息处理函数。一些UI库使用的是这种实现方式。
到此,分析结束,由于MFC、ATL的设计目标不同(MFC在于简单易用,ATL则是短小精悍),MFC、ATL采用了不同的实现手法,且各有优劣。MFC采用全局映射表的方式,由于需要查找,损耗一定的时间,并随着窗口数的增多而增长。ATL在效率上占优势,但增加了一些复杂性