# 何时创建窗口?
我们在直接使用 Win32API
开发窗口程序的时候,一般都是在消息循环之前调用相关的函数创建窗口,那么 MFC 的窗口是在何时被创建的呢?
实际上,我们重写了 CWinThread::InitInstance
成员虚函数,并在函数中编写了创建了窗口的代码;
根据我们之前分析的程序流程, CWinThread::InitInstance
的调用时机在 CWubApp::Run
之前,这自然也合情合理。
# 窗口的基本创建流程
还是之前的示例代码:
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <afxwin.h> class CMyFrameWnd :public CFrameWnd {
};
class CMyWinApp :public CWinApp { public: CMyWinApp() {
}
virtual BOOL InitInstance() { CMyFrameWnd* frame = new CMyFrameWnd; frame->Create(nullptr, L"MFCBase"); m_pMainWnd = frame; frame->ShowWindow(SW_SHOW); frame->UpdateWindow(); return TRUE; } };
CMyWinApp g_theApp;
|
CMyFrameWnd* frame = new CMyFrameWnd;
我们 new 了一个 CMyFrameWnd
对象,而 CMyFrameWnd
类继承自 CFrameWnd
类。
CFrameWnd
是对框架窗口的封装。
frame->Create(nullptr, L"MFCBase");
基于新建的 frame
,调用了 Create
成员函数,我们猜测内部应当是调用了 CreateWindow
之类创建窗口的函数。
m_pMainWnd
是 CWinThread
类的成员,指向当前线程的主窗口。
那么我们所做的其实就是,创建一个框架窗口,并使其成为 g_theApp
的主窗口。
读者可能会有些疑惑,线程和窗口又有什么关系?
实际上,Windows 产生的消息都是发送给线程的,每一个窗口都必须关联到一个线程中,一个线程可以被多个窗口关联。
消息循环就是线程的工作, GetMessage
从当前线程的消息队列中获取消息;
DispatchMessage
负责将消息再次派发给当前线程的所属窗口中对应的窗口 ( 回调窗口过程函数
)。
我们所定义的 g_theApp
,表示的是应用程序的主线程。
1 2
| frame->ShowWindow(SW_SHOW); frame->UpdateWindow();
|
这两行就更加明显了,猜测内部应当是调用了 ShowWindow
、 UpdateWindow
。
# 框架窗口的 Create 分析
我们在直接使用 Win32API
开发桌面应用时,创建窗口前会先注册窗口类、指定各种各样的字段和参数,创建窗口,十分繁琐。
而 MFC 为我们简化到只需要传递两个参数就能创建一个窗口,接下来我们进入这个函数内部一探究竟。
CFrameWnd::Create 第一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| BOOL CFrameWnd::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, LPCTSTR lpszMenuName, DWORD dwExStyle, CCreateContext* pContext) { HMENU hMenu = NULL; if (lpszMenuName != NULL) { HINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, ATL_RT_MENU); if ((hMenu = ::LoadMenu(hInst, lpszMenuName)) == NULL) { TRACE(traceAppMsg, 0, "Warning: failed to load menu for CFrameWnd.\n"); PostNcDestroy(); return FALSE; } }
|
进到函数中,发现参数其实还是不少的。
为什么我们只传递了两个呢?想必是 MFC 在成员函数的声明处,为后面的参数设置了缺省值。
if (lpszMenuName != NULL)
我们并没有指定这个参数,因此此参数使用缺省值 NULL
,跳过此部分。
CFrameWnd::Create 第二部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| m_strTitle = lpszWindowName;
if (!CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext)) { TRACE(traceAppMsg, 0, "Warning: failed to create CFrameWnd.\n"); if (hMenu != NULL) DestroyMenu(hMenu); return FALSE; }
return TRUE; }
|
关键调用明显就只有 CreateEx
这个成员函数了,继续深入。
CWnd::CreateEx 第一部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam) { ASSERT(lpszClassName == NULL || AfxIsValidString(lpszClassName) || AfxIsValidAtom(lpszClassName)); ENSURE_ARG(lpszWindowName == NULL || AfxIsValidString(lpszWindowName));
CREATESTRUCT cs; cs.dwExStyle = dwExStyle; cs.lpszClass = lpszClassName; cs.lpszName = lpszWindowName; cs.style = dwStyle; cs.x = x; cs.y = y; cs.cx = nWidth; cs.cy = nHeight; cs.hwndParent = hWndParent; cs.hMenu = nIDorHMenu; cs.hInstance = AfxGetInstanceHandle(); cs.lpCreateParams = lpParam;
if (!PreCreateWindow(cs)) { PostNcDestroy(); return FALSE; } AfxHookWindowCreate(this); HWND hWnd = CreateWindowEx(cs.dwExStyle, cs.lpszClass, cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy, cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
|
我们可以直接看到对全局函数 CreateWindowEx
的调用,说明在这之前应该存在注册窗口类的代码。
if (!PreCreateWindow(cs))
实际上窗口类的注册位于 PreCreateWindow
成员函数中,如果 cs.lpszClass
为 NULL
,则会指定一个不会重复的窗口类名并基于此窗口类名注册窗口类。
而 cs.lpszClass
的值正是 lpszClassName
参数,是我们在调用 CFrameWnd::Create
时传递的 NULL。
AfxEndDeferRegisterClass 部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| BOOL AFXAPI AfxEndDeferRegisterClass(LONG fToRegister) { AFX_MODULE_STATE* pModuleState = AfxGetModuleState(); fToRegister &= ~pModuleState->m_fRegisteredClasses; if (fToRegister == 0) return TRUE;
LONG fRegisteredClasses = 0;
WNDCLASS wndcls; memset(&wndcls, 0, sizeof(WNDCLASS)); wndcls.lpfnWndProc = DefWindowProc; wndcls.hInstance = AfxGetInstanceHandle(); wndcls.hCursor = afxData.hcurArrow;
|
这个函数是 PreCreateWindow
内部调用的函数, WNDCLASS
是我们比较熟悉的结构,即注册窗口类需要使用的结构体。
需要关注的是,我们创建的窗口,窗口过程函数在哪里?
在这里出现了答案, DefWindowProc
函数就是我们创建的窗口过程函数。
但是是不是有点不对劲? DefWindowProc
好像是 Windows 提供的默认窗口过程函数吧?
既然是 Windows 提供的函数实现,我们根本无法干涉,有消息也会回调这个函数。
显然,这是不正确的,那么我们暂时从 AfxEndDeferRegisterClass
函数退回到 CWnd::CreateEx
函数,继续我们的代码分析。
AfxHookWindowCreate(this);
这里调用了一个比较关键的函数,这也是MFC能再次拿到消息处理权的关键所在。
AfxHookWindowCreate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void AFXAPI AfxHookWindowCreate(CWnd* pWnd) { _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); if (pThreadState->m_pWndInit == pWnd) return;
if (pThreadState->m_hHookOldCbtFilter == NULL) { pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, _AfxCbtFilterHook, NULL, ::GetCurrentThreadId()); if (pThreadState->m_hHookOldCbtFilter == NULL) AfxThrowMemoryException(); } ASSERT(pThreadState->m_hHookOldCbtFilter != NULL); ASSERT(pWnd != NULL); ASSERT(pWnd->m_hWnd == NULL);
ASSERT(pThreadState->m_pWndInit == NULL); pThreadState->m_pWndInit = pWnd; }
|
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
这里获取了一个新的全局变量的地址,我们就叫它 线程状态
。
1 2
| pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
|
这里通过调用 SetWindowsHookEx
,告知 Windows,当前线程一旦收到某些消息时,提前回调 _AfxCbtFilterHook
函数,而 WM_CREATE
消息就在此列。
_AfxCbtFilterHook
又是 MFC 提供的全局函数,那么自当前函数调用完成后,消息处理权再次被 MFC 抓到了手里。
1
| pThreadState->m_pWndInit = pWnd;
|
使全局变量 线程状态
的成员 m_pWndInit
我们 new 出来的指向 frame
对象。
即设置待初始化窗口。
再次回到 CWnd::CreateEx
函数。
1 2 3
| HWND hWnd = CreateWindowEx(cs.dwExStyle, cs.lpszClass, cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy, cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
|
这一行就没有什么好说的了,创建窗口,同时会产生 WM_CREATE
消息,就会回调到 _AfxCbtFilterHook
。
所以我们要继续分析 _AfxCbtFilterHook
,以得知 MFC 如何处理消息。
# MFC 指定的消息钩子回调: _AfxCbtFilterHook
经过分析, _AfxCbtFilterHook
的基本工作如下:
- 为
frame
和窗口句柄建立映射关系
- 通过全局变量
线程状态
的成员 m_pWndInit
,得到 frame
。
- 通过窗口句柄查找对应的
CWnd
对象,由 CHandleMap
类实现
- 模块线程状态的成员
m_pmapHWND
指向了一个映射类对象,负责管理 HWND
到 CWnd*
的映射。
- 调用
SetWindowLongPtr
,为新创建的窗口设置新的窗口过程函数: AfxWndProc
。
即完成了对待初始化窗口的初始化工作。
具体代码就留给读者自行阅读了。
# 框架窗口类的消息接收
我们已经知晓窗口的窗口过程函数实际上是 AfxWndProc
,但这是 MFC 提供的全局函数,我们如何处理消息呢?
实际上,MFC 在 AfxWndProc
函数中,调用了 CWnd
类的 WindowProc
成员虚函数,因此,我们只需要重写该虚函数,就能拿到消息的处理权。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <afxwin.h>
class CMyFrameWnd :public CFrameWnd { virtual LRESULT WindowProc(UINT msgID, WPARAM wParam, LPARAM lParam) { switch (msgID) { case WM_CREATE: { AfxMessageBox(L"WM_CREATE"); break; } } return CFrameWnd::WindowProc(msgID, wParam, lParam); } };
class CMyWinApp :public CWinApp { public: CMyWinApp() {
}
virtual BOOL InitInstance() { CMyFrameWnd* frame = new CMyFrameWnd; frame->Create(nullptr, L"MFCBase"); m_pMainWnd = frame; frame->ShowWindow(SW_SHOW); frame->UpdateWindow(); return TRUE; } };
CMyWinApp g_theApp;
|
代码即重写了 WindowProc
成员虚函数,并在收到 WM_CREATE
消息时弹出信息框。
调试程序,会弹出信息框,内容是 WM_CREATE
,按下确定后,窗口才会出现。
# MFC 是如何找到我们的框架窗口类对象的?
我们的框架窗口类是一个全局对象,在窗口过程函数中,MFC是如何找到我们创建的对象,并调用其成员函数的呢?
还记得窗口过程的函数原型吗?第一个参数是类型为 HWND
的窗口句柄;
而我们在之前讲过,模块线程状态的成员 m_pmapHWND
指向了一个映射类对象,而 _AfxCbtFilterHook
函数中,会基于此映射类对象,维护从 HWND
到 CWnd*
的映射表。
MFC 在需要通过 HWND
查找到对应窗口类对象的地址时,也只需要访问这个映射类对象即可。
得到 frame
的地址后,就可以以此调用 WindowProc
成员虚函数。