温故而知新,学习MFC框架如何创建的过程
温故而知新,学习MFC框架如何创建的过程
选择自 liukaven 的 Blog
很久没有使用 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()
查看更多关于温故而知新,学习MFC框架如何创建的过程的详细内容...