///
/********* 文章系列:MFC技术内幕系列***********/
/************MFC技术内幕系列之(一)***********/
/****文章题目:MFC应用程序“生死因果”内幕*****/
/* Copyright(c)2002 bigwhite */
/* All rights Reserved */
/***********关键字:MFC,生死因果**************/
/* 时间:2002.7.23 */
/* 注释:本文所涉及的程序源代码均在Microsoft */
/ Visual Studio.Net Enterprise Architect Edition /
/* 开发工具包提供的源代码中 */
引言:
侯捷老师在他那本著名的"深入浅出MFC"(第二版)的第六章中对比着传统的Win32API编程,详细讲解了MFC应用程序“生死因果”,而且侯捷老师还在"深入浅出MFC"(第二版)一书的“无责任书评”中称应用程序和MFC Framework的因果关系,是学习MFC程序设计的关键,并把它作为学习MFC程序设计的"第一个台阶".
作为已是“过来人”的我非常赞同侯捷老师的观点,特写下此篇文章以供大家参考,本文章特别对MFC程序设计的初学者大有裨益。
正文:
初学MFC程序设计的人(甚至包括已经很精通Win32API编程的大虾们)都会感到很疑惑,对MFC应用程序的运行流程不能马上领悟,多数人都会提出类似"WinMain函数跑到哪里去了?","窗口函数(WinProc),消息循环好像一下子都消失了?"等问题。下面就让我们看一个MFC SDI应用程序的运行流程并挖掘一下MFC库的源代码,来尽力争取弄清MFC应用程序“生死因果”的内幕。
/* 1. Windows 帮忙 */
/* 程序诞生! */
//
Windows 操作系统为应用程序创建进程核心对象,并为该应用程序分配4GB的进程地址空间,系统加载器
将应用程序可执行文件映像以及一些必要的代码(包括数据和一些应用程序使用的dlls)加载到应用程序的进程地址空间中。
/
/* 2.启动函数是什么? */
/
Windows 操作系统在初始化该应用程序进程的同时,将自动为该应用程序创建一个主线程,该主线程与
C/C++运行时库的启动函数一道开始运行。很多初学者并不知道C/C++运行时库的启动函数是何方神圣,这里我
简单介绍一下:当你的应用程序编译后开始链接时,系统的链接器会根据你的应用程序的设置为你的应用程序
选择一个C/C++运行时库的启动函数(注释:这些函数声明在../Visual Studio.NET/vc7/crt/src/crt0.c中)
一般的ANSI版本的GUI的应用程序的C/C++运行时库的启动函数为:
int WinMainCRTStartup(void);
其它版本的C/C++运行时库的启动函数如下:
ANSI版本的CUI的应用程序: int mainCRTStartup(void);
Unicode版本的CUI的应用程序: int wmainCRTStartup(void);
Unicode版本的GUI的应用程序: int wWinMainCRTStartup(void);
C/C++运行时库的启动函数的主要功能为初始化C/C++运行时库和为所有全局和静态的C++类对象调用构造函数。
/
/* 3.侯捷老师所说的"引爆器" */
/
前面所说的C/C++运行时库的启动函数的主要功能之一是为所有全局和静态的C++类对象调用构造函数。侯捷老师所说的"引爆器"---CMyWinApp theApp这个Application Object就是由启动函数调用其构造函数构造出来的。CWinApp的构造函数到底作了什么?看看源代码吧,源代码最能说明问题了。
注释:CWinApp的构造函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appcore.cpp
CWinApp::CWinApp(LPCTSTR lpszAppName)
{
if (lpszAppName != NULL)
m_pszAppName = _tcsdup(lpszAppName);
else
m_pszAppName = NULL;
// initialize CWinThread state
AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();
AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;
ASSERT(AfxGetThread() == NULL);
pThreadState->m_pCurrentWinThread = this;
ASSERT(AfxGetThread() == this);
m_hThread = ::GetCurrentThread();
m_nThreadID = ::GetCurrentThreadId();
// initialize CWinApp state
ASSERT(afxCurrentWinApp == NULL); // only one CWinApp object please
pModuleState->m_pCurrentWinApp = this;
ASSERT(AfxGetApp() == this);
// in non-running state until WinMain
m_hInstance = NULL;
m_hLangResourceDLL = NULL;
m_pszHelpFilePath = NULL;
m_pszProfileName = NULL;
m_pszRegistryKey = NULL;
m_pszExeName = NULL;
m_pRecentFileList = NULL;
m_pDocManager = NULL;
m_atomApp = m_atomSystemTopic = NULL;
m_lpCmdLine = NULL;
m_pCmdInfo = NULL;
// initialize wait cursor state
...//
// initialize current printer state
...//
// initialize DAO state
m_lpfnDaoTerm = NULL; // will be set if AfxDaoInit called
// other initialization
...//
}
从源代码中可以看出CWinApp的构造函数主要收集了一些关于应用程序主线程的信息及初始化一些相关应用程序的信息。值得注意的是CWinApp类的一些主要的数据成员如:m_hInstance,m_lpCmdLine,m_pCmdInfo及m_atomApp等都初始化为NULL,这些成员在后面将被重新赋值。
//
/* 4. WinMain函数登场了 */
//
C/C++运行时库的启动函数int WinMainCRTStartup(void);所调用的WinMain函数---同时也是主线程的入口函数为:
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine
,int nCmdShow);
注释1:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appmodul.cpp中
注释2:_t 是为了照顾Unicode版本而定义的宏。
讲到这个时候你也许会稍稍展开你那紧皱的眉头,不过也许你还会问:"MFC中的WinMain函数到底作了什么?" 其实很简单,看看源代码就知道了。
extern "C" int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
// call shared/exported WinMain
return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}
这一下清楚了,MFC中的WinMain函数其实什么也没做,只是调用了一个函数AfxWinMain。
/
/* 5.MFC程序的入口点函数 */
//
MFC作了一个"乾坤大挪移",将WinMain函数的全部责任转移交给了MFC程序的入口点函数---AfxWinMain。
注释:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/winmain.cpp中。
// Standard WinMain implementation
// Can be replaced as long as 'AfxWinInit' is called first
int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();
// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;
// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;
// Perform specific initializations
if (!pThread->InitInstance())
{
if (pThread->m_pMainWnd != NULL)
{
TRACE(traceAppMsg, 0, "Warning: Destroying non-NULL m_pMainWnd/n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();
InitFailure:
#ifdef _DEBUG
// Check for missing AfxLockTempMap calls
if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
{
TRACE(traceAppMsg, 0, "Warning: Temp map lock count non-zero (%ld)./n",
AfxGetModuleThreadState()->m_nTempMapLock);
}
AfxLockTempMaps();
AfxUnlockTempMaps(-1);
#endif
AfxWinTerm();
return nReturnCode;
}
从上面源代码可以看出AfxWinMain函数主要由四大模块组成,他们分别是AfxWinInit,InitApplication,
InitInstance,Run。下面将分别介绍这四大模块的功能。
///
/* 5.1 AFX的内部初始化 */
///
AfxWinInit函数是既CWinApp类构造函数后的又一个重量级的函数。不妨看一下它的源代码:
注释:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appinit.cpp中。
BOOL AFXAPI AfxWinInit(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL);
// handle critical errors and avoid Windows message boxes
SetErrorMode(SetErrorMode(0) |
SEM_FAILCRITICALERRORS|SEM_NOOPENFILEERRORBOX);
// set resource handles
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
pModuleState->m_hCurrentInstanceHandle = hInstance;
pModuleState->m_hCurrentResourceHandle = hInstance;
// fill in the initial state for the application
CWinApp* pApp = AfxGetApp();
if (pApp != NULL)
{
// Windows specific initialization (not done if no CWinApp)
pApp->m_hInstance = hInstance;
hPrevInstance; // Obsolete.
pApp->m_lpCmdLine = lpCmdLine;
pApp->m_nCmdShow = nCmdShow;
pApp->SetCurrentHandles();
}
// initialize thread specific data (for main thread)
if (!afxContextIsDLL)
AfxInitThread();
// Initialize CWnd::m_pfnNotifyWinEvent
HMODULE hModule = ::GetModuleHandle(_T("user32.dll"));
if (hModule != NULL)
{
CWnd::m_pfnNotifyWinEvent = (CWnd::PFNNOTIFYWINEVENT)::GetProcAddress(hModule, "NotifyWinEvent");
}
return TRUE;
}
还记得我在第三个标题---侯捷老师所说的"引爆器"处 的话么,"CWinApp类的一些主要的数据成员在后面将被重新赋值。",AfxWinInit函数就是这些数据成员被赋值的地方,它重新初始化这些在整个程中都扮演重要角色的成员,并且调用AfxInitThread()为主线程作了一些初始化工作,这些都为以后MFC框架的正常运作铺平了道路。
/
/* 5.2 应用程序的全局初始化 */
/
InitApplication函数(virtual)为程序进行全局初始化:
注释1:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appcore.cpp中。
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;
}
由于初次调用时CDocManager::pStaticDocManager==0x00000000;m_pDocManager==0x00000000;所以InitApplication函数只是调用了CWinApp::LoadSysPolicies();而后者将加载一些注册表的信息用来初始化一些程序定义的结构并为程序注册一些基本信息。(由于该函数可能尚未文档化,所以关于LoadSysPolicies函数的说明只是看了源代码后的推测,下面列出了它的部分源代码仅供参考)
注释2:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appcore.cpp中。
BOOL CWinApp::LoadSysPolicies()
{
HKEY hkPolicy = NULL;
DWORD dwValue = 0;
DWORD dwDataLen = sizeof(dwValue);
DWORD dwType = 0;
// clear current policy settings.
m_dwPolicies = _AFX_SYSPOLICY_NOTINITIALIZED;
static _AfxSysPolicyData rgExplorerData[] =
{
{_T("NoRun"), _AFX_SYSPOLICY_NORUN},
{_T("NoDrives"), _AFX_SYSPOLICY_NODRIVES},
{_T("RestrictRun"), _AFX_SYSPOLICY_RESTRICTRUN},
{_T("NoNetConnectDisconnect"), _AFX_SYSPOLICY_NONETCONNECTDISCONNECTD},
{_T("NoRecentDocsHistory"), _AFX_SYSPOLICY_NORECENTDOCHISTORY},
{_T("NoClose"), _AFX_SYSPOLICY_NOCLOSE},
{NULL, NULL}
};
...//
static _AfxSysPolicyData rgComDlgData[] =
{
{_T("NoPlacesBar"), _AFX_SYSPOLICY_NOPLACESBAR},
{_T("NoBackButton"), _AFX_SYSPOLICY_NOBACKBUTTON},
{_T("NoFileMru"), _AFX_SYSPOLICY_NOFILEMRU},
{NULL, NULL}
};
static _AfxSysPolicies rgPolicies[] =
{
{_T("Software//Microsoft//Windows//CurrentVersion//Policies//Explorer"),
rgExplorerData},
{_T("Software//Microsoft//Windows//CurrentVersion//Policies//Network"),
rgNetworkData},
{_T("Software//Microsoft//Windows//CurrentVersion//Policies//Comdlg32"),
rgComDlgData},
{NULL, NULL}
};
_AfxSysPolicies *pPolicies = rgPolicies;
_AfxSysPolicyData *pData = NULL;
...//
}
注释3:在MFC文档中有这么一句话"The CWinApp::InitApplication member function is obsolete in MFC.",所以你大多情况下不用在意这个virtual函数。
/
/* 5.3 应用程序的标准实例化 */
//
CWinApp::InitInstance()是一个虚函数,大多数应用程序都要override这个函数。让我们看看应用程序向导MFC AppWizard(.exe)为SDI 程序作出的override后的代码吧!
BOOL CMyWinApp::InitInstance()
{
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControls()。否则,将无法创建窗口。
InitCommonControls();
CWinApp::InitInstance();//显式调用基类的InitInstance()
// 初始化 OLE 库
if (!AfxOleInit())
{
AfxMessageBox(IDP_OLE_INIT_FAILED);
return FALSE;
}
AfxEnableControlContainer();
// 标准初始化
SetRegistryKey(_T("应用程序向导生成的本地应用程序"));
LoadStdProfileSettings(4); // 加载标准 INI 文件选项(包括 MRU)
// 注册应用程序的文档模板。
// 文档模板将用作文档、框架窗口和视图之间的连接
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTestDoc),
RUNTIME_CLASS(CMainFrame), // 主 SDI 框架窗口
RUNTIME_CLASS(CTestView));
pDocTemplate->SetContainerInfo(IDR_CNTR_INPLACE);
AddDocTemplate(pDocTemplate);
// 分析标准外壳命令、DDE、打开文件操作的命令行
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// 唯一的一个窗口已初始化,因此显示它并对其进行更新
m_pMainWnd->ShowWindow(SW_HIDE);
m_pMainWnd->UpdateWindow();
return TRUE;
}
CMyWinApp::InitInstance()先显式调用了基类的InitInstance();
我们先看看这个基类的函数的定义吧!
注释1:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appcore.cpp中。
BOOL CWinApp::InitInstance()
{
InitLibId();
m_hLangResourceDLL = LoadAppLangResourceDLL();
if(m_hLangResourceDLL != NULL)
{
AfxSetResourceHandle(m_hLangResourceDLL);
_AtlBaseModule.SetResourceInstance(m_hLangResourceDLL);
}
return TRUE;
}
注释2:vc.net中的CWinApp::InitInstance()已与vc6.0中的CWinApp::InitInstance()有所区别。
基类的InitInstance()
先 调用InitLibId()函数用于Initializes the data member containing the GUID of the current module;不过该函数现在为空,估计以后微软会填充该函数。
之后调用LoadAppLangResourceDLL()函数加载应用程序所需资源;在vc6.0中的CWinApp::InitInstance()函数只有一条语句:即return TRUE;
CMyWinApp::InitInstance()在其基类的帮助后,开始执行它自己的一系列代码来完成诸如"初始化 OLE 库","设置注册表主键以使程序能保存信息到注册表中","分析标准外壳命令","生成程序主框架,文档和视图结构","显示程序主窗口"等工作。
注释3:有关应用程序是如何在CMyWinApp::InitInstance()完成上面一系列工作的,将在本系列文章之二 的“MFC文档视图结构内幕”一文中详述。
注释4:在MSDN中有关InitInstance的叙述如下:"Windows allows several copies of the same program to run at the same time."
/* 5.4 "消息泵"启动了 */
众所周知,Windows是一个以消息为基础,以事件驱动的操作系统,每一个Win32程序也都是如此。那么
MFC应用程序是如何实现消息机制的呢?MFC应用程序框架将这种消息机制包装到了一个"消息泵"中,而这个"消息泵"在CMyWinApp::InitInstance()中被启动了。其源代码如下:
注释1:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appcore.cpp中。
// Main running routine until application exits
int CWinApp::Run()
{
if (m_pMainWnd == NULL && AfxOleGetUserCtrl())
{
// Not launched /Embedding or /Automation, but has no main window!
TRACE(traceAppMsg, 0, "Warning: m_pMainWnd is NULL in CWinApp::Run - quitting application./n");
AfxPostQuitMessage(0);
}
return CWinThread::Run();
}
由上面的源代码看出:CWinApp::Run()调用了其基类的Run()函数,继续看源代码:
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));
}
}
//CWinThread implementation helpers
BOOL CWinThread::PumpMessage()
{
return AfxInternalPumpMessage();
}
BOOL AFXAPI AfxInternalPumpMessage()//部分源码
{
_AFX_THREAD_STATE *pState = AfxGetThreadState();
if (!::GetMessage(&(pState->m_msgCur), NULL, NULL, NULL))
{
...//
return FALSE;
}
...//
// process this message
if (pState->m_msgCur.message != WM_KICKIDLE && !AfxPreTranslateMessage(&(pState->m_msgCur)))
{
::TranslateMessage(&(pState->m_msgCur));
::DispatchMessage(&(pState->m_msgCur));
}
return TRUE;
}
终于出现了::TranslateMessage和::DispatchMessage,熟悉Win32API编程的人一定会眼睛一亮,终于挖出源头了。
/
/* 6. 收尾工作 */
MFC应用程序的主要流程几乎都已被挖掘完了,下面看一下收尾工作是如何进行的。
当应用程序发现消息队列中出现了WM_QUIT消息时, nReturnCode = pThread->Run();CWinApp::Run()返回,并设置返回值。下面将执行AfxWinTerm函数。其源代码如下:
注释1:该函数定义在../Visual Studio.NET/vc7/atlmfc/src/mfc/appterm.cpp中。
// Standard cleanup called by WinMain and AfxAbort
void AFXAPI AfxWinTerm(void)
{
// unregister Window classes
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
AfxLockGlobals(CRIT_REGCLASSLIST);
LPTSTR lpsz = pModuleState->m_szUnregisterList;
while (*lpsz != 0)
{
LPTSTR lpszEnd = _tcschr(lpsz, '/n');
ASSERT(lpszEnd != NULL);
*lpszEnd = 0;
UnregisterClass(lpsz, AfxGetInstanceHandle());
lpsz = lpszEnd + 1;
}
pModuleState->m_szUnregisterList[0] = 0;
AfxUnlockGlobals(CRIT_REGCLASSLIST);
// cleanup OLE if required
CWinThread* pThread = AfxGetApp();
if (pThread != NULL && pThread->m_lpfnOleTermOrFreeLib != NULL)
(*pThread->m_lpfnOleTermOrFreeLib)(TRUE, FALSE);
// cleanup thread local tooltip window
AFX_MODULE_THREAD_STATE* pModuleThreadState = AfxGetModuleThreadState();
if (pModuleThreadState->m_pToolTip != NULL)
{
if (pModuleThreadState->m_pToolTip->DestroyToolTipCtrl())
pModuleThreadState->m_pToolTip = NULL;
}
_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
if (!afxContextIsDLL)
{
// unhook windows hooks
if (pThreadState->m_hHookOldMsgFilter != NULL)
{
::UnhookWindowsHookEx(pThreadState->m_hHookOldMsgFilter);
pThreadState->m_hHookOldMsgFilter = NULL;
}
if (pThreadState->m_hHookOldCbtFilter != NULL)
{
::UnhookWindowsHookEx(pThreadState->m_hHookOldCbtFilter);
pThreadState->m_hHookOldCbtFilter = NULL;
}
}
}
由源代码可以看出:该函数主要作一些清除工作,将该释放的东西释放,比如卸载钩子等。
等到AfxWinTerm函数结束,AfxWinMain函数返回nReturnCode值,且该值也将作为_WinMain函数的返回值返回。
让我们回过头来再看一看C/C++运行时库的启动函数的源代码:
注释2:函数定义在../Visual Studio.NET/vc7/crt/src/crtexe.c中
int WinMainCRTStartup(void)//部分源代码
{
int argc; /* three standard arguments to main */
_TSCHAR **argv;
_TSCHAR **envp;
int argret;
int mainret;
int managedapp;
#ifdef _WINMAIN_
_TUCHAR *lpszCommandLine;
STARTUPINFO StartupInfo;
#endif /* _WINMAIN_ */
_startupinfo startinfo;
/*
* Determine if this is a managed application
*/
managedapp = check_managed_app();
...//
StartupInfo.dwFlags = 0;
GetStartupInfo( &StartupInfo );
#ifdef WPRFLAG
mainret = wWinMain(
#else /* WPRFLAG */
mainret = WinMain(
#endif /* WPRFLAG */
GetModuleHandleA(NULL),
NULL,
lpszCommandLine,
StartupInfo.dwFlags & STARTF_USESHOWWINDOW
? StartupInfo.wShowWindow
: SW_SHOWDEFAULT);
...//
if ( !managedapp )
exit(mainret);
...//
return mainret;
}
WinMain函数将返回值传给mainret,WinMainCRTStartup调用C运行时函数exit(int status);后者做什么了呢?看看微软自己的文档重视如何说的:
"The exit functions terminate the calling process. exit calls, in last-in-first-out (LIFO) order, the functions registered by atexit and _onexit, then flushes all file buffers before terminating the process.The status value is typically set to 0 to indicate a normal exit and set to some other value to indicate an error."
事实上,exit函数除了做以上工作外,还为所有全局的和静态的C++类对象调用析构函数(如~CMyWinApp),
将返回值传递给Windows操作系统的ExitProcess函数,使得操作系统可以撤销该进程并设置它的exit代码。
//
/* 7. 结束了 */
/
经过这么大篇幅的挖掘,一个MFC应用程序从生到死的流程我们都已目睹完了,相信你一定有所收获,在本文中涉及到一些细节的时候我有时只是一句话代过,因为我将在以后的系列文章中详细讲解,比如:MFC文档视图结构等细节,希望读者能继续关注我的文章。
/* 8. "下集预告" */
MFC技术内幕系列之(二)---《MFC文档视图结构内幕 》