深入淺出話VC++(2)——MFC的本質


一、引言

上一專題中,純手動地完成了一個Windows應用程序,然而,在實際開發中,我們大多數都是使用已有的類庫來開發Windows應用程序。MFC(Microsoft Foundation Class, 微軟基礎類庫)是微軟為了簡化程序員的開發工作而將Windows API 封裝到C++類中,利用這些類,程序員可以有效地完成Windows平台下應用程序的開發。本專題將詳細剖析它。

二、利用向導創建一個MFC程序

用於幫助有效地開發Windows應用程序的類庫除了MFC外,還有其他開源類庫提供,比如說QT,只是QT不是微軟開發的罷了,為了更好地剖析MFC,下面讓我們用Visual Studio中的MFC模板和向導工具來創建一個基於MFC的單文檔(SDI)應用程序。

  1. 啟動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;
}
View Code

接着繼續調用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;
}
View Code

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));
    }
}
View Code

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本身封裝好了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM