一、引言
上一專題中,純手動地完成了一個Windows應用程序,然而,在實際開發中,我們大多數都是使用已有的類庫來開發Windows應用程序。MFC(Microsoft Foundation Class, 微軟基礎類庫)是微軟為了簡化程序員的開發工作而將Windows API 封裝到C++類中,利用這些類,程序員可以有效地完成Windows平台下應用程序的開發。本專題將詳細剖析它。
二、利用向導創建一個MFC程序
用於幫助有效地開發Windows應用程序的類庫除了MFC外,還有其他開源類庫提供,比如說QT,只是QT不是微軟開發的罷了,為了更好地剖析MFC,下面讓我們用Visual Studio中的MFC模板和向導工具來創建一個基於MFC的單文檔(SDI)應用程序。
- 啟動Visual studio 2010,單擊文件(FIle)菜單——>新建項目——>項目,在出現的項目窗口中選擇Visual C++ 語言,然后選擇MFC應用程序,並輸入項目的名稱為SDIMFC,具體如下圖所示。

2. 輸入項目名稱后點擊確定按鈕,將出現MFC應用程序向導窗口,點擊下一步,應用程序類型選擇:單個文檔,如下圖所示:

3. 點擊下一步,出現MFC向導的第三個對話框,復合文檔支持保持默認選擇,然后在出現的對話框中一直點擊下一步來完成一個單文檔MFC應用程序的創建。下面,按下Ctrl+F5來運行MFC應用程序,之后將看到我們創建的MFC應用程序界面,具體如下圖所示:

在上面的程序中,我們並沒有編寫任何代碼,運行它后就生成了一個帶標題欄,系統菜單,具有最大化、最小化框和一個可調邊框的應用程序,這一切的工作都是由MFC的向導工具幫我們完成,即該向導工具為我們生成了很多代碼,下面就以這個簡單的MFC程序來分析下MFC框架。
三、MFC框架詳細解析
我們看下用MFC向導工具幫我們生成的哪些代碼。你可以在VS中點擊類視圖選項卡(如果VS界面上沒有看到類視圖的,可以通過菜單欄視圖—>類視圖的方式顯示出來),就可以看到如下圖所示的類。

從上圖可以發現,在MFC中,類的命名都是以字母“C”開頭的,這種命名方式只是一種約定,讓開發人員很快識別出該類是否屬於MFC類庫中的類。從圖片可以看到,前面創建的單文檔應用程序中有15個類,但這里我們只分析4個基本類,因為這4個基本類是每個Windows應用程序都會包含的,這4個類是:CMainFrame類、C+工程名(SDIMFC)+App類、C+工程名+Doc類(即CSDIMFCDoc類)和CSDIMFCView類(也是C+工程名+View的結構)。這4個類的基類都是MFC中類,基類的查看可以通過在VS類視圖點擊
圖標。關於MFC中類圖層次結構圖可以參考MSDN:http://msdn.microsoft.com/zh-cn/library/ws8s10w4.aspx,下圖(摘自MSDN)很好地詮釋了MFC中層次結構圖類別。

3.1 MFC應用程序中的WinMain函數
前面對我們創建的MFC應用程序結構進行了一個簡單的介紹,下面讓我們深入剖析MFC應用程序的實現原理,在前一專題講到,所有Window下窗口應用程序都要遵循這樣一個過程:程序首先進入WinMain函數,然后設計窗口類、注冊窗口類、創建窗口、顯示和更新窗口、最后進入消息循環,將消息傳遞給窗口過程函數進行處理。然后在MFC應用程序中,我們使用VS的查找工具在MFC項目中查看WinMain函數卻找不到,再查看CreateWindow函數也找不到,那么是不是MFC應用程序不需要WinMain函數,不需要創建窗口嗎?這個疑問答案肯定是否定的,因為MFC應用程序一樣是Windows應用程序,所以一定遵循上一專題介紹的過程,只是MFC提供的類幫我們對這些類進行了封裝,這些函數都存在於MFC的源代碼中,下面我們一起去找找程序的入口WinMain函數。
既然WinMain函數存在與MFC源碼中,自然我們就要知道MFC源碼在哪里了,在安裝Visual studio的時候,我們已經安裝了MFC的源代碼,具體路徑為:VS的安裝路徑\VC\atlmfc\src\mfc,如果你本機把VS安裝到D:\Program Files(x86)的話,則MFC源代碼路徑在:D:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\atlmfc\src\mfc。 下面利用Windows搜索工具查看WinMain函數的存在那個C++類中,在搜索之前,需要設置下Windows搜索工具,默認情況下,Windows搜索工具搜索內容在沒有索引的位置,只搜索文件名的,這里需要設置為搜索文件名和內容,具體設置如下圖所示(Win7下選擇工具—>文件夾選項即可顯示下圖):

