MFC教程-2

2 WinMain入口函数

WinMain流程

现在讨论MFC应用程序如何启动。

WinMain 函数是MFC提供的应用程序入口。进入WinMain前,全局应用程序对象已经生成。WinMain流程如图5-3所示。图中,灰色框是对被调用的虚拟函数的注释,程序员可以或必须覆盖它以实现MFC要求的或用户希望的功能;大括号所包含的图示是相应函数流程的细化,有应用程序对象App的初始化、Run 函数的实现、PumpMessage的流程,等等。

从图中可以看出:

(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所述,这里不作进一步的讨论。

到此,文档对象、边框窗口对象、视窗口对象已经创建,文件已经打开或者创建,边框窗口、视窗口已经创建。现在,是显示和定位窗口、显示文档数据的时候了,这些通过调用CFrameWnd的虚拟函数InitialUpdateFrame完成,如图5-8所示。

窗口初始化

这是第五步,初始化边框窗口、视窗口等。

InitialUpdateFrame的原型如下:

void CFrameWnd::InitialUpdateFrame(CDocument* pDoc, BOOL bMakeVisible)

其中:

第一个参数是当前的文档对象;

第二个参数表示边框窗口是否应该可见并激活。

该函数是在文档模板的OpenDocumentFile中调用的,传递过来的第一个参数是刚才创建的文档,第二个参数是TRUE,见图5-8。

InitialUpdateFrame的处理过程参见图5-14,解释如下:

首先,如果当前边框窗口没有活动视,则获取ID为AFX_IDW_PANE_FIRST的视pView。如果该视不存在,则pView=NULL;否则 (pView!=NULL),调用成员函数SetActiveView(pView,FALSE)把它设置为活动视,参数2为FALSE表示并不通知它成为活动视(见图5-14)。

然后,如果 InitialUpdateFrame的参数bMakeVisible为TRUE,则给所有边框窗口的视发送WM_INITIALUPDATE消息,通知它们在显示之前使用文档数据来初始化视。这导致视类的虚拟函数OnInitUpdate被调用,该函数又调用OnUpdate来完成初始化。其他子窗口(如状态栏、工具栏)也将收到WM_INITIALUPDATE消息,导致它们更新状态。

其三,调用pView->OnActivateFrame(WA_INACTIVE,this)给活动视(若存在的话)一个机会来保存当前焦点。这里,解释这个函数:

void CView::OnActivateFrame( UINT nState,CFrameWnd* pFrameWnd );

其中,参数1取值为WA_INACTIVE/WA_ACTIVE/WA_CLICKACTIVE,具体见消息WM_ACTIVE的解释;参数2指向被激活的框架窗口。

视对象通过该虚拟函数在它所属的边框窗口被激活或者失去激活时作一些特别的处理,例如,CFormView用它来保存或者恢复输入焦点控制。

其四,在OnActivateFrame之后,调用成员函数ActivateFrame激活框架窗口。这个过程将产生一个消息WM_ACTIVE(处理该消息的过程在下一节作解释),它导致OnActiveTopLevel和OnActive被调用。接着,如果活动视非空,则调用成员函数 OnActivateView激活它。

至此,参数bMakeVisible为TRUE时显示窗口的处理完毕。

最后,如果参数pDoc非空,则更新边框窗口计数,更新边框窗口的标题。更新边框窗口计数是为了在一个文档对应多个视的情况下,给显示同一文档的不同文档边框窗口编号,编号从1开始,保存在边框窗口的成员变量m_nWindow里。例如有两个边框对应一个文档tt1,则它们的标题分别为“tt1:1”、 “tt1:2”。如果只有一个文档只对应一个边框窗口,则成员变量m_nWindow等于-1,标题不含编号,如“tt1”。

当然,对于SDI应用程序,不存在一个文档对应多个视的情况。上述情况是针对MDI应用程序而言的。SDI应用程序执行该过程时,相当于MDI程序的一个特例。

图 5-14涉及的一些函数由图5-15、5-15图解。

 

图5-14中的函数SetActiveView的图解如图5-15所示,其原型如下,:

void CFrameWnd::SetActiveView(CView * pViewNew, BOOL bNotify = TRUE)

其中:

参数1指向被设置的视对象,若非视类型的对象,则为NULL;

参数 2表示是否通知被设置的视。

图5-15中的变量m_pViewActive是CFrameWnd的成员变量,用来保存边框窗口的活动视。

图5-15中的流程可以概括为:Deactivate当前视(m_pViewActive非空时);设置当前活动视;若参数bNotify为TRUE,通知pViewNew被激活。

图5-14中的函数ActivateFrame图解如图5-16所示,其原型如下,:

void CFrameWnd::ActivateFrame(UINT nCmdShow)

参数nCmdShow用来传递给CWnd::ShowWindow,指定显示窗口的方式。参数缺省为1,图5-14调用时设置为-1。

该函数用来激活(Activate)和恢复(Restore)边框窗口,使得它对用户可见可用。在初始化、OLE事件、DDE事件等需要显示边框窗口的地方调用。图5-16表示的MFC缺省实现是激活边框窗口并把它放到顶层。

程序员可以覆盖该虚拟函数ActivateFrame来控制边框窗口怎样被激活。

图5-16中的函数BringToTop是CFrameWnd内部使用的成员函数(protected)。它调用::BringWindowToTop把窗口放到Z轴上的顶层。

至此,边框窗口初始化的过程已经描述清楚,视的初始化见下一节。

视的初始化

第六步,在边框窗口初始化之后,初始化视。

如图5-14所示,视、工具条窗口处理消息WM_INITAILUPDATE(MFC内部消息),完成初始化。这里只讨论视的消息处理函数,其原型如下:

void CView::OnInitialUpdate()

图5-14对该函数的注释说明了该函数的特殊之处。其缺省实现是调用OnUpdate(NULL, 0, NULL)更新视。可以覆盖OnInitialUpdate实现自己的初始化。

OnUpdate是一个虚拟函数,其原型如下:

void CView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)

其中:

参数1指向修改文档数据的视;若更新所有的视,设为NULL;

参数2是一个包含了修改信息的long型变量;

参数3指向一个包含修改信息的对象(从CObject派生的对象)。

参数2、参数3是在文档更新对应视的时候传递过来的。

该函数用来更新显示视窗口,反映文档的变化,在MFC中,它为函数CView::OnInitialUpdate和CDocument::UpdateAllViews所调用。其缺省实现是使整个客户区无效。在下一次收到WM_PAINT消息时,重绘无效区。

工具条的初始化见讨论第13章。

激活边框窗口(处理WM_ACTIVE)

第七步,在窗口初始化完成之后,激活并显示出来。

下面讨论边框窗口激活时的处理(对WM_ACTIVE的处理)。

WM_ACTIVE的消息参数

wParam的低阶word指示窗口是被激活还是失去激活:WA_ACTIVE,被鼠标点击以外的方法激活;WA_CLICKACTIVE,由鼠标点击激活;WA_INACTIVE,失去激活;

wParam的高阶word指示窗口是否被最小化;非零表示最小化;

lPararm表示将激活的窗口句柄(WA_INACTIVE),或者将失去激活的窗口句柄(WA_CLICKACTIVE、WA_ACTIVE)。

在标准Windows消息处理的章节中,曾指出处理WM_ACTIVE消息时,先要调用一个函数_AfxHandleActivate,此函数的原型如下:

static void AFXAPI _AfxHandleActivate(CWnd* pWnd,

WPARAM nState,CWnd* pWndOther)

其中:

参数1是接收消息的窗口;

参数2是窗口状态,为WM_ACTIVE的消息参数wParam;

参数3是WM_ACTIVE的消息参数lParam表示的窗口。

_AfxHandleActivate是MFC内部使用的函数,声明和实现均在WinCore.CPP文件中。实现如下:

如果pWnd指向的窗口不是子窗口,而且pWnd和pWndOther窗口的顶级父窗口(TopLevelParent)不是同一窗口,则发送MFC定义的消息WM_ACTIVATETOPLEVEL给pWnd的顶级窗口,消息参数wParam是nState,消息参数lParam指向一个长度为二的数组,数组里存放pWnd和pWndOther所指窗口的句柄。否则,_AfxHandleActivate不作什么。

从这里可以看出:只有顶层的主边框窗口能处理WM_ACTIVE消息,事实上,Windows系统只会给顶层的非子窗口发送WM_ACTIVE消息。

