MFC内部机制探秘

时间:2021-11-27 05:05:26

由于MFC应用程序涵盖了基于SDK的windows程序几乎所有的功能,所以使用MFC AppWizard创建的MFC程序将自动具有WIndows程序的基本功能,我们今天就来探寻一下MFC的框架机制。

首先大家先利用向导制动建立一个基于MFC的单文档应用程序。我取名为MFC_DISCOVER,方便大家自己验证自己的程序。

1.声明全局对象

利用应用程序对象theApp启动应用程序,theApp一般在应用程序类的实现文件中声明,而且为全局对象。在类视图点击CMFC_DISCOVERApp,再点击下面的构造成员函数,
MFC内部机制探秘
然后在函数对应编辑框中会找到这样一句话:
MFC内部机制探秘
声明该类的变量显然会调用该类的构造函数,构造函数就在声明该变量的上面,但是我们知道CMFC_DISCOVER类派生于CWinApp类,所以会先调用基类的构造函数,
MFC内部机制探秘
点击上图中的CWinApp类右键,点击“转到定义”,可以查看到CWinApp类的构造函数
MFC内部机制探秘

// Constructor
explicit CWinApp(LPCTSTR lpszAppName = NULL);

然而我们发现该类依然有派生类CWinThread,继续按照上面做发现有这样的关系:

CObeject->CCmdTarget->CWinTread
我就不一一截图了,我们知道是在这个过程中进入WinMain函数的,那么WinMain在哪儿呢?
我们在项目管理搜索栏目里面输入WinMain,搜索如下:
MFC内部机制探秘
在tchar.h文件中发现有这样一个定义,也就是说_tWinMain就是WinMain,现在我们需要找到_tWinMain函数。不要设断点,直接按F10键,我们就看到了_tWinMain函数了:
MFC内部机制探秘
这个函数在MFC的源文件appmodul.cpp中,那这个main怎么被MFC调用的呢?你看那注释,是linkage to this module,也就是被链接器去调用的.准确说是被C-Runtime DLL,C运行时动态链接库调用的.


我们知道在MFC中能从代码里看到的入口点是定义一个全局的继承于CWinApp的类.这样定义下.在C++中全局变量是先于main被执行的,所以先初始化theApp后才接着调用main,WinMain里面又调用了AfxWinMain函数

2.AfxWinMain

要查看AFxWinMain函数的源码,就需要一步你的安装目录下啦,找到安装目录->vc98->MFC->Src(MFC的部分源代码) 搜索 WinMain,然后打开,就会有下面的代码(不要改动这个代码

int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);

int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();

// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;

// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;

// Perform specific initializations
if (!pThread->InitInstance())//调用******InitIncetance函数**********
{
if (pThread->m_pMainWnd != NULL)
{
TRACE(traceAppMsg, 0, "Warning: Destroying non-NULL m_pMainWnd\n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();//******进入消息循环*********

InitFailure:
#ifdef _DEBUG
// Check for missing AfxLockTempMap calls
if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
{
TRACE(traceAppMsg, 0, "Warning: Temp map lock count non-zero (%ld).\n",
AfxGetModuleThreadState()->m_nTempMapLock);
}
AfxLockTempMaps();
AfxUnlockTempMaps(-1);
#endif

AfxWinTerm();
return nReturnCode;
}

在AfxWInMain函数中,通过InitInstance函数完成吧了对窗口的创建步骤:设计窗口类,注册窗口类,创建窗口,显示窗口,更新窗口,消息循环,以及窗口函数等。
**根据多态性原理,AfxWinMain函数实际上是调用CMFC_DISCOVERApp的InitInstance函数来完成应用程序的一些初始化工作,包括窗口类的注册,创建,显示和更新等。
InitInstance函数代码如下:

BOOL CMFC_DISCOVERApp::InitInstance()
{
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControlsEx()。否则,将无法创建窗口。
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// 将它设置为包括所有要在应用程序中使用的
// 公共控件类。
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);

CWinApp::InitInstance();


// 初始化 OLE 库
if (!AfxOleInit())
{
AfxMessageBox(IDP_OLE_INIT_FAILED);
return FALSE;
}

AfxEnableControlContainer();

EnableTaskbarInteraction(FALSE);

// 使用 RichEdit 控件需要 AfxInitRichEdit2()
// AfxInitRichEdit2();

// 标准初始化
// 如果未使用这些功能并希望减小
// 最终可执行文件的大小,则应移除下列
// 不需要的特定初始化例程
// 更改用于存储设置的注册表项
// TODO: 应适当修改该字符串,
// 例如修改为公司或组织名
SetRegistryKey(_T("应用程序向导生成的本地应用程序"));
LoadStdProfileSettings(4); // 加载标准 INI 文件选项(包括 MRU)


// 注册应用程序的文档模板。文档模板
// 将用作文档、框架窗口和视图之间的连接
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMFC_DISCOVERDoc),
RUNTIME_CLASS(CMainFrame), // 主 SDI 框架窗口
RUNTIME_CLASS(CMFC_DISCOVERView));
if (!pDocTemplate)
return FALSE;
AddDocTemplate(pDocTemplate);


