深入浅出mfc:《MFC深入浅出》小峰电脑书库5(2)

从图中可以看出:
( 1)一些虚拟函数被调用的时机
对应用程序类(线程类)的 InitIntance、ExitInstance、Run、ProcessMessageFilter、OnIdle、PreTranslateMessage来说,InitInstance在应用程序初始化时调用,ExitInstance在程序退出时调用,Run在程序初始化之后调用导致程序进入消息循环,ProcessMessageFilter、OnIdle、PreTranslateMessage在消息循环时被调用,分别用来过滤消息、进行Idle处理、让窗口预处理消息。
( 2)应用程序对象的角色
首先,应用程序对象的成员函数 InitInstance被WinMain调用。对程序员来说,它就是程序的入口点(真正的入口点是WinMain,但MFC向程序员隐藏了WinMain的存在)。由于MFC没有提供InitInstance的缺省实现,用户必须自己实现它。稍后将讨论该函数的实现。
其次,通过应用程序对象的 Run函数,程序进入消息循环。实际上,消息循环的实现是通过CWinThread::Run来实现的,图中所示的是CWinThread::Run的实现,因为CWinApp没有覆盖Run的实现,程序员的应用程序类一般也不用覆盖该函数。
( 3)Run所实现的消息循环
它调用 PumpMessage来实现消息循环,如果没消息,则进行空闲(Idle)处理。如果是WM_QUIT消息,则调用ExitInstance后退出消息循环。
( 4)CWinThread::PumpMessage
该函数在 MFC函数文档里没有描述,但是MFC建议用户使用。它实现获取消息,转换(Translate)消息,发送消息的消息循环。在转换消息之前,调用虚拟函数PreTranslateMessage对消息进行预处理,该函数得到消息目的窗口对象之后,使用CWnd的WalkPreTranslateTree让目的窗口及其所有父窗口得到一个预处理当前消息的机会。关于消息预处理,见消息映射的有关章节。如果是WM_QUIT消息,PumpMessage返回FALSE;否则返回TRUE。

MFC空闲处理
MFC 实现了一个Idle处理机制,就是在没有消息可以处理时,进行Idle处理。Idle处理的一个应用是更新用户接口对象的状态。更新用户接口状态的内容见消息映射的章节。
空闲处理由函数 OnIdle完成,其原型为BOOL OnIdle(int)。参数的含义是当前空闲处理周期已经完成了多少次OnIdle调用,每个空闲处理周期的第一次调用,该参数设为0,每调用一次加1;返回值表示当前空闲处理周期是否继续调用OnIdle。

MFC 的缺省实现里,CWinThread::OnIdle完成了工具栏等的状态更新。如果覆盖OnIdle,需要调用基类的实现。

在处理完一个消息或进入消息循环时,如果消息队列中没有消息要处理,则 MFC开始一个新的空闲处理周期;

当 OnIdle返回FASLE,或者消息队列中有消息要处理时,当前的空闲处理周期结束。
从图5-3中Run的流程上可以清楚的看到MFC空闲处理的情况。

本节描述了应用程序从InitInstance开始初始化、从Run进入消息循环的过程,下面将就SDI应用程序的例子描述该过程中创建各个所需MFC对象的流程。
SDI应用程序的对象创建
如前一节所述,程序从 InitInstance开始。在SDI应用程序的InitInstance里,至少有以下语句:
// 第一部分,创建文档模板对象并把它添加到应用程序的模板链表
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CTView));
AddDocTemplate(pDocTemplate);

// 第二部分,动态创建文档、视、边框窗口等MFC对象和对应的Windows对象
//Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;

// 第三部分,返回TRUE,WinMain下一步调用Run开始消息循环,
// 否则,终止程序
return TRUE;

对于第二部分,又可以分解成许多步骤。
下面将解释每一步。
文档模板的创建
第一步是创建文档模板。
文档模板的作用是动态创建其他 MFC对象,它保存了要动态创建类的动态创建信息和该文档类型的资源ID。这些信息保存在文档模板的成员变量里:m_nIDResource(资源ID)、m_pDocClass(文档类动态创建信息)、m_pFrameClass(边框窗口类动态创建信息)、m_pViewClass(视类动态创建信息)。
资源 ID包括菜单、像标、快捷键、字符串资源的ID,它们都使用同一个ID值,如IDR_MAINFRAME。其中,字符串资源描述了文档类型,由七个被“\n”分隔的子字符串组成,各个子串可以通过CDocTemplate的成员函数GetDocString(CString& rString, enum DocStringIndex index)来获取。DocStringIndex是CDocTemplate类定义的枚举变量以区分七个子串,描述如下(英文是枚举变量名称)。
WindowTitle 应用程序窗口的标题。仅仅对SDI程序指定。
DocName 用来构造缺省文档名的字符串。当用File菜单的菜单项new创建新文档时,缺省文档名由该字符串加一个数字构成。如果空,使用“unitled”。
FileNewName 文档类型的名称,在打开File New对话框时显示。
FilterName 匹配过滤字符串,在File Open对话框用来过滤要显示的文件。如果不指定,File Open对话框的文件类型(file style)不可访问。
FilterExt 该类型文档的扩展名。如果不指定,则不可访问对话框的文件类型(File Style)。
RegFileTypeId 文档类型在Windows 注册库中的存储标识。
RegFileTypeName 文档类型在Windows 注册库中的类型名称。

文档模板被应用程序对象创建和管理。应用程序类 CWinApp有一个CDocManager类型的成员变量m_pDocManager,通过该变量来管理应用程序的文档模板列表,把一些相关的操作委派给CDocManager对象处理。
CDocManager 使用CPtrList类型的m_templateList变量来存储文档模板,并提供了操作文档模板列表的系列函数。

从语句 pDocTemplate = new CSingleDocTemplate(…)可以看出应用程序对象创建模板时传递一个资源ID和三个类的动态创建信息给它:
IDR_MAINFRAME ,资源ID
RUNTIME_CLASS(CTDoc) ,文档类动态创建信息
RUNTIME_CLASS(CMainFrame), 边框窗口类动态创建信息
RUNTIME_CLASS(CTView) ,视类动态创建信息
文档模板对象接收这些信息并把它们保存到对应的成员变量里头。然后 AddDocTemplate实际调用m_pDocManager->AddDocTemplate,把创建的模板对象加入到文档模板管理器的模板列表中,也就是应用程序对象的文档模板列表中。

文件的创建或者打开
第二步是创建或者打开文件。
对于 SDI程序,MFC对象的动态创建过程是在创建或者打开文件中发生的。但是为什么没有看到文件操作相关的语句呢?
CCommandLineInfo
首先,需要弄清楚类 CcommandLineInfo,它是用来处理命令行信息的类,CWinApp::PareCommandLine调用CCommandLineInfo的成员函数ParseParm分析启动程序时的参数,把分析结果保存在CCommandLineInfo对象的成员变量里。CCommandLineInfo的定义如下:
class CCommandLineInfo : public CObject
{
BOOL m_bShowSplash;
BOOL m_bRunEmbedded;
BOOL m_bRunAutomated;

enum { FileNew, FileOpen, FilePrint, FilePrintTo, FileDDE,
AppUnregister, FileNothing = -1 } m_nShellCommand;
// not valid for FileNew
CString m_strFileName;
// valid only for FilePrintTo
CString m_strPrinterName;
CString m_strDriverName;
CString m_strPortName;
};
由上述定义可以看出,分析结果分几类:是否 OLE激活;应该执行什么动作(FileNew、FileOpen等);传递的参数(打开或打印的文件名,打印设备、端口等)。
当命令行空时,执行 FileNew命令。原因在于CCommandLineInfo的缺省构造函数:
CCommandLineInfo::CCommandLineInfo()
{
m_bShowSplash = TRUE;
m_bRunEmbedded = FALSE;
m_bRunAutomated = FALSE;
m_nShellCommand = FileNew;// 指定了SHELL命令操作
}
缺省构造把应该执行的动作指定为 FileNew。

处理命令行命令
其次,分析 CWinApp::ProcessShellCommand(CCommandLineInfo& rCmdInfo)的流程,它处理命令行的命令,流程如图5-3所示。
图 5-4第三层表示根据命令类型进一步调用的函数,都是CWinApp或者派生类的成员函数。对于FILEDDE类型没有进一步的调用。
命令类型是 FILENEW时,调用的函数就是标准命令ID_FILE_NEW对应的处理函数OnFileNew;命令类型是FILEOPEN时调用的函数是OpenDocumentFile,标准命令ID_FILE_OPEN的处理函数OnFileOpen的工作实际上就是由OpenDocumentFile完成的。函数FileNew、OpenDocumentFile导致了窗口、文档的创建。

OnFileNew
接着,分析 CWinApp::OnFileNew流程,如图5-5所示。
图 5-5的说明:
应用程序对象得到文档模板管理器指针,调用文档模板管理器的成员函数OnFileNew(m_pDocManager->OnFileNew());模板管理器获取文档模板对象指针,调用文档模板对象的 OpenDocumentFile 函数(pTemplate->OpenDocumentFile(NULL))。如果模板管理器发现有多个文档模板,就弹出一个对话框让用户选择文档模板。这里和后面的图解中类似于CWinApp::、CDocManager::、CDocTemplate::等的函数类属限制并不表示直接源码中有这样的限制,而是通过指针或者指针的动态约束可以认定调用了某个类的成员函数,其正确性仅仅限于本书图解的MFC的缺省实现。
如图 5-5所示,程序员可以覆盖有关虚拟函数或命令处理函数:如果程序员在自己的应用程序类中覆盖了OnFileNew,则可以实现完全不同的处理流程;一般情况下,不会从文档模板类派生新类,如果派生的话,可以覆盖CDocTemplate的虚拟函数。

OnFileOpen
分析了 OnFileNew后,现在分析CWinApp::OnFileOpen(),其流程如图5-6所示。
CWinApp::OnFileOpen 和OnFileNew类似,不过,第二步须得到一个要打开的文件的名称,第三步调用的是应用程序对象的OpenDocumentFile,而不是文档模板对象的该函数。

应用程序对象的 OpenDocumentFile
分析应用程序的打开文档函数: CWinApp::OpenDocumentFile(LPCSTR name),其流程如图5-7所示。
 
应用程序对象把打开文件操作委托给文档模板管理器,后者又委托给文档模板对象来执行。如果是 SDI程序,则委托给单文档对象;如果是MDI程序,则委托给多文档对象——这是由指针所指对象的实际类型决定的,因为该函数是一个虚拟函数。

文档模板的 OpenDocumentFile
不论是 FileNew还是FileOpen,最后的操作都归结到由文档模板来打开文件(文件名空则创建文件)。
CSingleDocTemplate::OpenDocumentFile(lpcstr name ,BOOL visible)的流程见图5-8。有一点需要指出的是:创建了一个文档对象,并不等于打开了一个文档(件)或者创建了一个新文档(件)。
图 5-8显示的流程大致可以描述如下:
如果已经有文档打开,则保存当前的文档;否则,文档对象还没有创建,需要创建一个新的文档对象。因为这时边框窗口还没有生成,所以还要创建边框窗口对象( MFC对象)和边框窗口。MFC边框窗口对象动态创建,HWND边框窗口由LoadFrame创建。MFC边框窗口被创建时,CFrameWnd的缺省构造函数被调用,它把正创建的对象(this所指)加入到模块-线程状态的边框窗口列表m_frameList之首。
边框窗口创建过程中由 CreateView动态创建MFC视对象和HWND视窗口。
接着,如果没有指定要打开的文件名,则创建一个新的文件;否则,则打开文件,并使用序列化机制读入文件内容。
通过上述过程,动态地创建了 MFC边框窗口对象、视对象、文档对象以及对应的Windows对象,并填写了有关对象的成员变量,建立起这些MFC对象的关系。