WM_ACTIVATETOPLEVEL消息的处理

CWnd及派生类CFrameWnd实现了对WM_ACTIVATETOPLEVEL消息的处理,分别解释如下:

消息处理函数CWnd::OnActivateTopLevel如果失去激活,则取消工具栏的提示(TOOLTIP)。

消息处理函数CFrameWnd::OnActivateTopLevel调用CWnd的OnActivateTopLevel;如果接收WM_ACTIVE消息的窗口是线程主窗口,则使得其活动的视窗口变成非活动的(OnActive(FALSE, pActiveView,pActiveView)。

从这里可以知道,在顶层窗口接收到WM_ACTIVE消息后,MFC会进行一些固定的处理,然后才调用WM_ACTIVE消息处理函数。

WM_ACTIVE消息的处理

在_AfxHandleActivate和WM_ACTIVATETOPLEVEL消息处理完之后,才是对WM_ACTIVE的处理。CWnd和CFrameWnd都实现了消息处理。

CWnd的消息处理函数:

void CWnd::OnActive(UINT nState, CWnd* pWndOther, BOOL bMinimized)

其中:

参数1取值为WA_INACTIVE/WA_ACTIVE/WA_CLICKACTIVE;

参数2指向激活或者失去激活的窗口,具体同WM_ACTIVE消息;

参数3表示是否最小化。

此函数的实现是调用Default(),作缺省处理。

CFrameWnd的消息处理函数:

void CFrameWnd::OnActive(UINT nState,CWnd* pWndOther, BOOL bMinimized)

首先调用CWnd::OnActivate。

如果活动视非空,消息是WA_ACTIVE/WA_CLICKACTIVE,并且不是最小化,则重新激活当前视,调用了以下函数:

pActiveView->OnActiveView(TRUE,pActiveView,pActiveView);

并且,如果活动视非空,通知它边框窗口状态的变化(激活/失去激活),调用以下函数:

pActiveView->OnActivateFrame(nState, this)。

SDI流程的回顾

从InitialInstance 开始,首先应用程序对象创建文档模板,文档模板创建文档对象、打开或创建文件;然后,文档模板创建边框窗口对象和边框窗口;接着边框窗口对象创建视对象和视窗口。这些创建是以应用程序的文档模板为中心进行的。在创建这些MFC对象的同时,建立了它们之间的关系。创建这些之后,进行初始化,激活主边框窗口,把边框窗口、视窗口显示出来。

这样,一个SDI应用程序就完成了启动过程,等待着用户的交互或者输入。

5.3.4 节将在SDI程序启动流程的基础之上,介绍MDI应用程序的启动流程。两者的相同之处可以这样类比:创建SDI边框窗口、视、文档的过程和创建MDI文档边框窗口、视、文档的过程类似。不同之处主要表现在:主边框窗口的创建不一样;MDI有文档边框窗口的创建,SDI没有;SDI只能一个文档、一个视; MDI可能多文档、多个视。

MDI程序的对象创建

MDI应用程序对象的InitialInstance函数一般含有以下代码:

//第一部分:创建和添加模板

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate(

IDR_TTTYPE,

RUNTIME_CLASS(CTtDoc),

RUNTIME_CLASS(CChildFrame),//custom MDI child frame

RUNTIME_CLASS(CTtView));

AddDocTemplate(pDocTemplate);

//第二部分:创建MFC框架窗口对象和Windows主边框窗口

// 创建主MDI边框窗口

CMainFrame* pMainFrame = new CMainFrame;

if (!pMainFrame->LoadFrame(IDR_MAINFRAME))

return FALSE;

m_pMainWnd = pMainFrame;

//第三部分:处理命令行,命令行空则执行OnFileNew创建新文档

//分析命令行

CCommandLineInfo cmdInfo;

ParseCommandLine(cmdInfo);

// 处理命令行命令

if (!ProcessShellCommand(cmdInfo))

return FALSE;

第四部分:显示和更新主框架窗口

// 主窗口已被初始化,现在显示和更新主窗口

pMainFrame->ShowWindow(m_nCmdShow);

pMainFrame->UpdateWindow();

SDI应用程序对象的InitialInstance和SDI应用程序对象的InitialInstance比较,有以下的相同和不同之处。相同之处在于:

创建和添加模板;处理命令行。

不同之处在于:

创建的模板类型不同。SDI使用单文档模板,边框窗口类从CFrameWnd派生;MDI使用多文档模板,边框窗口类从CMDIChildWnd派生. 主窗口类型不同。SDI的是从CFrameWnd派生的类;MDI的是从CMDIFrameWnd派生的类。 主框架窗口的创建方式不同。SDI在创建或者打开文档时动态创建主窗口对象,然后加载主窗口(LoadFrame)并初始化;MDI使用第二部分的语句来创建动态主窗口对象和加载主窗口,第四部分语句显示、更新主窗口。 命令行处理的用途不一样。SDI一定要有命令行处理部分的代码,因为它导致了主窗口的创建;MDI可以去掉这部分代码,因为它的主窗口的创建、显示等由第二、四部分的语句来处理。

有别于SDI的主窗口加载过程

和SDI应用程序一样,MDI应用程序使用LoadFrame加载主边框窗口,但因为LoadFrame的虚拟属性,所以MDI调用了CMDIFrameWnd的LoadFrame函数,而不是CFrameWnd的LoadFrame。

LoadFrame的参数1指定了资源ID,其余几个参数取缺省值。和SDI相比,第四个创建上下文参数为NULL,因为MDI主窗口不需要文档、视等的动态创建信息。

图 5-17图解了CMdiFrameWnd::LoadFrame的流程:

首先,用同样的参数调用基类CFrameWnd的LoadFrame,其流程如图5-11所示,但由于参数4表示的创建上下文为空,所以,CFrameWnd::LoadFrame在加载了菜单和快捷键之后,给所有子窗口发送WM_INITUPDATE消息。

另外,WM_CREATE消息怎样处理呢?由于CMDIFrameWnd没有覆盖OnCreate,所以还是由基类CFrameWnd::OnCreate 处理。但是它调用虚拟函数OnCreateClient(见图5-12)时,由于CMDIFrameWnd覆盖了该函数,所以动态约束的结果是 CMDIFrameWnd::OnCreateClient被调用,它和基类的OnCreateClient不同,后者CreateView创建MFC视对象和视窗口,前者调用虚拟函数CreateClient创建MDI客户窗口。MDI客户窗口负责创建和管理MDI子窗口。

CreateClient是CMDIFrameWnd的虚拟函数,其原型如下:

BOOL CMDIFrameWnd::CreateClient(

LPCREATESTRUCT lpCreateStruct, CMenu* pWindowMenu);

该函数主要用来创建MDI客户区窗口。它使用Windows系统预定义的“mdiclient”窗口类来创建客户区窗口,保存该窗口句柄在 CMDIFrameWnd的成员变量m_hWndMDIClient中。调用::CreateWindowEx创建客户窗口时传递给它的窗口创建数据参数(第11个参数)是一个CLIENTCREATESTRUCT结构类型的参数,该结构指定了一个菜单和一个子窗口ID:

typedef struct tagCLIENTCREATESTRUCT{

HMENU hWindowMenu;

UINT idFirstChild;

}CLIENTCREATESTRUCT;

hWindowMenu 表示主边框窗口菜单栏上的“Windows弹出菜单项”的句柄。MDICLIENT类客户窗口在创建MDI子窗口时,把每一个子窗口的标题加在这个弹出菜单的底部。idFirstChild是第一个被创建的MDI子窗口的ID号,第二个MDI子窗口ID号为idFirstChild+1,依此类推。

这里,hWindowMenu的指定不是必须的,程序员可以在MDI子窗口激活时进行菜单的处理;idFirstChild的值是AFX_IDM_FIRST_MDICHILD。

综合地讲,CMDIFrameWnd::LoadFrame完成创建MDI主边框窗口和MDI客户区窗口的工作。

创建了MDI边框窗口和客户区窗口之后,接着是处理WM_INITUPDATE消息,进行初始化。但是按SDI应用程序的讨论顺序,下一节先讨论MDI子窗口的创建。

MDI子窗口、视、文档的创建

和SDI应用程序类似,MDI应用程序通过文档模板来动态创建MDI子窗口、视、文档对象。不同之处在于:这里使用了多文档模板,调用的是CMDIChildWnd(或派生类)的消息处理函数和虚拟函数,如果它覆盖了CFrameWnd的有关函数的话。

还是以处理标准命令消息ID_FILE_NEW的OnFileNew为例。

表示OnFileNew的图5-5、表示OnFileOpen的图5-6在多文档应用程序中仍然适用,但表示OpenDocumentFile的图5-8有所不同,其第三步中地单文档模板应当换成多文档模板,关于这一点,参阅图5-8的说明。

(1)多文档模板的OpenDocumentFile

MDI的OpenDocumentFile的原型如下:

CDocument* CMultiDocTemplate::OpenDocumentFile(

LPCTSTR lpszPathName, BOOL bMakeVisible);

它的原型和单文档模板的该函数原型一样,但处理流程比图5-8要简单些:

第一,不用检查是否已经打开了文档;

第二,不用判断是否需要创建框架窗口或者文档对象,因为不论新建还是打开文档都需要创建新的文档框架窗口(MDI子窗口)和文档对象。

除了这两点,其他处理步骤基本相同,调用同样名字的函数来创建文档对象和MDI子窗口。虽然是名字相同的函数,但是参数的值可能有异,又由于C++的虚拟机制和MFC消息映射机制,这些函数可能来自不同层次类的成员函数,因而导致有不同的处理过程和结果,即SDI创建了CFrameWnd类型的对象和边框窗口;MDI则创建了CMDIChildWnd类型的对象和边框窗口。不同之处解释如下:

(2)CMDIChildWnd的虚拟函数LoadFrame

CMDIChildWnd:: LoadFrame代替了图5-8中的CFrameWnd::LoadFrame,两者流程大致相同,可以参见图5-11。但是它们用来创建窗口的函数不同。前者调用了函数CMDIChildWnd::Create(参数1…参数6);后者调用了CFrameWnd::Create(参数1…参数7)。

这两个窗口创建函数,虽然都是虚拟函数,但是有很多不同之处:

前者是CMDIChildWnd定义的虚拟函数,后者是CWnd定义的虚拟函数; 前者在参数中指定了父窗口,即主创建窗口,后者的父窗口参数为NULL; 前者指定了WS_CHILD风格,创建的是子窗口,后者创建一个顶层窗口; 前者给客户窗口m_hWndMDIClient(CMDIFrameWnd的成员变量)发送WM_MDICREATE消息让客户窗口来创建MDI子窗口(主边框窗口的子窗口是客户窗口,客户窗口的子窗口是MDI子窗口),后者调用::CreateEx函数来创建边框窗口; 前者的窗口创建数据是指向MDICREATESTRUCT结构的指针,该结构的最后一个域存放一个指向CCreateContext结构的指针,后者是指向CCreateContext结构的指针。

MDICREATESTRUCT结构的定义如下:

typedef struct tagMDICREATESTRUCT { // mdic

LPCTSTR szClass;

LPCTSTR szTitle;

HANDLE hOwner;

int x;

int y;

int cx;

int cy;

DWORD style;

LPARAM lParam;

}MDICREATESTRUCT;

该结构的用处和CREATESTRUCT类似,只是它仅用于MDI子窗口的创建上,用来保存创建MDI子窗口时的窗口创建数据。域lParam保存一个指向CCreateContext结构的指针。

WM_CREATE的处理函数不同

创建MDI子窗口时发送的WM_CREATE消息由CMDIChildWnd的成员函数OnCreate(LPCREATESTRUCT lpCreateStruct)处理。

OnCreate 函数仅仅从lpCreateStruct指向的数据中取出窗口创建数据,即指向MDICREATESTRUCT结构的指针,并从该结构得到指向 CCreateContext结构的指针pContext,然后调用虚拟函数OnCreateHelper(lpCreateStruct, pContext)。

此处动态约束的结果是调用了CFrameWnd的成员函数OnCreateHelper。SDI应用程序的OnCreate也调用了CFrameWnd::OnCreateHelper,所以后面的处理(创建视等)可参见SDI的流程了。

待MDI子窗口、视、文档对象创建完毕,多文档模板的OpenDocumentFile也调用InitialUpdateFrame来进行初始化。

MDI子窗口的初始化和窗口的激活

(1)MDI子窗口的初始化

完成了 MDI子窗口、视、文档的创建之后,多文档模板的OpenDocumenFile调用边框窗口的虚拟函数InitialUpdateFrame进行初始化,该函数流程参见图5-14。不过,这里this指针指向CMDIChildWnd对象,由于C++虚拟函数的动态约束,初始化过程调用了 CMDIChildWnd的ActivateFrame函数(不是CFrameWnd的ActivateFrame),来显示MDI子窗口,更新菜单等等,见图5-18。

图5-18的说明:

第一,调用基类CFrameWnd的ActivateFrame显示窗口时,由于当前窗口是文档边框窗口,所以没有发送WM_ACTIVATE消息,而是发送消息WM_MDIACTIVATE。

第二,由于Windows不处理MDI子窗口的激活,所以必须由MFC或者程序员来完成。当一个激活的MDI子窗口被隐藏后从可见变成不可见,但它仍然是活动的,这时需要把下一文档边框窗口激活以便用户看到的就是激活的窗口。在没有其他文档边框窗口时,则把该隐藏的文档边框窗口标记为“伪失去激活”。当一个文档边框窗口从不可见变成可见时,检查变量m_bPseudoInactive,若真则该窗口从Windows角度看仍然是激活的,只需要调用 OnMDIActivate把它改成“MFC激活”。OnMDIActivate把变量m_bPseudoInactive的值改变为FALSE。

至此,MDI子窗口初始化调用描述完毕。初始化将导致MDI窗口被显示、激活。下面讨论MDI子窗口的激活。

(2)MDI子窗口的激活

通过给客户窗口发送消息WM_MDIACTIVATE来激活文档边框窗口。客户窗口发送WM_MDIACTIVATE消息给将被激活或者取消激活的MDI子窗口(文档边框窗口),这些子窗口调用消息处理函数OnMDIActivate响应该消息WM_MDIACTIVATE。关于MDI消息,见表5-12。

用户转向一个子窗口(包括文档边框窗口)导致它的顶层(TOP LEVEL)边框窗口收到WM_ACTIVATE消息而被激活,子窗口是文档边框窗口的话将收到WM_MDIACTIVATE消息。

但是,一个边框窗口被其他方式激活时,它的文档边框窗口不会收到WM_MDIACTIVATE消息,而是最近一次被激活的文档边框窗口收到WM_NCACTIVATE消息。该消息由CWnd::Default缺省处理,用来重绘文档边框窗口的标题栏、边框等等。

MDI子窗口用OnMDIActiveate函数处理WM_MDIACTIVATE消息。其原型如下:

void CMDIChildWnd::OnMDIActivate( BOOL bActivate,

CWnd* pActivateWnd,CWnd* pDeactivateWnd );

其中:

参数1表示是激活(TRUE),还是失去激活(FALSE);

参数2表示将被激活的MDI子窗口;

参数3表示将被失去激活的MDI子窗口;

简单地说,该函数把m_bPseudoInactive的值改变为FALSE,调用成员函数OnActivateView通知失去激活的子窗口的视它将失去激活,调用成员函数OnActivateView通知激活子窗口的视它将被激活。

至于MDI主边框窗口,它还是响应WM_ACTIVATE消息而被激活或相反。CMDIFrameWnd没有提供该消息的处理函数,它调用基类CFrameWnd的处理函数OnActivate。

现在,MDI应用程序的启动过程描述完毕。

表5-12 MDI消息

消息

说明

WM_MDIACTIVATE

激活MDI Child窗口

WM_MDICASCADE

CASCADE排列MDI Child窗口

WM_MDICREATE

创建MDI Child窗口

WM_MDIDESTROY

销毁MDI Child窗口

WM_MDIGETACTIVE

得到活动的MDI Child窗口

WM_MDIICONARRANGE

安排最小化了的MDI Child窗口

WM_MDIMAXIMIZE

MDI Child窗口最大化

WM_MDINEXT

激活Z轴顺序的下一MDI Child窗口

WM_MDIREFRESHMENU

根据当前MDI Child窗口更新菜单

WM_MDIRESTORE

恢复MDI Child窗口

WM_MDISETMENU

根据当前MDI Child窗口设置菜单

WM_MDITITLE

TITLE安排MDI Child窗口

 

你可能感兴趣的:(MFC大杂烩)