溫故而知新,學習MFC框架如何建立的過程

iiprogram發表於2007-10-10
很久沒有使用MFC了,以至於都忘記MFC框架複雜的視窗、文件、視的建立過程了。
下面我們跟蹤一個MFC MDI的應用程式,來溫習或學習一下。
 
使用AppWizard建立一個MDI應用程式,我建立的應用程式叫MDITest,這樣MFC生成了如下的類:

類名
作用
CMDITestApp
派生於CWinApp的應用程式類。
CMainFrame
派生於CMDIFrameWnd的MDI框架視窗類。
CMDITestDoc
派生於CDocument的文件類。
CChildFrame
派生於CMDIChildWnd的MDI子視窗類。
CMDITestView
派生於CView的文件顯示類。
在執行時刻,CMainFrame, CChildFrame, CMDITestView的視窗關係如下面的表格示出:

CMainFrame
(Menu, Toolbar
MDIClient
 
CChildFrame
CMDITestView
   pDocument = *CMDITestDoc   (帶有文件的指標)
 
 
 
 
 
[StatusBar]
其中,最外層的是頂層視窗CMainFrame,裡面包含一個MDIClient視窗。CChildFrame做為子視窗包含於MDIClient中(可以包含多個),CChildFrame裡面則是真實的文件表示視窗CMDITestView了。
 
我們從這裡開始:

// CMDITestApp 初始化
BOOL CMDITestApp::InitInstance()
 
做為CWinApp的派生類,通常需要過載InitInstance(), ExitInstance()兩個函式,以完成應用的初始化和退出。我們現在關心InitInstance中關於文件模板、視窗處理的部分,而忽略掉一些CommonControl, OLE初始化部分。
 
整個InitInstance程式碼如下:

BOOL CMDITestApp::InitInstance()
{
     InitCommonControls();      // 這裡刪減了大量註釋和錯誤處理
     CWinApp::InitInstance();
     AfxOleInit();
     AfxEnableControlContainer();
     SetRegistryKey(_T("應用程式嚮導生成的本地應用程式"));
     LoadStdProfileSettings(4); // 載入標準 INI 檔案選項(包括 MRU)
 
     TRACE("Before CMultiDocTemplate/n");
     // 註冊應用程式的文件模板。文件模板
     // 將用作文件、框架視窗和檢視之間的連線
     CMultiDocTemplate* pDocTemplate;
     pDocTemplate = new CMultiDocTemplate(IDR_MDITestTYPE,
         RUNTIME_CLASS(CMDITestDoc),
         RUNTIME_CLASS(CChildFrame), // 自定義 MDI 子框架
         RUNTIME_CLASS(CMDITestView));
     if (!pDocTemplate)
         return FALSE;
     TRACE("Before AddDocTemplate/n");
     AddDocTemplate(pDocTemplate);
 
     // 建立主 MDI 框架視窗
     TRACE("Before new CMainFrame/n");
     CMainFrame* pMainFrame = new CMainFrame;
     TRACE("Before pMainFrame->LoadFrame/n");
     if (!pMainFrame || !pMainFrame->LoadFrame(IDR_MAINFRAME))
         return FALSE;
     m_pMainWnd = pMainFrame;
 
     TRACE("Before ParseCommandLine/n");
     CCommandLineInfo cmdInfo;
     ParseCommandLine(cmdInfo);
 
     // 排程在命令列中指定的命令。如果
     // 用 /RegServer、/Register、/Unregserver 或 /Unregister 啟動應用程式,則返回 FALSE。
     TRACE("Before ProcessShellCommand/n");
     if (!ProcessShellCommand(cmdInfo))
         return FALSE;
 
     TRACE("Before pMainFrame->ShowWindow/n");
     // 主視窗已初始化,因此顯示它並對其進行更新
     pMainFrame->ShowWindow(m_nCmdShow);
     TRACE("Before pMainFrame->UpdateWindow/n");
     pMainFrame->UpdateWindow();
     return TRUE;
}
 
為了研究整個建立過程,我在其中新增了一些TRACE來跟蹤建立順序。
 
忽略掉開始的亂七八糟的初始化,從CMultiDocTemplate開始:

     CMultiDocTemplate* pDocTemplate = new CMultiDocTemplate(IDR_MDITestTYPE,
         RUNTIME_CLASS(CMDITestDoc),
         RUNTIME_CLASS(CChildFrame), // 自定義 MDI 子框架
         RUNTIME_CLASS(CMDITestView));
     AddDocTemplate(pDocTemplate);
(作了一點點簡化)
這裡首先建立了一個CMultiDocTemplate —— 文件模板,文件模板包括的三個執行時刻類資訊:Document – CMDITestDoc, FrameWnd – CChildFrame, View – CMDITestView。
然後通過AddDocTemplate函式將新建立的文件模板新增到模板管理器之中(我們以後再研究模板管理器)。
 
然後建立主框架視窗CMainFrame:

     CMainFrame* pMainFrame = new CMainFrame;
     if (!pMainFrame || !pMainFrame->LoadFrame(IDR_MAINFRAME))
         return FALSE;
 
其中,需要研究的是LoadFrame的實現,以及裡面都做了些什麼。我們稍後研究。
 
處理命令列,在這裡第一個空文件被建立出來:

     CCommandLineInfo cmdInfo;
     ParseCommandLine(cmdInfo);
 
     // 排程在命令列中指定的命令。如果用 /RegServer、/Register、/Unregserver 或 /Unregister 啟動應用程式,則返回 FALSE。
     if (!ProcessShellCommand(cmdInfo))               // ß 這裡建立出初始空文件
         return FALSE;
 
我們一會會重點研究ProcessShellCommand。
 
最後,顯示主視窗:

     pMainFrame->ShowWindow(m_nCmdShow);
     pMainFrame->UpdateWindow();
 
至此,WinApp::InitInstance()完成了自己的工作。
 
上面遺留了三個待研究的分支,讓我們現在去研究它們:
1、 CDocTemplate
2、 CFrameWnd::LoadFrame
3、 CWnd::ProcessShellCommand
 
 
 
研究CDocTemplate
 
我們的例子中是構造了一個CMultiDocTemplate,它是從CDocTemplate派生而來,所以我們主要研究CDocTemplate。
CDocTemplate的幾個關鍵屬性列表如下:

     CRuntimeClass* m_pDocClass;         // class for creating new documents
     CRuntimeClass* m_pFrameClass;       // class for creating new frames
     CRuntimeClass* m_pViewClass;        // class for creating new views
 
其中:

m_pDocClass
表示文件類型別,在此例子中就是CMDITestDoc
m_pFrameClass
表示容納View視窗的框架視窗類型別,此例中為CChildFrame
m_pViewClass
表示顯示文件的View視類型別,此例中為CMDITestView
 
我們可以這樣認為,CDocTemplate用於描述Frame-View-Doc的關係。當然它還有一大堆別的屬性,我們暫時先忽略。
 
一會還會看到CDocTemplate的建立文件、框架、視的過程,放在ProcessShellCommand中研究。
 
 
研究LoadFrame
 
讓我們繼續研究CFrameWnd::LoadFrame是怎麼運作的。使用的方法是跟蹤進入。。。

BOOL CMDIFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
     CWnd* pParentWnd, CCreateContext* pContext)
{
     // 呼叫基類 CFrameWnd 的 LoadFrame, pContext 在建立主視窗時 = NULL
     //   pParentWnd = NULL
     if (!CFrameWnd::LoadFrame(nIDResource, dwDefaultStyle,
      pParentWnd, pContext))
         return FALSE;
 
     // save menu to use when no active MDI child window is present
     ASSERT(m_hWnd != NULL);
     // 主視窗帶有選單,所以。。。
     m_hMenuDefault = ::GetMenu(m_hWnd);
     if (m_hMenuDefault == NULL)
         TRACE(traceAppMsg, 0, "Warning: CMDIFrameWnd without a default menu./n");
     return TRUE;
}
注意,我們的MDITest Application的主視窗CMainFrame是從CMDIFrameWnd派生的,所以進入到這裡,參考程式碼中紅色的註釋部分。繼續跟蹤進入CFrameWnd::LoadFrame。
 

