MFC内部机制探秘
2016-04-29 09:55
901 查看
由于MFC应用程序涵盖了基于SDK的windows程序几乎所有的功能,所以使用MFC AppWizard创建的MFC程序将自动具有WIndows程序的基本功能,我们今天就来探寻一下MFC的框架机制。
首先大家先利用向导制动建立一个基于MFC的单文档应用程序。我取名为MFC_DISCOVER,方便大家自己验证自己的程序。
1.声明全局对象
利用应用程序对象theApp启动应用程序,theApp一般在应用程序类的实现文件中声明,而且为全局对象。在类视图点击CMFC_DISCOVERApp,再点击下面的构造成员函数,然后在函数对应编辑框中会找到这样一句话:
声明该类的变量显然会调用该类的构造函数,构造函数就在声明该变量的上面,但是我们知道CMFC_DISCOVER类派生于CWinApp类,所以会先调用基类的构造函数,
点击上图中的CWinApp类右键,点击“转到定义”,可以查看到CWinApp类的构造函数
// Constructor explicit CWinApp(LPCTSTR lpszAppName = NULL);
然而我们发现该类依然有派生类CWinThread,继续按照上面做发现有这样的关系:
CObeject->CCmdTarget->CWinTread
我就不一一截图了,我们知道是在这个过程中进入WinMain函数的,那么WinMain在哪儿呢?
我们在项目管理搜索栏目里面输入WinMain,搜索如下:
在tchar.h文件中发现有这样一个定义,也就是说_tWinMain就是WinMain,现在我们需要找到_tWinMain函数。不要设断点,直接按F10键,我们就看到了_tWinMain函数了:
这个函数在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
相关文章推荐
- 电脑突然死机导致远程git项目异常问题解决
- Java 多态
- UIView的alpha、hidden和opaque属性之间的关系和区别
- 【框架】Loader监听数据源的变化
- 上下箭头选中 选项事件 JS
- 修复npm安装全局模块权限问题
- (转)OKhttp封装
- ubuntu自动挂载ISCSI硬盘
- 返回指定日期所在周、月
- 这些情况,去面试就是浪费时间!
- 在coding中,遇到了它
- IOS中Json解析的四种方法
- 如何搞定前端资源服务跨域问题之nginx篇
- BZOJ 3648|寝室管理|点分治|树状数组|平衡树
- 全国行政区划代码具体到县
- 接口回调
- linux之crond服务
- memcache的使用http://blog.csdn.net/scelong/article/details/7245343
- Android仿斗鱼滑动登录验证
- nyoj_1 A+B Problem