// 分析标准 shell 命令、DDE、打开文件操作的命令行
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);



// 调度在命令行中指定的命令。如果
// 用 /RegServer、/Register、/Unregserver 或 /Unregister 启动应用程序,则返回 FALSE。
if (!ProcessShellCommand(cmdInfo))
return FALSE;

// 唯一的一个窗口已初始化,因此显示它并对其进行更新
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}

不需要用完全看完,注意到在上面的代码中有这句:

if (!ProcessShellCommand(cmdInfo))
return FALSE;

程序会通过上面的代码调用CMainFram::LoadFrame函数进行注册和创建窗口。

3.LoadFrame

该函数具体代码如下:


BOOL CFrameWnd::LoadFrame(UNIT nIDResource, DWORD dwDefaultStyle, CWnd* pParentWnd, CCreateContext* pContext)
{
/*主窗口的菜单、图标、加速键、及标题都以nIDResource标识。除创建窗口外,还要做许多工作,如设置帮助上下文ID、装入加速键、初始化子窗口。所以在文档/视图框架程序中,总是使用LoadFrame()创建主窗口。*/
ASSERT_VALID_IDR(nIDResource);
ASSERT(m_nIDHelp == 0 || m_nIDHelp == nIDResource);
m_nIDHelp = nIDResource; // ID for help context (+HID_BASE_RESOURCE)
CString strFullString;
if (strFullString.LoadString(nIDResource))
AfxEXtractSubString(m_strTitle, strFullString, 0); //取得窗口标题
VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));//**装入图标,注册窗口类**
LPCTSTR lpszClass = GetIconWndClass(dwDefaultStyle, nIDResource);
LPCTSTR lpszTitle = m_strTitle;//调用CFrameWnd::Create()
if (!Create(lpszClass, lpszTitle, dwDefaultStyle, rectDefault, pParentWnd, MAKEINTRESOURCE(nIDResource), 0L, pContext))//**创建窗口**
return FALSE;//存储菜单句柄
ASSERT(m_hWnd != NULL);
m_hMenuDefault = ::GetMenu(m_hWnd);//装入加速键
LoadAccelTable(MAKEINTRESOURCE(nIDResource));
if (pContext == NULL) //初始化子窗口
SendMessageToDescendants(WM_INITIALUPDATE, 0, 0, TRUE, TRUE);
return TRUE;
}

在上面的代码中,首先通过AfxEndDeferRegisterClass函数实现窗口注册,再调用Create函数实现窗口创建。该函数定义如下:

BOOL CFrameWnd::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, LPCTSTR lpszMenuName, DWORD dwExStyle, CCreateContext* pContext)
{
/*可见,参数列表与CWnd::Create()稍有不同。因为目的是创建主窗口,所以第6个参数要求菜单资源名*/
HMENU hMenu = NULL;
if (lpszMenuName != NULL)
{
//搜索包含该菜单资源的实例(当前进程或者按进行装入的DLL)
HINSTANCE hInst = AfxFindResourceHandl(lpszMenuName, RT_MENU);
//装入菜单资源
if ((hMenu = ::LoadMenu(hInst, lpszMenuName)) == NULL)
{
TRACE0(“Warning: failed to load menu for CFrameWnd.\n”);
PostNcDestroy(); //perhaps delete to C++ object
return FALSE;
}
}
m_strTitle = lpszWindowName; //存储窗口标题,以备后用(如刷新显示)
// 调用CWnd::CreateEx()
if (!CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle, rect.left, rect.top, rect.right – rect.left, rect,bottom – rect.top, pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext))
{
if (hMenu != NULL)
DestroyMenu(hMenu); //如果创建失败,释放菜单资源
return FALSE;
}
return TRUE;
}

其中又调用了CreateEx函数来完成实际的创建工作,

4.CreateEx

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

#ifdef _DEBUG
if (hWnd == NULL)
{
TRACE1("Warning: Window creation failed: GetLastError returns 0x%8.8X/n",
GetLastError());
}
#endif