打开文件过程中所涉及的消息处理函数和虚拟函数
图5-8描述的整个过程中系列消息处理函数和虚拟函数被调用。例如:在Windwos边框窗口和视窗口被创建时会产生WM_CREATE等消息,导致OnCreate等消息处理函数的调用,CFrameWnd和CView都覆盖了该函数,所以在边框窗口和视窗口的创建中,同样的消息调用了不同的处理函数CFrameWnd::OnCreate和CView::OnCreate。
图5-8涉及的几个虚拟函数的流程分别由图5-9、图5-10图解。图5-9表示CDocument的OnNewDocument的流程;图5-10表示CDocument的OpenDocument的流程。这两个函数分别在创建新文档或者打开一个文档时被调用。从流程可以看出,对于OpenDocument函数,MFC的缺省实现主要用来设置修改标识、序列化读入打开文档的内容。图5-10显示了序列化的操作过程:
首先,使用文档对象打开或者创建的文件句柄创建一个用于读出数据的CArchive对象loadarchive;然后使用它通过Serialize进行序列化操作,完毕,CArchive对象被自动销毁,文件句柄被关闭。
从这些图中可以看到何时、何处调用了什么消息处理函数和虚拟函数,这些函数用来作了什么事情。必要的话,程序员可以在派生类覆盖它们。
在创建工作完成之后,进行初始化,使用文档对象的数据来更新视和显示窗口。

至此,本节描述了MFC的SDI程序从分析命令行到创建或打开文件的处理过程,文档对象已经动态创建。总结如下:
命令行分析→应用程序的FileNew→文档模板的OpenDocumentFile(NULL)→文档的OnNewDocument
命令行分析→应用程序的 FileOPen→文档模板的OpenDocumentFile(filename)→文档的OpenDocument
边框窗口对象、视对象的动态创建和对应 Windows对象的创建从LoadFrame开始,这些将在下一节论述。



SDI边框窗口的创建
第三步是创建 SDI边框窗口。
图 5-8已经分析了创建SDI边框窗口的时机和创建方法,下面,从LoadFrame开始分析整个窗口创建过程。
CFrameWnd::LoadFrame
CFrameWnd::LoadFrame 的流程如图5-11所示,其原型如下:
BOOL CFrameWnd::LoadFrame(UINT nIDResource,
DWORD dwDefaultStyle,
CWnd* pParentWnd,
CCreateContext* pContext)
第一个参数是和该框架相关的资源 ID,包括字符串、快捷键、菜单、像标等;
第二个参数指定框架窗口的“窗口类”和窗口风格;此处创建 SDI窗口时和缺省值相同,为WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE;
第三个参数指定框架窗口的父窗口,此处和缺省值相同,为 NULL;
第四个参数指定创建的上下文,如图 5-8所示由CreateNewFrame生成了该变量并传递给LoadFrame。其缺省值为NULL。
创建上下文结构的定义:
struct CCreateContext
{
CRuntimeClass* m_pNewViewClass; //View 的动态创建信息
CDocument*m_pCurrentDoc;// 指向一文档对象,将和新创建视关联

// 用来创建MDI子窗口的信息(C MDIChildFrame::LoadFrame使用)
CDocTemplate* m_pNewDocTemplate;

// for sharing view/frame state from the original view/frame
CView* m_pLastView;
CFrameWnd* m_pCurrentFrame;
};
这里,传递给 LoadFrame的CCreateContext变量是:
( 视的动态创建信息,新创建的文档对象,当前文档模板,NULL,NULL)。
其中,“新创建的文档对象”就是图 5-8中创建的那个文档对象。从此图中还可以看到,LoadFrame被CreateNewFrame调用,CreateNewFrame是文档模板的成员函数,被文档模板的成员函数OpenDocumentFile所调用,所以,LoadFrame间接地被文档模板调用,“当前文档模板”就是调用它的模板对象。顺便指出,对SDI程序来说是这样的,对MDI程序有所不同。“视的动态创建信息”也是文档模板传递过来的。