設置完成之后,在搜索框中輸入WinMain,你將看到如下圖所示的一個搜索結果:
WinMain函數的實現實際在appmodul.cpp文件里,用VS打開該cpp文件,你將看到WinMain函數的定義:
extern "C" int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, int nCmdShow) #pragma warning(suppress: 4985) { // call shared/exported WinMain return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow); }
上面代碼中是_tWinMain函數啊,並不是我要的WinMain函數的,難道是找錯了嗎?對於這個疑問,答案也是否定的,我們沒有找錯,這里_tWinMain是一個宏定義,按F12即可以看到它代表的是WinMain。宏定義源碼如下(存在於tchar.h頭文件中):
/* Program */ #define _tmain main // 宏定義 #define _tWinMain WinMain
為了證明我們找到的WinMain正是我們需要找到的入口函數,我們可以在appmodul.cpp文件中_tWinMain函數中設置一個斷點,然后按下F5按鈕運行SDIMFC程序,我們發現,SDIMFC程序會在我們剛才設置的斷點處停下來,具體如下圖所示:

我們已經找到了WinMain函數在MFC中的實現了,但是並沒有弄明白,我們創建的MFC程序是如何調用appmodul.cpp中的_tWinMain函數的,即程序中的MFC類如何與WinMain函數聯系起來的呢?下面就讓我們看看CSDIMFCApp類(至於為什么想到該類,因為其后綴為App,即應用程序,所以猜測程序在進入WinMain函數之前會先進入該類),在類視圖中雙擊該類將在VS中看到該類的定義,從類定義可以知道,CSDIMFCApp類繼承於CWinAppEx類,CWinAppEx類又繼承於CWinApp,為了證明在WinMain函數之前先執行了CSDIMFCApp類中代碼,我們在CSDIMFCApp類中的構造函數設置一個斷點,然后按F5再運行下該程序,將發現程序首先停在CSDIMFCApp類的構造函數處,然后進入到_tWinMain函數(該斷點是我們之前設置的斷點)。這里又引起另外一個疑問了——為什么程序會首先調用CSDIMFCApp的構造函數呢?既然構造函數被調用了,肯定定義了該類的一個對象,然后,我們可以發現在CSDIMFCApp類中,定義了一個CSDIMFCApp類型的全局對象theApp,存在於SDIMFC.cpp文件中,具體定義代碼如下:
// 唯一的一個 CSDIMFCApp 對象 CSDIMFCApp theApp; // 初始化對象,這種方式為調用類的無參數構造來初始化,所以會調用類的無參構造函數
然后我們在這個全局對象處設置一個斷點,然后再按F5調試運行下該程序,你將發現程序執行的順序為:theApp全局對象—>CSDIMFCApp構造函數(調用派生類的構造函數之前會調用其父類的構造函數)—>_tWinMain函數。在MFC程序中,theApp對象是用來唯一標識應用程序實例的,每個MFC程序有且僅有一個應用程序對象(這里為theApp對象)。
3.2 設計和注冊窗口類
現在我們已經找到MFC中的WinMain函數了,根據前一專題的內容,接下來就是找到MFC應用程序中的窗口類和注冊窗口類的代碼,在上一專題中,窗口類和注冊都是在WinMain函數中定義的,下面讓我們看下MFC中WinMain函數都幫我們封裝了什么,在MFC中的WinMain函數中只是簡單對AfxWinMain函數進行調用,下面讓我們看看AfxWinMain具體代碼:
1 // AfxWinMain函數 2 int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 3 _In_ LPTSTR lpCmdLine, int nCmdShow) 4 { 5 ASSERT(hPrevInstance == NULL); 6 7 int nReturnCode = -1; 8 CWinThread* pThread = AfxGetThread(); 9 CWinApp* pApp = AfxGetApp(); 10 11 // AFX internal initialization 12 if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow)) 13 goto InitFailure; 14 15 // App global initializations (rare) 16 if (pApp != NULL && !pApp->InitApplication()) 17 goto InitFailure; 18 19 // Perform specific initializations 20 if (!pThread->InitInstance()) 21 { 22 if (pThread->m_pMainWnd != NULL) 23 { 24 TRACE(traceAppMsg, 0, "Warning: Destroying non-NULL m_pMainWnd\n"); 25 pThread->m_pMainWnd->DestroyWindow(); 26 } 27 nReturnCode = pThread->ExitInstance(); 28 goto InitFailure; 29 }
// PThread->Run函數是完成消息循環任務的 30 nReturnCode = pThread->Run(); 31 32 InitFailure: 33 #ifdef _DEBUG 34 // Check for missing AfxLockTempMap calls 35 if (AfxGetModuleThreadState()->m_nTempMapLock != 0) 36 { 37 TRACE(traceAppMsg, 0, "Warning: Temp map lock count non-zero (%ld).\n", 38 AfxGetModuleThreadState()->m_nTempMapLock); 39 } 40 AfxLockTempMaps(); 41 AfxUnlockTempMaps(-1); 42 #endif 43 44 AfxWinTerm(); 45 return nReturnCode; 46 }
上面代碼首先調用AfxGetThread函數獲得一個指向CWinThread類型的指針,然后再調用了AfxGetApp函數獲得一個指向CWinApp類型的指針,再繼續調用AfxWinInit函數進行AFX(以AFX為前綴的函數為應用程序框架函數,Application Framework)內部初始化,接着pApp調用InitApplication函數,該函數主要完成MFC內部管理方面的工作,該函數為虛函數,在CWinApp中的實現為(函數實現代碼查找按照前面介紹的方式進行查看):
// 主要完成MFC內部管理工作 BOOL CWinApp::InitApplication() { if (CDocManager::pStaticDocManager != NULL) { if (m_pDocManager == NULL) m_pDocManager = CDocManager::pStaticDocManager; CDocManager::pStaticDocManager = NULL; } if (m_pDocManager != NULL) m_pDocManager->AddDocTemplate(NULL); else CDocManager::bStaticInit = FALSE; LoadSysPolicies(); return TRUE; }
接着繼續調用pThread的InitInstance函數,按F12可知,該函數聲明為虛函數,根據類的多態性,這里AfxWinMain函數中調用的InitInstance函數為調用子類CSDIMFCApp的InitInstance函數,該函數的定義代碼為:
1 / CSDIMFCApp 初始化 2 BOOL CSDIMFCApp::InitInstance() 3 { 4 // 如果一個運行在 Windows XP 上的應用程序清單指定要 5 // 使用 ComCtl32.dll 版本 6 或更高版本來啟用可視化方式, 6 //則需要 InitCommonControlsEx()。否則,將無法創建窗口。 7 INITCOMMONCONTROLSEX InitCtrls; 8 InitCtrls.dwSize = sizeof(InitCtrls); 9 // 將它設置為包括所有要在應用程序中使用的 10 // 公共控件類。 11 InitCtrls.dwICC = ICC_WIN95_CLASSES; 12 InitCommonControlsEx(&InitCtrls); 13 14 CWinAppEx::InitInstance(); 15 16 17 // 初始化 OLE 庫 18 if (!AfxOleInit()) 19 { 20 AfxMessageBox(IDP_OLE_INIT_FAILED); 21 return FALSE; 22 } 23 24 AfxEnableControlContainer(); 25 26 EnableTaskbarInteraction(FALSE); 27 28 // 使用 RichEdit 控件需要 AfxInitRichEdit2() 29 // AfxInitRichEdit2(); 30 31 // 標准初始化 32 // 如果未使用這些功能並希望減小 33 // 最終可執行文件的大小,則應移除下列 34 // 不需要的特定初始化例程 35 // 更改用於存儲設置的注冊表項 36 // TODO: 應適當修改該字符串, 37 // 例如修改為公司或組織名 38 SetRegistryKey(_T("應用程序向導生成的本地應用程序")); 39 LoadStdProfileSettings(4); // 加載標准 INI 文件選項(包括 MRU) 40 41 42 InitContextMenuManager(); 43 44 InitKeyboardManager(); 45 46 InitTooltipManager(); 47 CMFCToolTipInfo ttParams; 48 ttParams.m_bVislManagerTheme = TRUE; 49 theApp.GetTooltipManager()->SetTooltipParams(AFX_TOOLTIP_TYPE_ALL, 50 RUNTIME_CLASS(CMFCToolTipCtrl), &ttParams); 51 52 // 注冊應用程序的文檔模板。文檔模板 53 // 將用作文檔、框架窗口和視圖之間的連接 54 CSingleDocTemplate* pDocTemplate; 55 pDocTemplate = new CSingleDocTemplate( 56 IDR_MAINFRAME, 57 RUNTIME_CLASS(CSDIMFCDoc), 58 RUNTIME_CLASS(CMainFrame), // 主 SDI 框架窗口 59 RUNTIME_CLASS(CSDIMFCView)); 60 if (!pDocTemplate) 61 return FALSE; 62 AddDocTemplate(pDocTemplate); 63 64 65 // 分析標准 shell 命令、DDE、打開文件操作的命令行 66 CCommandLineInfo cmdInfo; 67 ParseCommandLine(cmdInfo); 68 69 70 71 // 調度在命令行中指定的命令。如果 72 // 用 /RegServer、/Register、/Unregserver 或 /Unregister 啟動應用程序,則返回 FALSE。 73 if (!ProcessShellCommand(cmdInfo)) 74 return FALSE; 75 76 // 唯一的一個窗口已初始化,因此顯示它並對其進行更新 77 m_pMainWnd->ShowWindow(SW_SHOW); 78 m_pMainWnd->UpdateWindow(); 79 // 僅當具有后綴時才調用 DragAcceptFiles 80 // 在 SDI 應用程序中,這應在 ProcessShellCommand 之后發生 81 return TRUE; 82 }
在上面代碼中,77行代碼和78行代碼為窗口的顯示和更新,m_pMainWnd為我們創建的窗口,按F12轉到其定義為指向CWnd類型的指針,Cwnd類是MFC為我們預定義的標准窗口類,現在我們已經找到MFC程序中的窗口類,接下來就是找到MFC中是如何注冊窗口的,在MFC中,窗口類的注冊是由AfxEndDeferRegisterClass函數完成的,其定義在Wincore.cpp文件中,AfxEndDeferRegisterClass函數內部又是通過AfxRegisterClass函數(該函數也定義在wincore.cpp文件中)來注冊窗口類,由於篇幅的問題,這里就不貼其函數的定義源碼,大家可以在本機中進行查看。
3.3 創建窗口
我們已經找到了MFC設計窗口和注冊的封裝,接下來就是MFC程序中是如何創建一個窗口的,該功能在MFC中是由CWnd類的CreateEx函數進行完成的,該函數的聲明在afxwin.h文件中,具體代碼如下:
virtual BOOL CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam = NULL);
實現代碼位於wincore.cpp文件中,我們程序中創建的是CMainFrame窗口,CMainFrame類繼承於CFrameWndEx,該類又繼承於CFrameWnd,CFrameWnd類的Create函數內部會調用CreateEx函數,而CFrameWnd的Create函數又由CFrameWnd類的LoadFrame函數調用。CFrameWnd類的Create函數聲明位於afxwin.h文件中,其實現代碼位於winfrm.cpp文件中,實現代碼如下:
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; } } 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; }
3.4 顯示窗口和更新窗口
在CSDIMFCApp類的InitInstance函數內容即有窗口顯示和更新窗口的代碼,具體代碼如下:
// 唯一的一個窗口已初始化,因此顯示它並對其進行更新 m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow();
3.5 消息循環
CWinThread類的Run函數就是完成消息循環這一任務的,該函數在AfxWinMain函數中進行了調用,其定義在thrdcore.cpp文件中,其定義代碼如下所示:
// main running routine until thread exits int CWinThread::Run() { ASSERT_VALID(this); _AFX_THREAD_STATE* pState = AfxGetThreadState(); // for tracking the idle time state BOOL bIdle = TRUE; LONG lIdleCount = 0; // acquire and dispatch messages until a WM_QUIT message is received. for (;;) { // phase1: check to see if we can do idle work while (bIdle && !::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)) { // call OnIdle while in bIdle state if (!OnIdle(lIdleCount++)) bIdle = FALSE; // assume "no idle" state } // phase2: pump messages while available do { // pump message, but quit on WM_QUIT if (!PumpMessage()) return ExitInstance(); // reset "no idle" state after pumping "normal" message //if (IsIdleMessage(&m_msgCur)) if (IsIdleMessage(&(pState->m_msgCur))) { bIdle = TRUE; lIdleCount = 0; } } while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE)); } }
3.6 窗口過程函數
在AfxEndDeferRegisterClass函數中其中有一行這樣的代碼(下面代碼紅色標記處):
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;
.....
}
但實際上,MFC中並不是把所有消息都交給DefWindowProc這一默認窗口過程進行處理的,而是采用了一種稱為消息映射機制來處理各種消息,MFC消息映射機制指的是可以通過類向導為類添加消息處理函數,具體操作為,在類視圖中右鍵某個類,然后選擇類向導,在彈出的MFC類向導窗體中切換到消息選項卡來添加某個消息的處理函數,下圖是CMainFrame執行類向導的截圖:

該過程類似.NET中WinForm中通過某個控件的事件來添加事件處理函數。(WinForm中事件對應於MFC中的消息)。
至此,我們已經分析完了MFC程序的運行機制了,可以發現其經歷過程和前一專題介紹是一致,只是MFC中我們不需要自己實現這些過程了,這些都由MFC框架幫我們封裝好了,從而減少開發人員的任務量,將更多的時間放在實現程序的業務邏輯上面。下面讓我們一起來梳理下MFC程序的運行過程:
- 首先利用全局應用程序對象theApp啟動應用程序。
- 調用全局應用程序對象的構造函數,從而會調用基類CWinApp的構造函數,后者完成一些應用程序的初始化工作。
- 進入到WinMain函數,即_tWinMain函數,在該函數中調用了AfxWinMain函數,后者獲取子類(程序中指的CSDIMFCApp類)的指針,利用該指針調用InitInstance虛函數,根據多態原理,實際調用的是子類CSDIMFCApp的InitInstance函數,子類CSDIMFCApp的InitInstance函數完成應用程序的一些初始化工作,包括窗口類的注冊、創建、窗口顯示和更新。
- 進入消息循環。雖然注冊函數中設置了默認的窗口過程函數,但是,MFC應用程序實際上采用消息映射機制來處理各種消息的,當收到WM_QUIT消息時,將退出消息循環,程序結束。
四、文檔/視圖結構
我們創建的MFC程序除了主框架窗口外,還有一個窗口是視類窗口,對應於CView類,框架窗口是視類窗口的一個父窗口,它們之間的關系如下圖所示。主框架窗口是整個應用程序外框所包括的部分,而視類窗口只是主框架窗口中的空白的地方。

在我們之前創建的MFC程序中還有一個CSDIMFCDoc類,它派生與CDocument類,后者的基類又是CCmdTarget,而CCmdTarget又派生於CObject類,從而,可以知道CSDIMFCDoc類不是一個窗口類,實際上它是一個文檔類。MFC提供了一個文檔/視圖結構(Document/View),這里文檔指的是CDocument類,而視圖指的是CView類。微軟在設計MFC時,考慮到數據本身應該與它的顯示分離(這點在微軟的很多技術中都有體現,例如Asp.net MVC ),於是就采用文檔和視圖結構來實現這一想法。數據的存儲和加載由文檔類來完成,數據的顯示和修改由視圖類來完成,從而把數據管理和顯示方法分離開來。
五、小結
到此,本專題的內容就介紹結束了,本專題主要剖析了MFC框架的運行機制,從而發現MFC應用程序同樣遵循Win32 SDK程序相應的過程,包括設計窗口類、注冊窗口類、創建窗口、顯示和更新窗口、消息循環和窗口處理過程函數,只不過這些操作都被MFC本身封裝好了。