BOOL CFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
     CWnd* pParentWnd, CCreateContext* pContext)
{
     // only do this once
     ASSERT_VALID_IDR(nIDResource);    // nIDResource = 128, IDR_MAINFRAME
     ASSERT(m_nIDHelp == 0 || m_nIDHelp == nIDResource);
 
     m_nIDHelp = nIDResource;    // ID for help context (+HID_BASE_RESOURCE)
 
     CString strFullString;
     if (strFullString.LoadString(nIDResource)) // = MDITest
         AfxExtractSubString(m_strTitle, strFullString, 0);    // 取得第一個子串
 
     VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
 
     // attempt to create the window
    // GetIconWndClass 會呼叫 virtual PreCreateWindow 函式,別處也會呼叫,從而
    // 使得子類的PreCreateWindow 將被呼叫多次
     LPCTSTR lpszClass = GetIconWndClass(dwDefaultStyle, nIDResource);
     CString strTitle = m_strTitle;
     // 呼叫 CFrameWnd::Create() 實際建立出視窗。
     // 注意:在這裡將給 CMainFrame 傳送 WM_CREATE 等多個訊息。觸發 CMainFrame 的
     //   OnCreate 處理等。
     if (!Create(lpszClass, strTitle, dwDefaultStyle, rectDefault,
      pParentWnd, MAKEINTRESOURCE(nIDResource), 0L, pContext))
     {
         return FALSE;   // will self destruct on failure normally
     }
 
     // save the default menu handle, 好像 CMDIFrameWnd 也儲存了一次?
     ASSERT(m_hWnd != NULL);
     m_hMenuDefault = ::GetMenu(m_hWnd);
 
     // load accelerator resource
     LoadAccelTable(MAKEINTRESOURCE(nIDResource));
 
     // WM_INITIALUPDATE 是 MFC 發明的訊息,參見後面的說明。
     if (pContext == NULL)   // send initial update
         SendMessageToDescendants(WM_INITIALUPDATE, 0, 0, TRUE, TRUE);
 
     return TRUE;
}
 
