下面我們跟蹤一個(gè)MFC MDI的應(yīng)用程序,來溫習(xí)或?qū)W習(xí)一下。
使用AppWizard創(chuàng)建一個(gè)MDI應(yīng)用程序,我創(chuàng)建的應(yīng)用程序叫MDITest,這樣MFC生成了如下的類:
類名
|
作用
|
CMDITestApp
|
派生于CWinApp的應(yīng)用程序類。
|
CMainFrame
|
派生于CMDIFrameWnd的MDI框架窗口類。
|
CMDITestDoc
|
派生于CDocument的文檔類。
|
CChildFrame
|
派生于CMDIChildWnd的MDI子窗口類。
|
CMDITestView
|
派生于CView的文檔顯示類。
|
在運(yùn)行時(shí)刻,CMainFrame, CChildFrame, CMDITestView的窗口關(guān)系如下面的表格示出:
CMainFrame
(Menu, Toolbar …)
MDIClient
CChildFrame
CMDITestView
pDocument = *CMDITestDoc (帶有文檔的指針)
|
|
|
[StatusBar]
|
其中,最外層的是頂層窗口CMainFrame,里面包含一個(gè)MDIClient窗口。CChildFrame做為子窗口包含于MDIClient中(可以包含多個(gè)),CChildFrame里面則是真實(shí)的文檔表示窗口CMDITestView了。
我們從這里開始:
// CMDITestApp 初始化
BOOL CMDITestApp::InitInstance()
|
做為CWinApp的派生類,通常需要重載InitInstance(), ExitInstance()兩個(gè)函數(shù),以完成應(yīng)用的初始化和退出。我們現(xiàn)在關(guān)心InitInstance中關(guān)于文檔模板、窗口處理的部分,而忽略掉一些CommonControl, OLE初始化部分。
整個(gè)InitInstance代碼如下:
BOOL CMDITestApp::InitInstance()
{
InitCommonControls(); // 這里刪減了大量注釋和錯(cuò)誤處理
CWinApp::InitInstance();
AfxOleInit();
AfxEnableControlContainer();
SetRegistryKey(_T("應(yīng)用程序向?qū)傻谋镜貞?yīng)用程序"));
LoadStdProfileSettings(4); // 加載標(biāo)準(zhǔn) INI 文件選項(xiàng)(包括 MRU)
TRACE("Before CMultiDocTemplate\n");
// 注冊應(yīng)用程序的文檔模板。文檔模板
// 將用作文檔、框架窗口和視圖之間的連接
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);
// 創(chuàng)建主 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);
// 調(diào)度在命令行中指定的命令。如果
// 用 /RegServer、/Register、/Unregserver 或 /Unregister 啟動應(yīng)用程序,則返回 FALSE。
TRACE("Before ProcessShellCommand\n");
if (!ProcessShellCommand(cmdInfo))
return FALSE;
TRACE("Before pMainFrame->ShowWindow\n");
// 主窗口已初始化,因此顯示它并對其進(jìn)行更新
pMainFrame->ShowWindow(m_nCmdShow);
TRACE("Before pMainFrame->UpdateWindow\n");
pMainFrame->UpdateWindow();
return TRUE;
}
|
為了研究整個(gè)創(chuàng)建過程,我在其中添加了一些TRACE來跟蹤創(chuàng)建順序。
忽略掉開始的亂七八糟的初始化,從CMultiDocTemplate開始:
CMultiDocTemplate* pDocTemplate = new CMultiDocTemplate(IDR_MDITestTYPE,
RUNTIME_CLASS(CMDITestDoc),
RUNTIME_CLASS(CChildFrame), // 自定義 MDI 子框架
RUNTIME_CLASS(CMDITestView));
AddDocTemplate(pDocTemplate);
(作了一點(diǎn)點(diǎn)簡化)
|
這里首先創(chuàng)建了一個(gè)CMultiDocTemplate —— 文檔模板,文檔模板包括的三個(gè)運(yùn)行時(shí)刻類信息:Document – CMDITestDoc, FrameWnd – CChildFrame, View – CMDITestView。
然后通過AddDocTemplate函數(shù)將新創(chuàng)建的文檔模板添加到模板管理器之中(我們以后再研究模板管理器)。
然后創(chuàng)建主框架窗口CMainFrame:
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame || !pMainFrame->LoadFrame(IDR_MAINFRAME))
return FALSE;
|
其中,需要研究的是LoadFrame的實(shí)現(xiàn),以及里面都做了些什么。我們稍后研究。
處理命令行,在這里第一個(gè)空文檔被建立出來:
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// 調(diào)度在命令行中指定的命令。如果用 /RegServer、/Register、/Unregserver 或 /Unregister 啟動應(yīng)用程序,則返回 FALSE。
if (!ProcessShellCommand(cmdInfo)) // ß 這里創(chuàng)建出初始空文檔
return FALSE;
|
我們一會會重點(diǎn)研究ProcessShellCommand。
最后,顯示主窗口:
pMainFrame->ShowWindow(m_nCmdShow);
pMainFrame->UpdateWindow();
|
至此,WinApp::InitInstance()完成了自己的工作。
上面遺留了三個(gè)待研究的分支,讓我們現(xiàn)在去研究它們:
1、 CDocTemplate
2、 CFrameWnd::LoadFrame
3、 CWnd::ProcessShellCommand
研究CDocTemplate
我們的例子中是構(gòu)造了一個(gè)CMultiDocTemplate,它是從CDocTemplate派生而來,所以我們主要研究CDocTemplate。
CDocTemplate的幾個(gè)關(guān)鍵屬性列表如下:
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
|
我們可以這樣認(rèn)為,CDocTemplate用于描述Frame-View-Doc的關(guān)系。當(dāng)然它還有一大堆別的屬性,我們暫時(shí)先忽略。
一會還會看到CDocTemplate的創(chuàng)建文檔、框架、視的過程,放在ProcessShellCommand中研究。
研究LoadFrame
讓我們繼續(xù)研究CFrameWnd::LoadFrame是怎么運(yùn)作的。使用的方法是跟蹤進(jìn)入。。。
BOOL CMDIFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
CWnd* pParentWnd, CCreateContext* pContext)
{
// 調(diào)用基類 CFrameWnd 的 LoadFrame, pContext 在創(chuàng)建主窗口時(shí) = 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派生的,所以進(jìn)入到這里,參考代碼中紅色的注釋部分。繼續(xù)跟蹤進(jìn)入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); // 取得第一個(gè)子串
VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
// attempt to create the window
// GetIconWndClass 會調(diào)用 virtual PreCreateWindow 函數(shù),別處也會調(diào)用,從而
// 使得子類的PreCreateWindow 將被調(diào)用多次
LPCTSTR lpszClass = GetIconWndClass(dwDefaultStyle, nIDResource);
CString strTitle = m_strTitle;
// 調(diào)用 CFrameWnd::Create() 實(shí)際創(chuàng)建出窗口。
// 注意:在這里將給 CMainFrame 發(fā)送 WM_CREATE 等多個(gè)消息。觸發(fā) 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 發(fā)明的消息,參見后面的說明。
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中進(jìn)行了如下事情:
1、 注冊窗口類(AfxDeferRegisterClass)
2、 實(shí)際創(chuàng)建窗口(Create)
3、 處理菜單、快捷鍵,發(fā)送WM_INITIALUPDATE消息給所有子窗口。實(shí)際將在CView中處理此消息。(例如:在ToolBar上面放一個(gè)FormView,可能就能收到這個(gè)消息并處利?)
至此,CMainFrame已經(jīng)成功創(chuàng)建,菜單已經(jīng)裝載,工具條、狀態(tài)行等已經(jīng)在CMainFrame::OnCreate中創(chuàng)建。讓我們接著研究第一個(gè)子窗口是怎么被創(chuàng)建出來的,該過程和CMainFrame::LoadFrame比起來就不那么直接了。
研究CWnd::ProcessShellCommand
第一個(gè)MDI子窗口是從這里面建立出來的,這實(shí)在是缺乏直觀性。不過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)) // 關(guān)鍵是這里
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;
}
|
進(jìn)入到ProcessShellCommand,要處理很多種不同命令,我們忽略其它命令,單獨(dú)看FileNew部分。
注意:實(shí)際進(jìn)入到了AfxGetApp()->OnCmdMsg(ID_FILE_NEW, 0, NULL, NULL)之中。
AfxGetApp()實(shí)際返回了CMDITestApp的唯一實(shí)例,它從CWinApp – CWinThread – CCmdTarget – CObject 派生而來。我們沒有重載OnCmdMsg,所以進(jìn)入到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
// 為了簡化,刪減了一些斷言等。以下循環(huán)用于查找處理此消息的入口。
for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL;
pMessageMap = (*pMessageMap->pfnGetBaseMap)())
{
lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
if (lpEntry != NULL)
{
// 找到了消息處理項(xiàng)入口,分發(fā)此消息。
return _AfxDispatchCmdMsg(this, nID, nCode,
lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
}
}
return FALSE; // 未找到則不處理
}
|
最終MFC很愉快地找到了一個(gè)入口項(xiàng), CWinApp::OnFileNew(void) 要處理這個(gè)消息。繼續(xù)進(jìn)入到_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)(); // ß 實(shí)際調(diào)用 pTarget 指向的這個(gè)成員函數(shù)
break;
// 下面還有大量的多種 AfxSigCmd_xxx,忽略掉它們。
default: // illegal
ASSERT(FALSE); return 0; break;
}
return bResult;
}
|
其中 (pTarget->*mmf.pfn_Cmd_v_v)() 對CWinApp::OnFileNew() 產(chǎn)生調(diào)用,pTarget = CMDITestApp類實(shí)例。調(diào)用進(jìn)入如下:
void CWinApp::OnFileNew()
{
if (m_pDocManager != NULL)
m_pDocManager->OnFileNew();
}
|
進(jìn)入進(jìn)入到CDocManager::OnFileNew()
void CDocManager::OnFileNew()
{
if (m_templateList.IsEmpty())
// 提示沒有模板并返回
CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead(); // 第一個(gè)
if (m_templateList.GetCount() > 1)
// 彈出一個(gè)對話框(很難看的)提示用戶選擇一個(gè)文檔模板
// 在這個(gè)例子里面,pTemplate 就是 CMDITestApp::InitInstance() 里面創(chuàng)建的那個(gè)模板
pTemplate->OpenDocumentFile(NULL);
}
|
在進(jìn)入CMultiDocTemplate::OpenDocumentFile之前,我觀察了一下調(diào)用堆棧,結(jié)果如下:
> 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)
{
// 以下代碼刪減了驗(yàn)證、斷言部分
CDocument* pDocument = CreateNewDocument(); // 創(chuàng)建文檔對象
CFrameWnd* pFrame = CreateNewFrame(pDocument, NULL); // 創(chuàng)建框架窗口
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)
// 錯(cuò)誤提示啦
// CRuntimeClass* m_pDocClass -> CreateObject 實(shí)例化文檔類。
// 在此例子中既是 CMDITestDoc
CDocument* pDocument = (CDocument*)m_pDocClass->CreateObject();
AddDocument(pDocument); // 添加到模板里的文檔列表,MultiDocTemplate 保存此一文檔
return pDocument;
}
|
CMDITestDoc有如下的定義,僅能從CRuntimeClass里面創(chuàng)建的。
class CMDITestDoc : public CDocument
{
protected: // 僅從序列化創(chuàng)建
CMDITestDoc(); // 被保護(hù)的構(gòu)造函數(shù)
DECLARE_DYNCREATE(CMDITestDoc) // 支持從 CRuntimeClass 信息中創(chuàng)建。
|
再接著進(jìn)行CreateNewFrame。
CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pDoc, CFrameWnd* pOther)
{
// create a frame wired to the specified document
CCreateContext context; // 這個(gè) CreateContext 傳遞到 LoadFrame 中
context.m_pCurrentFrame = pOther; // 此例中 = NULL
context.m_pCurrentDoc = pDoc; // = 剛才創(chuàng)建的文檔
context.m_pNewViewClass = m_pViewClass; // 顯示此文檔的視類的類型
context.m_pNewDocTemplate = this;
if (m_pFrameClass == NULL)
// 提示錯(cuò)誤并返回
// 利用 CRuntimeClass 信息創(chuàng)建框架窗口對象,此例中為 CChildFrame
CFrameWnd* pFrame = (CFrameWnd*)m_pFrameClass->CreateObject();
// 這里,我們又看到了 LoadFrame , 參考前面的 LoadFrame 吧
// 在這里面,View 窗口也被產(chǎn)生出來。參考 TRACE 輸出。
pFrame->LoadFrame(m_nIDResource,
WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, // default frame styles
NULL, &context);
return pFrame;
}
|
LoadFrame之后View窗口將被創(chuàng)建出來,接著進(jìn)入到CMDITestDoc::OnNewDocument中,現(xiàn)在僅僅是一個(gè)空的函數(shù),沒有特定代碼。
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);
}
|
現(xiàn)在,文檔、框架窗口、視窗口全部被創(chuàng)建出來,我們勝利的返回到ProcessShellCommand處。顯示和更新主窗口,完成了WinApp::InitInstance :
// 主窗口已初始化,因此顯示它并對其進(jìn)行更新
pMainFrame->ShowWindow(m_nCmdShow);
pMainFrame->UpdateWindow();
|
看一下至此的TRACE輸出,中間的DLL加載被去掉了:
Before CMultiDocTemplate
Before AddDocTemplate
Before new CMainFrame
CMainFrame::CMainFrame()
Before pMainFrame->LoadFrame
CMainFrame::PreCreateWindow entry // 注意:PreCreateWindow 被兩次調(diào)用
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() // 文檔對象被創(chuàng)建
CChildFrame::CChildFrame() // 子框架窗口被創(chuàng)建
CChildFrame::PreCreateWindow entry
CChildFrame::PreCreateWindow entry
CChildFrame::PreCreateWindow entry
CMDITestView::CMDITestView() entry // 子框架窗口的 OnCreate 中創(chuàng)建了 View 窗口
CMDITestView::PreCreateWindow entry
CMDITestDoc::OnNewDocument() entry
Before pMainFrame->ShowWindow
Before pMainFrame->UpdateWindow
// 退出時(shí)的 TRACE
CMDITestView::~CMDITestView()
CChildFrame::~CChildFrame()
CMDITestDoc::~CMDITestDoc()
CMainFrame::~CMainFrame()
|