# 何时创建窗口?

我们在直接使用 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 = frame;

m_pMainWndCWinThread 类的成员,指向当前线程的主窗口。
那么我们所做的其实就是,创建一个框架窗口,并使其成为 g_theApp 的主窗口。

读者可能会有些疑惑,线程和窗口又有什么关系?
实际上,Windows 产生的消息都是发送给线程的,每一个窗口都必须关联到一个线程中,一个线程可以被多个窗口关联。
消息循环就是线程的工作, GetMessage 从当前线程的消息队列中获取消息;
DispatchMessage 负责将消息再次派发给当前线程的所属窗口中对应的窗口 ( 回调窗口过程函数 )。
我们所定义的 g_theApp ,表示的是应用程序的主线程。

1
2
frame->ShowWindow(SW_SHOW);
frame->UpdateWindow();

这两行就更加明显了,猜测内部应当是调用了 ShowWindowUpdateWindow

# 框架窗口的 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)
{
// load in a menu that will get destroyed when window gets destroyed
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(); // perhaps delete the C++ object
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;    // save title for later

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));

// allow modification of several common create parameters
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.lpszClassNULL ,则会指定一个不会重复的窗口类名并基于此窗口类名注册窗口类。
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)
{
// mask off all classes that are already registered
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
fToRegister &= ~pModuleState->m_fRegisteredClasses;
if (fToRegister == 0)
return TRUE;

LONG fRegisteredClasses = 0;

// common initialization
WNDCLASS wndcls;
memset(&wndcls, 0, sizeof(WNDCLASS)); // start with NULL defaults
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); // only do once

ASSERT(pThreadState->m_pWndInit == NULL); // hook not already in progress
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 的基本工作如下:

  1. frame 和窗口句柄建立映射关系
    • 通过全局变量 线程状态 的成员 m_pWndInit ,得到 frame
    • 通过窗口句柄查找对应的 CWnd 对象,由 CHandleMap 类实现
      • 模块线程状态的成员 m_pmapHWND 指向了一个映射类对象,负责管理 HWNDCWnd* 的映射。
  2. 调用 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 函数中,会基于此映射类对象,维护从 HWNDCWnd* 的映射表。
MFC 在需要通过 HWND 查找到对应窗口类对象的地址时,也只需要访问这个映射类对象即可。

得到 frame 的地址后,就可以以此调用 WindowProc 成员虚函数。