以下是從TN024: MFC-Defined Messages And Resources中抽取的部分說明:

WM_INITIALUPDATE
This message is sent by the document template to all descendants of a frame window when it is safe for them to do their initial update. It maps to a call to CView::OnInitialUpdate but can be used in other CWnd-derived classes for other one-shot updating.
wParam
Not used (0)
lParam
Not used (0)
returns
Not used (0)
 
歸納一下,LoadFrame中進行了如下事情:
1、 註冊視窗類(AfxDeferRegisterClass)
2、 實際建立視窗(Create)
3、 處理選單、快捷鍵,傳送WM_INITIALUPDATE訊息給所有子視窗。實際將在CView中處理此訊息。(例如:在ToolBar上面放一個FormView,可能就能收到這個訊息並處利?)
 
至此,CMainFrame已經成功建立,選單已經裝載,工具條、狀態行等已經在CMainFrame::OnCreate中建立。讓我們接著研究第一個子視窗是怎麼被建立出來的,該過程和CMainFrame::LoadFrame比起來就不那麼直接了。
 
 
研究CWnd::ProcessShellCommand
 
第一個MDI子視窗是從這裡面建立出來的,這實在是缺乏直觀性。不過MFC就是這樣,沒辦法。

BOOL CWinApp::ProcessShellCommand(CCommandLineInfo& rCmdInfo)
{
     BOOL bResult = TRUE;
     switch (rCmdInfo.m_nShellCommand)
     {
     case CCommandLineInfo::FileNew:
         if (!AfxGetApp()->OnCmdMsg(ID_FILE_NEW, 0, NULL, NULL))      // 關鍵是這裡
              OnFileNew();
         if (m_pMainWnd == NULL)
              bResult = FALSE;
         break;
 
     case CCommandLineInfo::FileOpen:                // 忽略
     case CCommandLineInfo::FilePrintTo:           // 忽略
     case CCommandLineInfo::FilePrint:
     case CCommandLineInfo::FileDDE:
     case CCommandLineInfo::AppRegister:
     case CCommandLineInfo::AppUnregister:
     }
     return bResult;
}
進入到ProcessShellCommand,要處理很多種不同命令,我們忽略其它命令,單獨看FileNew部分。
注意:實際進入到了AfxGetApp()->OnCmdMsg(ID_FILE_NEW, 0, NULL, NULL)之中。
 
AfxGetApp()實際返回了CMDITestApp的唯一例項,它從CWinApp – CWinThread – CCmdTarget – CObject 派生而來。我們沒有過載OnCmdMsg,所以進入到CCmdTarget的OnCmdMsg處理中。為了研究,我們刪減了一些程式碼。

BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra,
     AFX_CMDHANDLERINFO* pHandlerInfo)
{
     // 這裡刪減了一些程式碼
     // determine the message number and code (packed into nCode)
     const AFX_MSGMAP* pMessageMap;
     const AFX_MSGMAP_ENTRY* lpEntry;
     UINT nMsg = 0;
     // 這裡刪減了一些程式碼,處理後 nMsg = WM_COMMAND
     // 為了簡化,刪減了一些斷言等。以下迴圈用於查詢處理此訊息的入口。
     for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL;
      pMessageMap = (*pMessageMap->pfnGetBaseMap)())
     {
         lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
         if (lpEntry != NULL)
         {
              // 找到了訊息處理項入口,分發此訊息。
              return _AfxDispatchCmdMsg(this, nID, nCode,
                   lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
         }
     }
     return FALSE;   // 未找到則不處理
}
最終MFC很愉快地找到了一個入口項,       CWinApp::OnFileNew(void)       要處理這個訊息。繼續進入到_AfxDispatchCmdMsg中去看看。
 

