前面几章介绍了MFC的核心概念和思想,即介绍了MFC对Windows对象的封装方法和特点;MFC对象的动态创建、序列化;MFC消息映射机制。
现在,考查MFC的应用程序结构体系,即以文档-视为核心的编程模式。学习本章,应该弄清楚以下问题:
MFC中诸多MFC对象的关系:应用程序对象,文档对象,边框窗口对象,文档边框窗口对象,视对象,文档模板对象等。
MFC对象的创建和销毁:由什么对象创建或销毁什么对象,何时创建,何时销毁?
MFC提供了那些接口来支持其编程模式?
MFC对象的关系
创建关系
这里讨论应用程序、文档模板、边框窗口、视、文档等的创建关系。图5-1大略地表示了创建顺序,但表5-1更直接地显示了创建与被创建的关系。
表5-1 MFC对象的创建关系
创建者 |
被创建的对象 |
应用程序对象 |
文档模板 |
文档模板 |
文档 |
文档模板 |
边框窗口 |
边框窗口 |
视 |
交互作用关系
应用程序对象有一个文档模板列表,存放一个或多个文档模板对象;文档模板对象有一个打开文档列表,存放一个或多个已经打开的文档对象;文档对象有一个视列表,存放显示该文档数据的一个或多个视对象;还有一个指针指向创建该文档的文档模板对象;视有一个指向其关联文档的指针,视是一个子窗口,其父窗口是边框窗口(或者文档边框窗口);文档边框窗口有一个指向其当前活动视的指针;文档边框窗口是边框窗口的子窗口。
Windows 管理所有已经打开的窗口,把消息或事件发送给目标窗口。通常,命令消息发送给主边框窗口。
图5-2大略地表示了上述关系:
MFC提供了一些函数来维护这些关系。
表5-2列出了从一个对象得到相关对象的方法。
表5-2 从一个对象得到另一个对象的方法
本对象 |
要得到的对象 |
使用的成员函数 |
CDocument对象 |
视列表 |
GetFirstViewPosition GetNextView |
文档模板 |
GetDocTemplate |
|
CView对象 |
文档对象 |
GetDocument |
边框窗口 |
GetParentFrame |
|
CMDIChildWnd或 CFrameWnd对象 |
活动视 |
GetActiveView |
活动视的文档 |
GetActiveDocument |
|
CMDIFrameWnd对象 |
活动文档边框窗口 |
MDIGetActive |
表5-3 从一个对象通知另一个对象的方法:
本对象 |
要通知的对象/动作 |
使用的成员函数 |
CView对象 |
通知文档更新所有视 |
CDocument::UpdateAllViews |
CDocument对象 |
更新一个视 |
CView::OnUpdate |
CFrameWnd或 CMDIFrameWnd对象 |
通知一个视为活动视 |
CView::OnActivateView |
设置一个视为活动视 |
SetActivateView |
可以通过表5-2得到相关对象,再调用表5-3中相应的函数。例如:视在接受了新数据或者数据被修改之后,使用表5-2中的函数GetDocument得到关联文档对象,然后调用表5-3中的文档函数UpdateAllViews更新其他和文档对象关联的视。
在表5-2和表5-3中,CView对象指CView或派生类的实例;成员函数列中如果没有指定类属,就是第一列对象的类的成员函数。
MFC提供的接口
MFC编程就是把一些应用程序特有的东西填入MFC框架。MFC提供了两种填入的方法:一种就是使用前一章论述的消息映射,消息映射给应用程序的各种对象处理各种消息的机会;另一种就是使用虚拟函数,MFC在实现许多功能或者处理消息、事件的过程中,调用了虚拟函数来完成一些任务,这样就给了派生类覆盖这些虚拟函数实现特定处理的机会。
下面两节将列出两类接口,有两个目的:一是为了让读者获得整体印象,二是后文将涉及到或者讨论其中的许多函数时,不显得突兀。
虚拟函数接口
几乎每一个MFC类都定义和使用了虚拟成员函数,程序员可以在派生类中覆盖它们。一般,MFC提供了这些函数的缺省实现,所以覆盖函数应该调用基类的实现。这里给出一个MFC常用虚拟函数的总览表(见表5-4),更详细的信息或它们的缺省实现动作参见MFC文档。由于基类的虚拟函数被派生类继承,所以在派生类中不作重复说明。
覆盖基类的虚拟函数可以通过ClassWizard进行,不过,并非所有的函数都可以这样,有的必须手工加入函数声明和实现。
表5-4 常见MFC类的虚拟函数接口
类 |
虚拟函数 |
覆盖的目的和功能 |
CCmdTarget |
OnCmdMsg |
发送、派发命令消息 |
OnFinalRelease |
OLE用途,引用为0时作清理工作 |
|
CWinThread |
ExitInstance |
在线程退出时作清理工作 |
InitInstance |
在线程开始时作初始化 |
|
OnIdle |
执行thread-specific idle-time处理 |
|
PreTranslateMessage |
在消息送给Windows函数TranslateMessage and DispatchMessage.之前进行消息过滤 |
|
IsIdleMessage |
检查是否是某个特别的消息 |
|
ProcessWndProcException |
截获线程消息/命令处理中的例外 |
|
ProcessMessageFilter |
线程消息过滤 |
|
Run |
实现线程特定的消息循环 |
|
CWinApp |
HideApplication |
关闭所有的窗口之前隐藏应用程序 |
CloseAllDocument |
退出程序之前关闭所有文档 |
|
转下页 |
续表 |
||
SaveModifiedDocument |
框架窗口关闭时用来保存文档 |
|
DoMessageBox |
实现客户化的messagebox |
|
DoWaitCursor |
关闭或打开等待光标 |
|
OnDDeCommand |
响应DDE命令 |
|
WinHelp |
调用WinHelp函数 |
|
CWnd |
WindowProc |
提供一个窗口过程 |
DefWindowProc |
为应用程序不处理的消息提供缺省处理 |
|
PostNcDestroy |
在窗口销毁之后被消息处理函数OnNcDestroy调用 |
|
OnNotify |
处理通知消息WM_NOTIFY |
|
OnChildNotify |
父窗口调用它给控制子窗口一个机会来处理通知反射消息 |
|
DoDataExchange |
Updata调用它来进行对话框数据交换和验证 |
|
CFrameWnd |
GetMessageBar |
返回一个指向框架窗口的状态条的指针 |
OnCreateClient |
创建框架的客户窗口 |
|
OnSetPreviewMode |
设置程序的主框架窗口进入或退出打印预览模式 |
|
NegotiateBorderSpace |
协调边框窗口的边框空间的大小(OLE用途) |
|
CMDIFrameWnd |
CreateClient |
创建CMDIFrameWnd的MDICLIENT窗,被CWnd的消息处理函数OnCreate调用. |
转下页 |
续表 |
||
GetWindowMenuPopup |
返回窗口的弹出式菜单 |
|
CDialog |
OnInitDialog |
对话框窗口的初始化 |
OnSetFont |
设置对话框控制的文本字体 |
|
OnOK |
模式对话框的OK按钮按下后进行的处理 |
|
OnCancel |
模式对话框的CANCEL按钮按下后进行的处理 |
|
CView |
IsSelected |
测试是否有一个文档被选择(OLE支持) |
OnActivateView |
视窗口激活时调用 |
|
OnActivateFrame |
当包含视窗口的框架窗口变成活动或非活动窗口时调用 |
|
OnBeginPrinting |
打印工作开始时调用,用来分配GDI资源 |
|
OnDraw |
用来屏幕显示、打印、打印预览文档内容 |
|
OnEndPrinting |
打印工作结束时调用,释放GDI资源 |
|
OnEndPrintPreview |
退出打印预览模式时调用 |
|
OnPrepareDC |
OnDraw或OnPrint之前调用,用来准备设备描述表 |
|
OnPreparePrinting |
文档打印或者打印预览前调用,可用来初始化打印对话框 |
|
OnPrint |
用来打印或打印预览文档 |
|
OnUpdate |
用来通知一个视的关联文档内容已经变化 |
|
CDocTemplate |
MatchDocType |
确定文档类型和文档模板匹配时的可信程度 |
转下页 |
续表 |
||
CreateNewDocument |
创建一个新的文档 |
|
CreateNewFrame |
创建一个包含文档和视的框架窗口 |
|
InitialUpdateFrame |
初始化框架窗口,必要时使它可见 |
|
SaveAllModified |
保存所有和模板相关的而且修改了的文档 |
|
CloseAllDocuments |
关闭所有和模板相关的文档 |
|
OpenDocumentFile |
打开指定路径的文件 |
|
SetDefaultTitle |
设置文档窗口缺省显示的标题 |
|
CDocument |
CanCloseFrame |
在关闭显示该文档的边框窗口之前调用 |
DeleteContents |
用来清除文档的内容 |
|
OnChangedViewList |
在与文档关联的视图被移走或新加入时调用 |
|
OnCloseDocument |
用来关闭文档 |
|
OnNewDocument |
用来创建新文档 |
|
OnOpenDocument |
用来打开文档 |
|
OnSaveDocument |
以来保存文档 |
|
ReportSaveLoadException |
处理打开、保存文档操作失败时的例外 |
|
GetFile |
返回一个指向Cfile对象的指针 |
|
ReleaseFile |
释放一个文件以便其他应用程序可以使用 |
|
SaveModified |
用来询问用户文档是否需要保存 |
|
PreCloseFrame |
在框架窗口关闭之前调用 |
消息映射方法和标准命令消息
窗口对象可以响应以“WM_”为前缀的标准Windows消息,消息处理函数名称以“ON”为前缀。不同类型的Windows窗口处理的Windows消息是有所不同的,因此,不同类型的MFC窗口实现的消息处理函数也有所不同。例如,多文档边框窗口能处理WM_MDIACTIVATE消息,其他类型窗口就不能。程序员从一定的MFC窗口派生自己的窗口类,对感兴趣的消息,覆盖基类的消息处理函数,实现自己的消息处理函数。
所有的命令目标(CCmdTarger或导出类对象)可以响应命令消息,程序员可以指定应用程序对象、框架窗口对象、视对象或文档对象等来处理某条命令消息。一般地,尽量由与命令消息关系密切的对象来处理,例如隐藏/显示工具栏由框架窗口处理,打开文件由应用程序对象处理,数据变化的操作由文档对象处理。
对话框的控制子窗口可以响应各类通知消息。
对于命令消息,MFC实现了一系列标准命令消息处理函数。标准命令ID在afxres.h中定义。表5-5列出了MFC标准命令的实现,从ID或者函数名可以大致地看出该函数的目的、功用,具体的实现有的后续章节会讲解,详细参见MFC技术文档。
程序员可以自己来处理这些标准消息,也可以通过不同的类或从不同的类导出自己的类来处理这些消息,不过最好遵循MFC的缺省实现。比如处理ID_FILE_NEW命令,最好由CWinApp的派生类处理。
表5-5 标准命令消息处理函数
ID |
函数 |
实现函数的类 |
ID_FILE_NEW |
OnFileNew |
CWinApp |
ID_FILE_OPEN |
OnFileOpen |
CWinApp |
ID_FILE_CLOSE |
OnFileClose |
CDocument |
ID_FILE_SAVE |
OnFileSave |
CDocument |
ID_FILE_SAVE_AS |
OnFileSaveAs |
CDocument |
ID_FILE_SAVE_COPY_AS |
OnFileSaveCopyAs |
COleServerDoc |
ID_FILE_UPDATE |
OnUpdateDocument |
COleServerDoc |
ID_FILE_PAGE_SETUP |
OnFilePrintSetup |
CWinApp |
转下页 |
续表 |
||
ID_FILE_PRINT |
OnFilePrint |
CView |
ID_FILE_PRINT_PREVIEW |
OnFilePrintPreview |
CView |
ID_FILE_MRU_FILE1...FILE16 |
OnUpdateRecentFileMenu |
CWinApp |
ID_EDIT_CLEAR |
CView没有实现, |
|
ID_EDIT_CLEAR_ALL |
但是,如果有实现 |
|
ID_EDIT_COPY |
函数,就是派生类 |
|
ID_EDIT_CUT |
CEditView的 |
|
ID_EDIT_FIND |
实现函数 |
|
ID_EDIT_PASTE_LINK |
||
ID_EDIT_PASTE_SPECIAL |
||
ID_EDIT_REPEAT |
||
ID_EDIT_REPLACE |
||
ID_EDIT_SELET_ALL |
||
ID_EDIT_UNDO |
||
ID_WINDOW_NEW |
OnWindowNew |
CMDIFrameWnd |
ID_WINDOW_ARRANGE |
OnMDIWindowCmd |
CMDIFrameWnd |
ID_WINDOW_CASCADE |
||
ID_WINDOW_TILE_HORZ |
||
ID_WINDOW_TILE_VERT |
||
ID_WINDOW_SPLIT |
CSplitterWnd |
|
ID_APP_ABOUT |
||
ID_APP_EXIT |
OnAppExit |
CWinApp |
ID_HELP_INDEX |
OnHelpIndex |
CWinApp |
ID_HELP_USING |
OnHelpUsing |
CWinApp |
ID_CONTEXT_HELP |
OnContextHelp |
CWinApp |
转下页 |
续表 |
||
ID_HELP |
OnHelp |
CWinApp |
ID_DEFAULT_HELP |
OnHelpIndex |
CWinApp |
ID_NEXT_PANE |
OnNextPaneCmd |
CSplitterWnd |
ID_PREV_PANE |
OnNextPaneCmd |
CSplitterWnd |
ID_OLE_INSERT_NEW |
||
ID_OLE_EDIT_LINKS |
||
ID_OLE_VERB_FIRST...LAST |
||
ID_VIEW_TOOLBAR |
CFrameWnd |
|
ID_VIEW_STATUS_BAR |
CFrameWnd |
|
ID_INDICATOR_CAPS ID_INDICATOR_NUM ID_INDICATOR_SCRL ID_INDICATOR_KANA |
OnUpdateKeyIndicator |
CFrameWnd |
MFC对象的创建过程
应用程序使用MFC的接口是把一些自己的特殊处理填入MFC框架,这些处理或者在应用程序启动和初始化的时候被调用,或者在程序启动之后和用户交互的过程中被调用,或者在程序退出和作清理工作的时候被调用。这三个阶段中,和用户交互阶段是各个程序自己的事情,自然都不一样,但是程序的启动和退出两个阶段是MFC框架所实现的,是MFC框架的一部分,各个程序都遵循同样的步骤和规则。显然,清楚MFC框架对这两个阶段的处理是很有必要的,它可以帮助深入理解MFC框架,更好地使用MFC框架,更有效地实现应用程序特定的处理。
MFC程序启动和初始化过程就是创建MFC对象和Windows对象、建立各种对象之间的关系、把窗口显示在屏幕上的过程,退出过程就是关闭窗口、销毁所创建的Windows对象和MFC对象的过程。所以,下面要讨论几种常用MFC对象的结构,它们是构成一个文档-视模式应用程序的重要部件。
应用程序中典型对象的结构
本节将主要分析应用程序对象、文档对象、文档模板等的数据结构。通过考察类的结构,特别是成员变量结构,弄清它的功能、目的以及和其他类的关系;另外,在后续有关分析中必定会提到这些成员变量,这里先作个说明,到时也不会显得突兀。
下面几节以表格的形式来描述各个类的成员变量。表格中,第一列打钩的表示是MFC类库文档有说明的;没打钩的在文档中没有说明,如果是public,则可以直接访问,但随着MFC版本的变化,以后MFC可能不支持这些成员;第二列是访问属性;第三列是成员变量名称;第四列是成员变量的数据类型;第五列是对成员变量的功能、用途的简要描述。
应用程序类的成员变量
应用程序对象的数据成员表由两部分组成,第一部分是CWinThread的成员变量,如表5-6所示,CWinApp继承了CWinThread的数据成员。第二部分是CWinApp自己定义的成员变量,如表5-7所示。
表5-6 CwinThread的成员变量
访问限制 |
变量名称 |
类型 |
解释 |
|
√ |
public |
m_bAutoDelete |
BOOL |
指定线程结束时是否销毁线程对象本身 |
√ |
public |
m_hThread |
HANDLE |
当前线程的句柄 |
√ |
public |
m_nThreadID |
UINT |
当前线程的ID |
√ |
public |
m_pMainWnd |
CWnd* |
指向应用程序主窗口的指针 |
√ |
public |
m_pActiveWnd |
CWnd* |
当OLE SERVER就地激活时指向客户程序主窗口的指针 |
public |
m_msgCur |
MSG |
当前消息(MSG结构) |
|
public |
m_pThreadParams |
LPVOID |
传递给线程开始函数的参数 |
|
public |
m_pfnThreadProc |
函数指针1 |
线程开始函数,AFX_THREADPROC类型 |
|
public |
m_lpfnOleTermOrFreeLib |
函数指针2 |
OLE用途,void (AFXAPI * fn)(BOOL,BOOL) |
|
public |
m_pMessageFilter |
指针 |
OLE消息过滤,指向COleMessageFilter对象 |
|
protected |
m_ptCursorLast |
CPoint |
最新鼠标位置 |
|
protected |
m_nMsgLast |
UINT |
消息队列中最新接收到的消息 |
表5-7 CWinApp的成员变量
访问限制 |
变量名称 |
类型 |
解释 |
|
√ |
public |
m_pszAppName |
LPCTSTR |
应用程序名称 |
√ |
public |
m_hInstance |
HINSTANCE |
标志应用程序当前实例句柄 |
√ |
public |
m_hPrevInstance |
HINSTANCE |
32位程序设为空 |
√ |
public |
m_lpCmdLine |
LPTSTR |
指向应用程序的命令行字符串 |
√ |
public |
m_nCmdShow |
int |
指定窗口开始的显示方式 |
√ |
public |
m_bHelpMode |
BOOL |
标识用户是否在上下文帮助模式 |
√ |
public |
m_pszExeName |
LPCTSTR |
应用程序的模块名 |
√ |
public |
m_pszHelpFilePath |
LPCTSTR |
应用程序的帮助文件名,缺省时同模块名 |
√ |
public |
m_pszProfileName |
LPCTSTR |
应用程序的INI文件名,缺省时同应用程序名 |
√ |
public |
m_pszRegistryKey |
LPCTSTR |
Register入口,如果不指定,使用INI文件。 |
public |
m_pDocManager; |
CDocManager * |
指向一个文档模板管理器 |
|
protected |
m_hDevMode |
HGLOBAL |
打印设备模式 |
|
protected |
m_hDevNames |
HGLOBAL |
打印设备名称 |
|
protected |
m_dwPromptContext |
DWORD |
被MESSAGE BOX覆盖的帮助上下文 |
|
protected |
m_nWaitCursorCount |
int |
等待光标计数 |
|
protected |
m_hcurWaitCursorRestore |
HCURSOR |
保存的光标,在等待光标之后恢复 |
|
protected |
m_pRecentFileList |
指针 |
指向CRecentFileList对象,最近打开的文件列表 |
|
public |
m_atomApp |
ATOM |
DDE用途 |
|
public |
m_atomSystemTopic |
m_atomApp |
DDE用途 |
|
public |
m_nNumPreviewPages |
UINT |
缺省被打印的页面 |
|
public |
m_nSafetyPoolSize |
size_t |
理想尺寸 |
|
public |
m_lpfnDaoTerm |
函数指针 |
DAO初始化设置时使用 |
CDocument的成员变量
表5-8 文档对象的属性。
访问限制 |
变量名称 |
类型 |
解释 |
|
protected |
m_strTitle |
CString |
文档标题 |
|
protected |
m_strPathName |
CString |
文档路径 |
|
protected |
m_pDocTemplate |
CDocTemplate* |
指向文档模板的指针 |
|
protected |
m_viewList |
CPtrList |
关联的视窗口列表 |
|
protected |
m_bModified |
BOOL |
文档是否有变化、需要存盘 |
|
public |
m_bAutoDelete |
BOOL |
关联视都关闭时是否删除文档对象 |
|
public |
m_bEmbedded |
BOOL |
文档是否由OLE创建 |
文档模板的属性
表5-9列出了文档模板的成员变量,5-10列出了单文档模板的成员变量,5-11列出了多文档模板的成员变量。单、多文档模板继承了文档模板的成员变量。
表5-9 文档模板的数据成员
访问限制 |
变量名称 |
类型 |
解释 |
|
public |
m_bAutoDelete |
BOOL |
||
public |
m_pAttachedFactory |
CObject * |
||
public |
m_hMenuInPlace |
HMENU |
就地激活时,OLE客户程序的菜单 |
|
public |
m_hAccelInPlace |
HACCEL |
就地激活时,OLE客户程序的快捷键 |
|
public |
m_hMenuEmbedding |
HMENU |
||
public |
m_hAccelEmbedding |
HACCEL |
||
public |
m_hMenuInPlaceServer |
HMENU |
||
public |
m_hAccelInPlaceServer |
HACCEL |
||
protected |
m_nIDResource |
UINT |
框架、菜单、快捷键等的资源ID |
|
protected |
m_nIDServerResource |
UINT |
||
public |
m_nIDEmbeddingResource |
UINT |
||
public |
m_nIDContainerResource |
UINT |
||
public |
m_pDocClass |
CRuntimeClass* |
指向文档类的动态创建信息 |
|
public |
m_pFrameClass |
CRuntimeClass* |
指向框架类的动态创建信息 |
|
public |
m_pViewClass |
CRuntimeClass* |
指向视类的动态创建信息,由字符串m_nIDResource描述 |
|
public |
m_pOleFrameClass |
CRuntimeClass* |
指向OLD框架类的动态创建信息 |
|
public |
m_pOleViewClass |
CRuntimeClass* |
||
public |
m_strDocStrings |
CString |
描述该文档类型的字符串 |
表5-10 单文档模板的成员变量
访问限制 |
变量名称 |
类型 |
解释 |
|
protected |
m_pOnlyDoc |
CDocment* |
指向唯一的文档对象 |
表5-11 单文档模板的成员变量
访问限制 |
变量名称 |
类型 |
解释 |
|
public |
m_hMenuShared |
HMENU |
该模板的MDI子窗口的菜单 |
|
public |
m_hAccelTable |
HACCEL |
该模板的MDI子窗口的快捷键 |
|
protected |
m_docList |
CPtrList |
该模板的文档列表 |
|
protected |
m_nUntitledCount |
UINT |
用来生成文件名的数字,如”untitled0”的0。 |
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。其中,字符串资源描述了文档类型,由七个被“ ”分隔的子字符串组成,各个子串可以通过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窗口 |