对图 5-11的说明:
在创建边框窗口之前,先注册“窗口类”。 LoadFrame注册了两个“窗口类”,一个为边框窗口,一个为视窗口。关于“窗口类”注册,见2.2.1节。
注册窗口类之后,创建边框窗口,并加载资源。创建边框窗口使用了 CFrameWnd的Create虚拟函数,最终调用::CreateEx创建窗口。::CreateEx有11个参数,其最后一个参数就是文档模板传递给LoadFrame的CCreateContext类型的指针,该指针将被传递给窗口过程,进一步由Windows传递给OnCreate函数。顺便指出,创建完毕的边框窗口的窗口过程是统一的MFC窗口过程。
创建边框窗口时,发送消息 WM_NCCREATE和WM_CREATE,导致对应的消息处理函数OnNcCreate和OnCreate被调用。CWnd提供了OnNcCreate处理非客户区创建消息,CFrameWnd没有处理该消息,但是提供了OnCreate处理消息WM_CREATE。OnCreate将创建视对象和视窗口。

CFrameWnd::OnCreate
按创建工作的进度,现在要讨论边框窗口创建消息( WM_CREATE)的处理了,处理函数是CFrameWnd的OnCreate,其原型如下:
int CFrameWnd::OnCreate(LPCREATESTRUCT lpcs)
其中,参数指向一个 CreateStruct结构(关于CreateStruct的描述见4.4.1节),它包含了窗口创建参数的副本,也就是说CreaeEx窗口创建函数的11个参数被对应地复制到该结构的11个域,例如它的第一个成员就可以转换成CCreateContext类型的指针。
函数 OnCreate处理WM_CREATE消息,它从lpcs指向的结构中分离出lpCreateParams并把它转换成为CCreateContext类型的指针pContext,然后,调用OnCreateHelp(lpcs,pContext),把创建工作委派给它完成。
CFrameWnd::OnCreateHelp 的原型如下,流程见图5-11。
int CFrameWnd::OnCreateHelp(LPCREATESTRUCT lpcs,
CCreateContext* pContext)
说明:由于 CFrameWnd覆盖了消息处理函数OnCreate来处理WM_CREATE消息,所以CWnd就失去了处理该消息的机会,除非CFrameWnd::OnCreate主动调用基类的该消息处理函数。图5-11展示了对CWnd::OnCreate的调用。
在边框窗口被创建之后,调用虚拟函数 OnCreateClient(lpcs,pContext),它的缺省实现将创建视对象和视窗口。
最后,在状态栏显示“ Ready”字样,调用RecalcLayout安排工具栏等的位置。关于WM_SETMESSAGESTRING消息和RecalcLayout函数,见工具栏有关13.2.3节。
到此, SDI的边框窗口已经被创建。下一节将描述视的创建。
视的创建
第四步,创建视。
如前一节所述,若 CFrameWnd::OnCreateClient(lpcs,pContext)判断pContext包含了视的动态创建信息,则调用函数CreateView创建视对象和视窗口。CreateView的原型如下,其流程如图5-13所示。
CWnd * CFrameWnd::CreateView(CCreateContext* pContext, UINT nID)
其中:
第一个参数是创建上下文;
第二个参数是创建视 (子窗口)的ID,缺省是AFX_IDW_PANE_FIRST,这里等同缺省值。
说明:
CreateView 调用了CWnd的Create函数创建HWND视窗口,视的子窗口ID是AFX_IDW_PANE_FIRST,父窗口是创建它的边框窗口。创建视窗口时的WM_CREATE、WM_NCCREATE消息分别被CView、CWnd的相关消息处理函数处理。处理情况如图5-13所述,这里不作
Tags:  深入浅出mfc

延伸阅读

最新评论

发表评论