AFX_STATIC BOOL AFXAPI _AfxDispatchCmdMsg(CCmdTarget* pTarget, UINT nID, int nCode,
     AFX_PMSG pfn, void* pExtra, UINT_PTR nSig, AFX_CMDHANDLERINFO* pHandlerInfo)
         // return TRUE to stop routing
{
     union MessageMapFunctions mmf;
     mmf.pfn = pfn;
     BOOL bResult = TRUE; // default is ok
 
     if (pHandlerInfo != NULL)
     {
         // just fill in the information, don't do it
         pHandlerInfo->pTarget = pTarget;
         pHandlerInfo->pmf = mmf.pfn;
         return TRUE;
     }
 
     switch (nSig)
     {
     case AfxSigCmd_v:
         // normal command or control notification
         ASSERT(CN_COMMAND == 0);        // CN_COMMAND same as BN_CLICKED
         ASSERT(pExtra == NULL);
         (pTarget->*mmf.pfnCmd_v_v)();         // ß 實際呼叫 pTarget 指向的這個成員函式
         break;
     // 下面還有大量的多種 AfxSigCmd_xxx,忽略掉它們。
     default:    // illegal
         ASSERT(FALSE); return 0; break;
     }
     return bResult;
}
 
其中 (pTarget->*mmf.pfn_Cmd_v_v)() 對CWinApp::OnFileNew() 產生呼叫,pTarget = CMDITestApp類例項。呼叫進入如下:
 

void CWinApp::OnFileNew()
{
     if (m_pDocManager != NULL)
         m_pDocManager->OnFileNew();
}
 
進入進入到CDocManager::OnFileNew()
 

void CDocManager::OnFileNew()
{
     if (m_templateList.IsEmpty())
          // 提示沒有模板並返回
     CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead();    // 第一個
     if (m_templateList.GetCount() > 1)
          // 彈出一個對話方塊(很難看的)提示使用者選擇一個文件模板
 
     // 在這個例子裡面,pTemplate 就是 CMDITestApp::InitInstance() 裡面建立的那個模板
     pTemplate->OpenDocumentFile(NULL);
}
 
在進入CMultiDocTemplate::OpenDocumentFile之前,我觀察了一下呼叫堆疊,結果如下:

>   mfc71d.dll!CDocManager::OnFileNew() 852 C++
    mfc71d.dll!CWinApp::OnFileNew() 25   C++
    mfc71d.dll!_AfxDispatchCmdMsg(CCmdTarget * pTarget=0x0042cae8, unsigned int nID=57600, int nCode=0, void (void)* pfn=0x0041153c, void * pExtra=0x00000000, unsigned int nSig=53, AFX_CMDHANDLERINFO * pHandlerInfo=0x00000000) 89   C++
    mfc71d.dll!CCmdTarget::OnCmdMsg(unsigned int nID=57600, int nCode=0, void * pExtra=0x00000000, AFX_CMDHANDLERINFO * pHandlerInfo=0x00000000) 396 + 0x27    C++
    mfc71d.dll!CWinApp::ProcessShellCommand(CCommandLineInfo & rCmdInfo={...}) 27 + 0x1e C++
    MDITest.exe!CMDITestApp::InitInstance() 101 + 0xc    C++
希望我還沒有迷路:)
 
 
CMultiDocTemplate::OpenDocumentFile 又是很多很多程式碼,讓我們選擇一些。

CDocument* CMultiDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,
     BOOL bMakeVisible)
{
     // 以下程式碼刪減了驗證、斷言部分
     CDocument* pDocument = CreateNewDocument();              // 建立文件物件
     CFrameWnd* pFrame = CreateNewFrame(pDocument, NULL);    // 建立框架視窗
 
     if (lpszPathName == NULL)
     {
         pDocument->OnNewDocument();           // 初始化文件
     }
     else
          // 開啟已有文件
 
     InitialUpdateFrame(pFrame, pDocument, bMakeVisible);
     return pDocument;
}
 
 
看一看CreateNewDocument()