if (!AfxUnhookWindowCreate())//挂钩结束
PostNcDestroy(); // cleanup if CreateWindowEx fails too soon

if (hWnd == NULL)
return FALSE;
ASSERT(hWnd == m_hWnd); // should have been set in send msg hook
return TRUE;
}

我们知道创建窗口、显示窗口是需要响应WM_PAINT消息的,也就是调用窗口函数,从上面的代码,我们看不出调用了DefWindowProc函数的代码,它是隐藏在AfxHookWindowCreate函数之中,该函数源码如下:

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,//设置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;
}

在CreateEX源码中我们看到AfxHookWindowCreate函数是先于CreatWindowEx函数被调用的,CreateWindowEx是创建真正的窗口对象函数,AfxHookWindowCreate调用了SetWindowHookEx函数设置了钩子,这样有满足设置的消息时,系统就发送给设置函数。这样每次创建窗口时,该函数就将窗口函数修改为AfxWinProc函数,这是消息循环的起点。
这里我们发现创建了一个CBT钩子,回调函数为_AfxCbtFilterHook,这个函数有点长,我们来看一下(看有注释的地方即可)

LRESULT CALLBACK
_AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
if (code != HCBT_CREATEWND)//专门用来钩创建窗口的钩子
{
// wait for HCBT_CREATEWND just pass others on...
return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code,
wParam, lParam);
}

ASSERT(lParam != NULL);
LPCREATESTRUCT lpcs = ((LPCBT_CREATEWND)lParam)->lpcs;
ASSERT(lpcs != NULL);

CWnd* pWndInit = pThreadState->m_pWndInit;
BOOL bContextIsDLL = afxContextIsDLL;
if (pWndInit != NULL || (!(lpcs->style & WS_CHILD) && !bContextIsDLL))
{
// Note: special check to avoid subclassing the IME window
if (_afxDBCS)
{
// check for cheap CS_IME style first...
if (GetClassLong((HWND)wParam, GCL_STYLE) & CS_IME)
goto lCallNextHook;

// get class name of the window that is being created
LPCTSTR pszClassName;
TCHAR szClassName[_countof("ime")+1];
if (DWORD_PTR(lpcs->lpszClass) > 0xffff)
{
pszClassName = lpcs->lpszClass;
}
else
{
szClassName[0] = '\0';
GlobalGetAtomName((ATOM)lpcs->lpszClass, szClassName, _countof(szClassName));
pszClassName = szClassName;
}

// a little more expensive to test this way, but necessary...
if (::AfxInvariantStrICmp(pszClassName, _T("ime")) == 0)
goto lCallNextHook;
}

ASSERT(wParam != NULL); // should be non-NULL HWND
HWND hWnd = (HWND)wParam;
WNDPROC oldWndProc;//用于保存原window回调函数
if (pWndInit != NULL)
{
AFX_MANAGE_STATE(pWndInit->m_pModuleState);

// the window should not be in the permanent map at this time
ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL);

// connect the HWND to pWndInit...
pWndInit->Attach(hWnd);
// allow other subclassing to occur first
pWndInit->PreSubclassWindow();

WNDPROC *pOldWndProc = pWndInit->GetSuperWndProcAddr();
ASSERT(pOldWndProc != NULL);

// subclass the window with standard AfxWndProc
WNDPROC afxWndProc = AfxGetAfxWndProc();
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC,
(DWORD_PTR)afxWndProc);//设置新的回调函数afxWndProc
ASSERT(oldWndProc != NULL);
if (oldWndProc != afxWndProc)
*pOldWndProc = oldWndProc;

pThreadState->m_pWndInit = NULL;
}
else
{
ASSERT(!bContextIsDLL); // should never get here

static ATOM s_atomMenu = 0;
bool bSubclass = true;

if (s_atomMenu == 0)
{
WNDCLASSEX wc;
memset(&wc, 0, sizeof(WNDCLASSEX));
wc.cbSize = sizeof(WNDCLASSEX);
s_atomMenu = (ATOM)::AfxCtxGetClassInfoEx(NULL, _T("#32768"), &wc);
}

// Do not subclass menus.
if (s_atomMenu != 0)
{
ATOM atomWnd = (ATOM)::GetClassLongPtr(hWnd, GCW_ATOM);
if (atomWnd == s_atomMenu)
bSubclass = false;
}
else
{
TCHAR szClassName[256];
if (::GetClassName(hWnd, szClassName, 256))
{
szClassName[255] = NULL;
if (_tcscmp(szClassName, _T("#32768")) == 0)
bSubclass = false;
}
}
if (bSubclass)
{
// subclass the window with the proc which does gray backgrounds
oldWndProc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC);
if (oldWndProc != NULL && GetProp(hWnd, _afxOldWndProc) == NULL)
{
SetProp(hWnd, _afxOldWndProc, oldWndProc);
if ((WNDPROC)GetProp(hWnd, _afxOldWndProc) == oldWndProc)
{
GlobalAddAtom(_afxOldWndProc);
SetWindowLongPtr(hWnd, GWLP_WNDPROC, (DWORD_PTR)_AfxActivationWndProc);
ASSERT(oldWndProc != NULL);
}
}
}
}
}

