MFC 对 Window 作了一层比较浅的封装,其创建过程基本与 SDK 差不多。它简化了窗口的注册,并将窗口过程与类关联起来;后者是比较重要的封装,它使消息能够分流 给“类的窗口过程”,既而通过消息映射,才能到达各个处理 函数。
使用传统的 SDK 来创建一个窗口有点繁琐,伪代码是这样的:
if (RegisterClass (…))
{
CreateWindowEx ( …);
ShowWindow ( …);
UpdateWindow ( …);
}
RegisterClass 根据一个 ClassName 注册一个窗口类,并指定窗口过程; CreateWindowEx 创建该窗口,其中的参数用于设置样式、位置等。
这段代码的繁琐之处其实在于函数的参数, CreateWindowEx 有 12 个参数,这常常让初学者望而生畏 。作为良好封装的窗口框架,一个重要任务是简化这些参数,让它成为可分别设置的属性。
窗口的创建
要看 MFC 如何创建一个窗口, CView 应该是最合适的了, CFrameWnd::CreateView 创建了一个视图窗口:
CWnd * CFrameWnd::CreateView ( CCreateContext * pContext , UINT nID )
{
CWnd * pView = (CWnd *)pContext ->m_pNewViewClass ->CreateObject ();
if (!pView ->Create(NULL, NULL, AFX_WS_DEFAULT_VIEW,
CRect ( 0,0,0,0), this, nID , pContext ) )
... ...
return pView ;
}
要创建 CView ,只需要一个 CWnd::Create 就可以了,不过这里面必然隐藏着一些事情,包括注册窗口类,指定窗口过程:
BOOL CWnd::Create 调用下面的函数
BOOL CWnd::CreateEx ( DWORD dwExStyle , LPCTSTR lpszClassName , ...)
{
// 设置创建结构
CREATESTRUCT cs ;
cs.dwExStyle = dwExStyle ;
cs.lpszClass = lpszClassName ;
... ...
// 创建之前做了什么?
PreCreateWindow ( cs );
// 创建窗口
AfxHookWindowCreate ( this);
HWND hWnd = :: CreateWindowEx (...);
AfxUnhookWindowCreate ( );
return TRUE;
}
CreateEx 直接调用 CreateWindowEx 将窗口创建出来,我们注意到上面调用 Create 时传给 lpszClassName 的是一个空值,那么窗口类在什么时候注册的呢,只有一个地方,那就是 PreCreateWindow 。 PreCreateWindow 是一个虚函数, CWnd 在这里默认地为窗口注册名字为 AfxWnd42 的窗口类,而派生类可以覆盖这个函数注册其他的窗口类名,比如 CView :
BOOL CView::PreCreateWindow ( CREATESTRUCT & cs )
{
if (cs.lpszClass == NULL)
{
VERIFY( AfxDeferRegisterClass (AFX_WNDFRAMEORVIEW_REG) );
cs.lpszClass = _afxWndFrameOrView ;
}
return TRUE;
}
CView 以 afxWndFrameOrView 注册窗口类,它的具体名字是: AfxFrameOrView42 。注册的行为就在 AfxDeferRegisterClass 里面,不过到这里我们可以打住了。
指定窗口过程
注册窗口类的时候要指定窗口过程, AfxDeferRegisterClass 是一个宏,会调用 AfxEndDeferRegisterClass 作具体的调用,在函数里看到这一句代码: wndcls.lpfnWndProc = DefWindowProc ; 窗口过程竟指定给系统的默认窗口过程,真正的窗口过程是在什么时候指定的呢?
在 CWnd::CreateEx 里面, CreateWindowEx 的前后各有一行代码,从字面上可以推断 MFC 监视了窗口的创建:
void AFXAPI AfxHookWindowCreate (CWnd * pWnd )
{
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData ( );
pThreadState ->m_hHookOldCbtFilter = ::SetWindowsHookEx (WH_CBT,
_AfxCbtFilterHook , NULL, ::GetCurrentThreadId ());
pThreadState ->m_pWndInit = pWnd ;
}
MFC 使用一个 CBT 钩子来监视窗口的创建,当 pWnd 的窗口句柄创建时, _AfxCbtFilterHook 将被调用,这个函数的用意已经非常明显,将新创建的窗口句柄附加到 pWnd ,并为 pWnd 指定窗口过程。这个函数较长,我就不列代码了,只是将大概的几点列举如下:
1. 通过 CWnd::Attach 将句柄附加给 pWnd , Attach 还做了另一件事,建立 pWnd 与句柄的哈唏 表,这是为了后面处理消息时可以找到正确的 CWnd 对象。哈唏 表在 MFC 中大量地被使用。
2. 子类化窗口过程,将窗口过程指定为 AfxWndProc ,并保存旧的窗口过程;通过 PreSubclassWindow ,你还可以指定自己的窗口过程,不过似乎只在 AfxWndProc 之后才能被调用。
由于整个进程的所有窗口创建都会先被 _AfxCbtFilterHook 钩住,所以里面也进行了一些过滤,比如 IME 窗口。这种情况对于多线程是否有效呢,也许 _afxThreadState 可以确保在多线程情况窗口创建的顺序,我并没有去深究。
总而言之,窗口过程最后被替换为 AfxWndProc :
LRESULT CALLBACK AfxWndProc ( HWND hWnd , UINT nMsg , WPARAM wParam , LPARAM lParam )
{
CWnd * pWnd = CWnd::FromHandlePermanent ( hWnd );
if (pWnd == NULL || pWnd ->m_hWnd != hWnd )
return ::DefWindowProc (hWnd , nMsg , wParam , lParam );
return AfxCallWndProc (pWnd , hWnd , nMsg , wParam , lParam );
}
通过 FromHandlePermanent 从哈唏 表找到与句柄对应的窗口类,执行点又进入 AfxCallWndProc ,接下来我们就会看到熟悉的 WindowProc ,也就是前面文章所说的消息处理的进入点:
LRESULT AFXAPI AfxCallWndProc ( CWnd * pWnd , HWND hWnd , UINT nMsg ,
WPARAM wParam = 0, LPARAM lParam = 0)
{
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData ( );
MSG oldState = pThreadState ->m_lastSentMsg ; // save for nesting
pThreadState ->m_lastSentMsg.hwnd = hWnd ;
pThreadState ->m_lastSentMsg.message = nMsg ;
pThreadState ->m_lastSentMsg.wParam = wParam ;
pThreadState ->m_lastSentMsg.lParam = lParam ;
LRESULT lResult ;
// delegate to object''s WindowProc
lResult = pWnd ->WindowProc (nMsg , wParam , lParam );
pThreadState ->m_lastSentMsg = oldState ;
return lResult ;
}
pThreadState 似乎与线程同步有关,从这一点可以看出 MFC 在多线程方面做得比 VCL 好得多,找个时间再来探索这个主题。
到此,窗口创建到消息处理就连惯起来 了。尽管不同的窗口类在处理上有些差异,但核心流程大概就是这样。
MFC 使用哈唏 表将窗口句柄与窗口类关联起来,很多 API 的对象与 MFC 的对象即是基于此关联起来的