CDocument* CDocTemplate::CreateNewDocument()
{
     // default implementation constructs one from CRuntimeClass
     if (m_pDocClass == NULL)
          // 錯誤提示啦
     // CRuntimeClass* m_pDocClass -> CreateObject 例項化文件類。
     // 在此例子中既是 CMDITestDoc
     CDocument* pDocument = (CDocument*)m_pDocClass->CreateObject();
     AddDocument(pDocument);      // 新增到模板裡的文件列表,MultiDocTemplate 儲存此一文件
     return pDocument;
}
 
 
CMDITestDoc有如下的定義,僅能從CRuntimeClass裡面建立的。

class CMDITestDoc : public CDocument
{
protected: // 僅從序列化建立
     CMDITestDoc();               // 被保護的建構函式
     DECLARE_DYNCREATE(CMDITestDoc)             // 支援從 CRuntimeClass 資訊中建立。
 
 
再接著進行CreateNewFrame。

CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pDoc, CFrameWnd* pOther)
{
     // create a frame wired to the specified document
     CCreateContext context;           // 這個 CreateContext 傳遞到 LoadFrame 中
     context.m_pCurrentFrame = pOther;         // 此例中 = NULL
     context.m_pCurrentDoc = pDoc;              // = 剛才建立的文件
     context.m_pNewViewClass = m_pViewClass;   // 顯示此文件的視類的型別
     context.m_pNewDocTemplate = this;
 
     if (m_pFrameClass == NULL)
          // 提示錯誤並返回
     // 利用 CRuntimeClass 資訊建立框架視窗物件,此例中為 CChildFrame
     CFrameWnd* pFrame = (CFrameWnd*)m_pFrameClass->CreateObject();
 
     // 這裡,我們又看到了 LoadFrame , 參考前面的 LoadFrame 吧
     // 在這裡面,View 視窗也被產生出來。參考 TRACE 輸出。
     pFrame->LoadFrame(m_nIDResource,
              WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE,   // default frame styles
              NULL, &context);
     return pFrame;
}
 
 
LoadFrame之後View視窗將被建立出來,接著進入到CMDITestDoc::OnNewDocument中,現在僅僅是一個空的函式,沒有特定程式碼。

BOOL CMDITestDoc::OnNewDocument()
{
   TRACE("CMDITestDoc::OnNewDocument() entry/n");
     if (!CDocument::OnNewDocument())
         return FALSE;
 
     // TODO: 在此新增重新初始化程式碼
     // (SDI 文件將重用該文件)
 
     return TRUE;
}
 
最後是CDocTemplate::InitialUpdateFrame,這裡面主要是啟用新建的框架、文件、視,看得挺頭疼的。

void CDocTemplate::InitialUpdateFrame(CFrameWnd* pFrame, CDocument* pDoc,
     BOOL bMakeVisible)
{
     // just delagate to implementation in CFrameWnd
     pFrame->InitialUpdateFrame(pDoc, bMakeVisible);
}
 
現在,文件、框架視窗、視視窗全部被建立出來,我們勝利的返回到ProcessShellCommand處。顯示和更新主視窗,完成了WinApp::InitInstance :

     // 主視窗已初始化,因此顯示它並對其進行更新
     pMainFrame->ShowWindow(m_nCmdShow);
     pMainFrame->UpdateWindow();
 
 
 
看一下至此的TRACE輸出,中間的DLL載入被去掉了:

Before CMultiDocTemplate
Before AddDocTemplate
Before new CMainFrame
CMainFrame::CMainFrame()
Before pMainFrame->LoadFrame
CMainFrame::PreCreateWindow entry         // 注意:PreCreateWindow 被兩次呼叫
CMainFrame::PreCreateWindow entry
CMainFrame::OnCreate entry before CMDIFrameWnd::OnCreate
CMainFrame::OnCreate before m_wndToolBar.CreateEx
CMainFrame::OnCreate before m_wndStatusBar.Create
Before ParseCommandLine
Before ProcessShellCommand
CMDITestDoc::CMDITestDoc()      // 文件物件被建立
CChildFrame::CChildFrame()      // 子框架視窗被建立
CChildFrame::PreCreateWindow entry
CChildFrame::PreCreateWindow entry
CChildFrame::PreCreateWindow entry
CMDITestView::CMDITestView() entry   // 子框架視窗的 OnCreate 中建立了 View 視窗
CMDITestView::PreCreateWindow entry
CMDITestDoc::OnNewDocument() entry
Before pMainFrame->ShowWindow
Before pMainFrame->UpdateWindow
 
// 退出時的 TRACE
CMDITestView::~CMDITestView()
CChildFrame::~CChildFrame()
CMDITestDoc::~CMDITestDoc()
CMainFrame::~CMainFrame()
 
 

相關文章