lCallNextHook:
LRESULT lResult = CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code,
wParam, lParam);

#ifndef _AFXDLL
if (bContextIsDLL)
{
::UnhookWindowsHookEx(pThreadState->m_hHookOldCbtFilter);
pThreadState->m_hHookOldCbtFilter = NULL;
}
#endif
return lResult;
}

接下来就可以准备进入消息循环了


5.消息循环

大家回到前面的AfxWinMain函数中可以看到有这么一行代码:

nReturnCode = pThread->Run();//******进入消息循环*********

Run函数的源码如下:

virtual int CWinThread::Run()
{
ASSERT_VALID(this);
//是否要做空闲处理
BOOL bIdle = TRUE;
//用户记录空闲处理已经连接执行的次数
LONG lIdleCount = 0;
//acquire and dispatch messages until a WM_QUIT message is received.
//消息循环
for (;;)
{
//如果空闲状态为真,且消息队列为空,则进行空闲处理
while(bIdle && !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
{
//PeekMessage()用于检查消息队列是否为空,但并不处理消息
//调用空闲处理程序,如果返回零,说明空闲处理已全部完成
if (!OnIdle(lIdleCount++))
bIdle = FALSE;
}
//空闲处理循环结束,进入消息泵循环
do
{
//调用消息泵,提取消息并分发消息
//如果收到WM_QUIT消息,则退出消息循环
if (!PumpMessage())
return ExitInstance();
//根据刚处理的消息类型,判断是否应该在没有消息到来时立即进行空闲处理
if (IsIdleMessage(&m_msgCur))
{
bIdle = TRUE;
//在重新进行空闲处理前,清空空闲处理的执行次数
lIdleCount = 0;
}
}while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
}
ASSERT(FALSE); //不可能执行的语句
}

可以看到进入消息循环后,由PumpMessage函数处理消息,是处理消息的关键,源码如下:

//省略了调试信息的输出功能
BOOL CWinThread::PumpMessage()
{
ASSERT_VALID(this);
//取出消息队列中的第一个消息,直到取得消息,该函数才返回
if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))
{
//收到WM_QUIT消息
return FALSE;
}
//处理消息,但不处理WM_KICKIDLE
if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
{
//转换虚键消息到字符消息
::TranslateMessage(&m_msgCur);
//分发消息
::DispatchMessage(&m_msgCur);
}
return TRUE;
}
阅读PumpMessage代码可知,消息泵并不处理WM_KICKIDLE消息,收到该消息后,直接返回。其实,WM_KICKIDLE消息被用来刺激空闲处理的执行,它作为一个空消息促使::GetMessage()返回。
虽然Run()是虚拟函数,但很少被重载。

CWinThread类是MFC用来封装线程的,包括UI线程和工作者线程。因此每个MFC程序至少使用一个CWinThread派生类。
可见Run函数和PumpMessage函数实现了Win32程序中的WinProc函数的功能。

至此MFC的机制就基本讲完了,如果觉得没说清楚的可以去看《深入浅出MFC》前几章就好,那本书讲得很细。本文中的所有代码,都可以在百度上搜到,源码只需要看有注释的关键几行。不得不说Windows将这个机制隐藏的很深,对于初学者学习,真的不是很友好。它这么做必然有它的道理,我想这与它的系统有个很深的关系,MFC也不是一下子就搞出来的,微软也走过错路,有兴趣的读者可以取百度一下这个历史。

参考链接:
(http://www.cnblogs.com/lidabo/archive/2012/07/04/2576832.html)
http://www.cnblogs.com/chinazhangjie/archive/2011/09/19/2181954.html
http://blog.csdn.net/weiwenhp/article/details/8455471
http://www.cnblogs.com/liuweilinlin/archive/2012/08/16/2643272.html
http://blog.csdn.net/misskissc/article/details/8555105