第7章 简单而完整:MFC骨干程序

优质
小牛编辑
135浏览
2023-12-01

当技术愈来愈复杂,

入门愈来愈困难,

我们的困惑愈来愈深,

犹豫愈来愈多。

上一章的Hello范例,对于MFC 程序设计导入很适合。但它只发挥了MFC的一小部份特性,只用了三个MFC类(CWinApp、CFrameWnd 和CDialog)。这一章我们要看一个完整的MFC应用程序骨干(注),其中包括丰富的UI对象(如工具栏、状态列)的生成,以及很重要的Document/View 架构观念。

注:我所谓的MFC应用程序骨干,指的是由AppWizard 产生出来的MFC程序,也就是像第4章所产生的 Scribble step0 那样的程序。

不二法门:熟记 MFC 类阶层架构

我还是要重复这一句话:MFC 程序设计的第一要务是熟记各类的阶层架构,并清楚了解其中几个一定会用到的类。一个MFC骨干程序(不含 ODBC 或 OLE 支持)运用到的类如图 7-1 所示,请与图6-1做个比较。

图7-1 本章范例程序所使用的MFC类。请与图6-1做比较。

MFC 程序的 UI 新风貌

一套好软件少不得一幅漂亮的用户接口。图7-2是信手拈来的几个知名 Windows 软件,它们一致具备了工具栏和状态栏等视觉对象,并拥有MDI风格。利用MFC,我们很轻易就能够做出同等级的UI接口。

撰写MFC程序,我们一定要放弃传统的「纯手工打造」方式,改用Visual C++ 提供的各种开发工具。AppWizard 可以为我们制作出 MFC 程序骨干;只要选择某些按钮,不费吹灰之力你就可以获得一个很漂亮的程序。这个全自动生产线做出来的程序虽不具备任何特殊功能(那正是我们程序员的任务),但已经拥有以下的特征:

标准的【File】菜单,以及对话框。

标准的【Edit】菜单(剪贴板功能)。这份菜单是否一开始就有功效,必须视你选用哪一种View而定,例如CEditView就内建有剪贴板功能。

标准MDI程序应该具备的【Window】菜单

【Help】菜单和 About 对话框亦已备妥。

此外,标准的工具栏和状态栏也已备妥,并与菜单内容建立起映射关系。所谓工具栏,是将某几个常用的菜单项目以按钮型式呈现出来,有一点热键的味道。这个工具栏可以随处停驻(dockable)。所谓状态栏,是主窗口最下方的文字显示区;只要菜单拉下,状态列就会显示鼠标座落的菜单项目的说明文字。状态栏右侧有三个小窗口(可扩充个数),用来显示一些特殊按键的状态。

打印与预览功能也已是半成品。【File】菜单拉下来可以看到【Print...】和【Print Preview】两项目:

骨干程序的Document和View目前都还是白纸一张,需要我们加工,所以一开始看不出打印与预览的真正功能。但如果我们在 AppWizard 中选用的View 类是 CEditView(如同第4章),用户就可以打印其编辑成果,并可以在打印之前预览。也就是说,一进程序代码都不必写,我们就获得了一个可以同时编辑多份文件的文字编辑软件。

Document/View 支撑你的应用程序

我已经多次强调,Document/View 是 MFC 进化为 Application Framework 的灵魂。这个特征表现于程序设计技术上远多于表现在用户接口上,因此用户可能感觉不到什么是Document/View。程序员呢?程序员将因陌生而有一段阵痛期,然后开始享受它带来的便利。

我们在OLE中看到各对象(注)的集合称为一份Document;在MDI中看到子窗口所掌握的数据称为一个Document;现在在 MFC 又看到Document。"Document" 如今处处可见,再过不多久八成也要和"Object" 一样地泛滥了。

OLE对象指的是PaintBrush 完成的一张bitmap、SoundRecorder完成的一段Wave声音、Excel 完成的一份电子表格、Word 完成的一份文字等等等。为了恐怕与 C++ 的「对象」混淆,有些书籍将 OLE object 称为 OLE item。

在MFC之中,你可以把Document简单想作是「数据」。是的,只是数据,那么MFC的CDocument简单地说就是负责处理数据的类。

问题是,一个预先写好的类怎么可能管理未知的数据呢?MFC 设计之际那些伟大的天才们并不知道我们的数据结构,不是吗?! 他怎么知道我的程序要处理的数据是简单如:

char  name[20];
char  address[30];
int   age;
bool  sex;

或是复杂如:

的确,预先处理未知的数据根本是不可能的。CDocument 只是把空壳做好,等君入瓮。它可以内嵌其它对象(用来处理基层数据类型如串列、数组等等),所以程序员可以在Document 中拼拼凑凑出实际想要表达的文件完整格式。下一章进入 Scribble 程序的实际设计时,你就能够感受这一点。

CDocument 的另一价值在于它搭配了另一个重要的类:CView。

不论什么型式,数据总是有体有面。实际的数据数值就是体,显示在屏幕上(甚而印表机上)的画面就是面(图 7-3a)。「数值的处理」应该使用字节、整数、浮点数、串列、数组等数据结构,而「数值的表现」应该使用绘图工具如坐标系统、笔刷颜色、点线圆弧、字形...。CView就是为了数据的表现而设计的。

图7-3a Document是数据的体,View是数据的面。

除了负责显示,View 还负责程序与用户之间的交谈接口。用户对数据的编辑、修改都需仰赖窗口上的鼠标与键盘动作才得完成,这些消息都将由View接受后再通知Document(图7-3b)。

图7-3b View 是Document的第一线,负责与用户接触。

Document/View 的价值在于,这些 MFC 类已经把一个应用程序所需的「数据处理与显示」的函数空壳都设计好了,这些函数都是虚函数,所以你可以(也应该)在派生类中改写它们。有关文件读写的动作在 CDocument 的 Serialize 函数进行,有关画面显示的动作在 CView 的 OnDraw 或 OnPaint 函数进行。当我为自己派生两个类 CMyDoc和 CMyView,我只要把全付心思花在 CMyDoc::Serialize 和 CMyView::OnDraw 身上,其它琐事一概不必管,整个程序自动会运作得好好的。

什么叫做「整个程序会自动运作良好」?以下是三个例子:

  • 如果按下【File/Open】,Application Framework 会启动对话框让你指定文件名,然后自动调用CMyDoc::Serialize 读文件 。Application Framework还会调用CMyView::OnDraw,把数据显示出来。

  • 如果萤幕状态改变,产生了WM_PAINT,Framework会自动调用你的CMyView::OnDraw,传一个Display DC 让你重新绘制窗口内容。

  • 如果按下【File/Print...】,Framework会自动调用你的CMyView::OnDraw,这次传进去的是个 Printer DC,因此绘图动作的输出对象就成了打印机。

MFC 已经把程序大架构完成了,模块与模块间的消息流动路径以及各函数的功能职司都已确定好(这是MFC之所以够格称为一个Framework 的原因),所以我们写程序的焦点就放在那些必须改写的虚函数身上即可。软件界当初发展 GUI 系统时,目的也是希望把程序员的心力导引到应用软件的真正目标去,而不必花在用户接口上。MFC 的Document/View 架构希望更把程序员的心力导引到真正的数据结构设计以及真正的数据显示动作上,而不要花在模块的沟通或消息的流动传递上。今天,程序员都对GUI称便,Document/View 也即将广泛地证明它的贡献。

Application Framework 使我们的程序写作犹如做填充题;Visual C++的软件开发工具则使我们的程序写作犹如做选择题。我们先做选择题,再在骨干程序中做填充题。的确,程序员的生活愈来愈像侯捷所言「只是软件IC装配厂里的男工女工」了。

现在让我们展开 MFC 深度之旅,彻底把 MFC 骨干程序的每一行都搞清楚。你应该已经从上一章具体了解了 MFC 程序从启动到结束的生命过程,这一章的例子虽然比较复杂,程序的生命过程是一样的。我们看看新添了什么内容,以及它们如何运作。我将以AppWizard 完成的 Scribble Step0(第4章)为解说对象,一行不改。然后我会做一点点修改,使它成为一个多窗口文字编辑器。

利用 Visual C++ 工具完成 Scribble step0

我已经在第4章示范过 AppWizard 的使用方法,并实际制作出 Scribble Step0 程序,这里就不再重复说明了。完整的骨干程序原始代码亦已列于第4章。

这些由「生产线」做出来的程序代码其实对初学者并不十分合适,原因之一是容易眼花撩乱,有许多 #if...#endif、批注、奇奇怪怪的符号(例如 //{ 和 //});原因之二是每一个类有自己的 .H 文件和 .CPP 文件,整个程序因而幅员辽阔(六个 .CPP 文件和六个 .H 文件)。

图7-4是Scribble step0程序中各类的相关数据。

图7-4 Scribble骨干程序中的重要组成份子

骨干程序使用哪些 MFC 类?

对,你看到的Scribble step0就是一个完整的MFC应用程序,而我保证你一定昏头转向茫无头绪。没有关系,我们才刚启航。

如果把标准图形接口(工具栏和状态栏)以及 Document/View 考虑在内,一个标准的MFC MDI程序使用这些类:

应用程序各显身手的地方只是各个可被改写的虚函数。这九个类在MFC的地位请看图7-1。下一节开始我会逐项解释每一个对象的产生时机及其重要性质。

Document/View不只应用在MDI程序,也应用在SDI程序上。你可以在 AppWizard 的「Options 对话框」(图 4-2b)选择SDI风格。本书以MDI程序为讨论对象。

为了对标准的MFC程序有一个大局观,图7-4显示Scribble step0中各重要组成份子(类),这些组成份子在运行时期的意义与主从关系显示于图 7-5。

图7-5 Scribble step0程序中的九个对象(几乎每个MFC MDI程序都如此)

图7-6是 Scribble step0程序缩影,我把运行时序标上去,对于整体概念的形成将有帮助。

图7-6 Scribble step0 运行时序。这是一张简图,

有一些次要动作(例如滑鼠拉曳功能、设定对话框底色)并未列出,

但是在稍后的细部讨论中会提到。

以下是图 7-6 程序流程之说明:

➀~➃动作与流程和前一章的Hello程序如出一辙。

➄我们改写InitInstance这个虚函数。

➅ new 一个CMultiDocTemplate对象,此对象规划Document、View 以及 Document Frame 窗口三者之关系。

➆new一个CMyMDIFrameWnd 对象,做为主窗口对象。

➇调用LoadFrame,产生主窗口并加挂菜单等诸元,并指定窗口标题、文件标题、文件文件扩展名等(关键在 IDR_MAINFRAME 常数)。LoadFrame 内部将调用 Create,后者将调用CreateWindowEx,于是触发WM_CREATE 消息。

➈由于我们曾于CMainFrame之中拦截WM_CREATE(利用 ON_WM_CREATE 宏),所以WM_CREATE 产生之际Framework会调用OnCreate。我们在此为主窗口挂上工具列和状态栏。

➉回到InitInstance,执行ShowWindow 显示窗口。

a.InitInstance 结束,回到AfxWinMain,执行Run,进入消息循环。其间的黑盒子已在上一章的 Hello 范例中挖掘过。

b.消息经由Message Routing 机制,在各类的Message Map中寻求其处理例程。WM_COMMAND/ID_FILE_OPEN 消息将由 CWinApp::OnFileOpen 函数处理。此函数由MFC 提供,它在显示过【File Open】对话框后调用 Serialize 函数。

c.我们改写Serialize函数以进行我们自己的文件读写动作。

d.WM_COMMAND/ID_APP_ABOUT 消息将由 OnAppAbout 函数处理。

e.OnAppAbout 函数利用 CDialog 的性质很方便地产生一个对话框。

Document Template 的意义

Document Template 是一个全新的观念。

稍早我已提过 Document/View 的概念,它们互为表里。View 本身虽然已经是一个窗口,其外围却必须再包装一个外框窗口做为舞台。这样的切割其实是为了让 View 可以非常独立地放置于「MDI Document Frame 窗口」或「SDI Document Frame 窗口」或「OLE Document Frame 窗口」等各种应用之中。也可以说,Document Frame 窗口是 View 窗口的一个容器。数据的内容、数据的表象、以及「容纳数据表象之外框窗口」三者是一体的,换言之,程序每打开一份文件(数据),就应该产生三份对象:

1. 一份Document 对象,

2. 一份View 对象,

3. 一份CMDIChildWnd 对象(做为外框窗口)

这三份对象由一个所谓的Document Template对象来管理。让这三份对象产生关系的关键在于CMultiDocTemplate:

如果程序支持不同的数据格式(例如一为TEXT一为BITMAP),那么就需要不同的Document Template:

这其中有许多值得讨论的地方,而 CMultiDocTemplate 的构造函数参数透露了一些端倪:

1.nIDResource:这是一个资源ID,表示此一文件类型(文件格式)所使用的资源。本例为IDR_SCRIBTYPE,在RC文件中代表多种资源(不同种类的资源可使用相同的ID):

其中的ICON是文件窗口被最小化之后的图示;MENU是当程序存在有任何文件窗口时所使用的菜单(如果没有开启任何文件窗口,菜单将是另外一套,稍后再述)。至于字符串表格(STRINGTABLE)中的字符串,稍后我有更进一步的说明。

2.pDocClass。 这 是 一 个 指 标 , 指 向Document 类 别( 衍 生 自 CDocument)之「CRuntimeClass 对象」。

3.pFrameClass。这是一个指针,指向Child Frame类(派生自 CMDIChildWnd)之「CRuntimeClass 对象」。

4.pViewClass。这是一个指针,指向View 类(派生自CView)之「CRuntimeClass对象」。

CRuntimeClass

我曾经在第3章「自制 RTTI」一节解释过什么是 CRuntimeClass。它就是「类型录网」串列中的元素类型。任何一个类只要在声明时使用 DECLARE_DYNAMIC或DECLARE_DYNCREATE或DECLARE_SERIAL宏,就会拥有一个静态的(static)CRuntimeClass 内嵌对象。

好,你看,Document Template 接受了三种类的CRuntimeClass 指针,于是每当用户打开一份文件,Document Template 就能够根据「类型录网」(第3章所述),动态生成出三个对象(document、view、document frame window)。如果你不记得 MFC 的动态生成是怎么一回事儿,现在正是复习第3章的时候。我将在第8章带你实际看看Document Template 的内部动作。

前面曾提到,我们在CMultiDocTemplate构造函数的第一个参数置入 IDR_SCRIBTYPE,代表RC文件中的菜单(MENU)、图标(ICON)、字符串(STRING)三种资源,其中又以字符串资源大有学问。这个字符串以 '\n' 分隔为七个子字符串,用以完整描述文件类型。七个子字符串可以在 AppWizard 的步骤四的【Advanced Options】对话框中指定:

每一个子字符串都可以在程序进行过程中取得,只要调用 CDocTemplate::GetDocString 并在其第二参数中指定索引值(1~7)即可,但最好是以 CDocTemplate 所定义的七个常数代替没有字面意义的索引值。下面就是 CDocTemplate 的7个常数定义:

所以,你可以这么做:

CString strDefExt, strDocName;
pDocTemplate->GetDocString(strDefExt, CDocTemplate::filterExt);
pDocTemplate->GetDocString(strDocName, CDocTemplate::docName);

七个子字符串意义如下:

Index意义
1.CDocTemplate::windowTitle主窗口标题栏上的字符串。SDI程序才需要指定它,MDI程序不需要指定,将以IDR_MAINFRAME字符串为默认值。
2. CDocTemplate::docName文件基底名称(本例为"Scrib")。这个名称再加上一个流水号代码,即成为新文件的名称(例如"Scrib1")。如果此字符串未被指定,文件预设名称为 "Untitled"。
3. CDocTemplate::fileNewName文件类型名称。如果一个程序支持多种文件,此字符串将显示在【File/New】对话框中。如果没有指明,就不能够在【File/New】对话框中处理此种文件。本例只支持一种文件类型,所以当你选按【File/New】,并不会出现对话框。第 13 章将示范「一个程序支持多种文件」的作法。
4. CDocTemplate::filterName文件类型以及一个适用于此类型之万用过滤字符串(wildcard filter string)。本例为 "Scribble(*.scb)"。这个字符串将出现在【File Open】对话框中的【List Files Of Type】列示盒中。
5. CDocTemplate::filterExt文件文件之扩展名,例如 "scb"。如果没有指明,就不能够在【File Open】对话框中处理此种文件文件。
6. CDocTemplate::regFileTypeId如果你以 ::RegisterShellFileTypes 对系统的登录数据库(Registry)注册文件类型,此值会出现在HKEY_CLASSES_ROOT 之下成为其子机代码(subkey)并仅供 Windows 内部使用。如果未指定,此种文件类型就无法注册,鼠标拖放 (drag and drop) 功能就会受影响。
7. CDocTemplate::regFileTypeName这也是储存在登录数据库(Registry)中的文件类型名称,并且是给人(而非只给系统)看的。它也会显示于程序中用以处理登录数据库之对话框内。

以下是Scribble范例中各个字符串出现的位置:

我必须再强调一次,AppWizard 早已帮我们产生出这些字符串。把这些来龙去脉弄清楚,只是为了知其所以然。当然,同时也为了万一你不喜欢AppWizard 准备的字符串内容,你知道如何去改变它。

Scribble 的 Document/View 设计

用最简单的一句话描述,Document 就是数据的体,View 就是数据的面。我们藉CDocument 管理数据,藉Collections Classes(MFC中的一组专门用来处理数据的类)处理实际的数据数据;我们藉CView 负责数据的显示,藉CDC和 CGdiObject 实际绘图。人们常说一体两面一体两面,在MFC中一体可以多面:同一份数据可以文字描述之,可以长条图描述之,亦可以曲线图描述之。

Document/View 之间的关系可以图7-3 说明。View 就像一个观景器(我避免使用「窗口」这个字眼,以免引起不必要的联想),用户透过 View 看到 Document,也透过 View 改变 Document。View 是 Document 的外显接口,但它并不能完全独立,它必须依存在一个所谓的 Document Frame 窗口内。

一份Document可以映射给许多个Views 显示,不同的Views可以对映到同一份巨大Document的不同局部。总之,请把View想象是一个镜头,可以观看大画布上的任何局部(我们可以选用 CScrollView 使之具备滚动条);在镜头上加特殊的偏光镜、柔光镜、十字镜,我们就会看到不同的影像 -- 虽然观察的对象完全相同。

数据的管理动作有哪些?读文件和写文件都是必要的,文件存取动作称为 Serialization,由Serialize 函数负责。我们可以(而且也应该)在 CMyDoc 中改写Serialize 函数,使它符合个人需求。数据格式的建立以及文件读写功能将在Scribble step1 中加入,本例(step0)的CScribbleDoc 中并没有什么成员变量(也就是说容纳不了什么数据),Serialize 则简直是个空函数:

void CScribbleDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // TODO: add storing code here
    }
    else
    {
        // TODO: add loading code here
    }
}

这也是我老说骨干程序啥大事也没做的原因。

除了文件读写,数据的显示也是必要的动作,数据的接受编辑也是必要的动作。两者都由View负责。用户对Document 的任何编辑动作都必须透过 Document Frame窗口,消息随后传到 CView。我们来想想我们的 View 应该改写哪些函数?

1. 当Document Frame窗口收到WM_PAINT,窗口内的View的OnPaint函数会被调用,OnPaint又调用OnDraw。所以为了显示数据,我们必须改写OnDraw。至于OnPaint,主要是做「只输出到屏幕而不到打印机」的动作。有关打印机,我将在第 12 章提到。

2. 为了接受编辑动作,我们必须在View 类中接受鼠标或键盘消息并处理之。如果要接受鼠标左键,我们应该改写View 类中的三个虚函数:

afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);

上述两个动作在 Scribble step0 都看不到,因为它是个啥也没做的程序:

void CScribbleView::OnDraw(CDC* pDC)
{
    CScribbleDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
    // TODO: add draw code for native data here
}

主窗口的诞生

上一章那个极为简单的 Hello 程序,主窗口采用CFrameWnd 类。本例是 MDI 风格,将采用 CMDIFrameWnd 类。

构造 MDI 主窗口,有两个步骤。第一个步骤是 new 一个 CMDIFrameWnd 对象,第二个步骤是调用其 LoadFrame 函数。此函数内容如下:

窗口产生之际会发出WM_CREATE 消息,因此CMainFrame::OnCreate会被执行起来,那里将进行工具栏和状态栏的建立工作(稍后描述)。LoadFrame函数的参数(本例为IDR_MAINFRAME )用来设定窗口所使用的各种资源,你可以从前一页的CFrameWnd::LoadFrame 原始代码中清楚看出。这些同名的资源包括:

这种作法(使用 LoadFrame 函数)与第6章的作法(使用Create函数)不相同,请注意。

工具栏和状态栏的诞生(Toolbar & Status bar)

工具栏和状态栏分别由 CToolBar 和 CStatusBar 掌管。两个对象隶属于主窗口,所以我们在 CMainFrame 中以两个变量(事实上是两个对象)表示之:

主窗口产生之际立刻会发出 WM_CREATE,我们应该利用这时机把工具栏和状态栏建立起来。为了拦截 WM_CREATE,首先需在 Message Map 中设定「映射项目」:

BEGIN_MESSAGE_MAP(CMyMDIFrameWnd, CMDIFrameWnd)
ON_WM_CREATE()
END_MESSAGE_MAP()

ON_WM_CREATE这个宏表示,只要WM_CREATE 发生,我的OnCreate函数就应该被调用。下面是由 AppWizard 产生的 OnCreate 标准动作:

其中有四个动作与工具栏和状态栏的产生及设定有关:

  • m_wndToolBar.Create(this) 表示要产生一个隶属于this(也就是目前这个对象,也就是主窗口)的工具栏。

  • m_wndToolBar.LoadToolBar(IDR_MAINFRAME) 将RC文件中 的工具列资源 载入。IDR_MAINFRAME 在RC文件中代表两种与工具栏有关的资源:

LoadToolBar函数一举取代了前一版的LoadBitmap+SetButtons 两个动 作 。LoadToolBar 知道如何把 BITMAP 资源和 TOOLBAR 资源搭配起来,完成工具栏的设定。当然啦,如果你不是使用 VC++资源工具来编辑工具栏,BITMAP 资源和TOOLBAR 资源就可能格数不符,那是不被允许的。TOOLBAR 资源中的各 ID 值就是菜单项目的子集合,因为所谓工具栏就是把比较常用的菜单项目集合起来以按钮方式提供给用户。

  • m_wndStatusBar.Create(this) 表示要产生一个隶属于this对象(也就是目前这个对象,也就是主窗口)的状态栏。

  • m_wndStatusBar.SetIndicators(,...) 的第一个参数是个数组;第二个参数是数组元素个数。所谓Indicator 是状态栏最右侧的「指示窗口」,用来表示大写键、数字键等的On/Off 状态。AFXRES.H 中定义有七种indicators:

本例使用其中三种:

鼠标拖放(Drag and Drop)

MFC 程序很容易拥有Drag and Drop功能。意思是,你可以从Shell(例如 Windows 95的文件总管)中以鼠标拉动一个文件,拖到你的程序中,你的程序因而打开此文件并读其内容,将内容放到一个Document Frame窗口中。甚至,用户在 Shell 中以鼠标对某个文件文件(你的应用程序的文件文件)快按两下,也能启动你这个程序,并自动完成开文件,读文件,显示等动作。

在SDK 程序中要做到Drag and Drop,并不算太难,这里简单提一下它的原理以及作法。当用户从 Shell 中拖放一个文件到程序A,Shell 就配置一块全局内存,填入被拖曳的文件名称(包含路径),然后发出WM_DROPFILES传到程序A的消息队列。程序A取得此消息后,应该把内存的内容取出,再想办法开文件读文件。

并不是张三和李四都可以收到WM_DROPFILES,只有具备 WS_EX_ACCEPTFILES 风格的窗口才能收到此一消息。欲让窗口具备此一风格,必须使用 CreateWindowEx (而不是传统的 CreateWindow),并指定第一个参数为 WS_EX_ACCEPTFILES。

剩下的事情就简单了:想办法把内存中的文件名和其它信息取出(内存 handle 放在WM_DROPFILES 消息的wParam 中)。这件事情有DragQueryFile 和 DragQueryPoint两个API 函数可以帮助我们完成。

SDK 的方法真的不难,但是MFC程序更简单:

这三个函数的用途如下:

  • CWnd::DragAcceptFile(BOOL bAccept=TRUE); 参数TRUE表示你的主窗口以及每一个子窗口(文件窗口)都愿意接受来自Shell的拖放文件。CFrameWnd内有一个OnDropFiles成员函数,负责对WM_DROPFIELS消息做出反应,它会通知application对象的OnOpenDocument(此函数将在第8章介绍),并夹带被拖放的文件的名称。

  • CWinApp::EnableShellOpen(); 当用户在Shell中对着本程序的文件文件快按两下时,本程序能够打开文件并读内容。如果当时本程序已执行, Framework不会再执行起程序的另一副本,而只是以DDE(Dynamic Data Exchange,动态数据交换)通知程序把文件(文件)读进来。DDE 处理例程内建在CDocManager之中(第8章会谈到这个类)。也由于DDE的能力,你才能够很方便地把文件图标拖放到打印机图标上,将文件打印出来。

    通常此函数后面跟随着RegisterShellFileTypes。

  • CWinApp::RegisterShellFileTypes(); 此函数将向Shell注册本程序的文件类型。有了这样的注册动作,用户在Shell的双击动作才有着力点。这个函数搜寻Document Template串列中的每一种文件类型,然后把它加到系统所维护的registry(登录数据库)中。

    在传统的Windows 程序中,对Registry 的注册动作不外乎两种作法,一是准备一个.reg 文件,由用户利用 Windows 提供的一个小工具regedit.exe,将.reg合并到系统的Registry中。第二种方法是利用::RegCreateKey、::RegSetValue等Win32 函数 ,直接编辑Registry。 MFC 程序的作法最简单,只要调用CWinApp::RegisterShellFileTypes 即可。

必须注意的是,如果某一种文件类型已经有其对应的应用程序(例如 .txt 对应Notepad,.bmp 对应PBrush,.ppt 对应PowerPoint,.xls 对应Excel),那么你的程序就不能够横刀夺爱。如果本例Scribble的文件文件扩展名为 .txt,用户在Shell中双击这种文件,启动的将是Notepad 而不是Scribble。

另一个要注意的是,拖放动作可以把任何类型的文件文件拉到你的窗口中,并不只限于你所注册的文件类型。你可以把.bmp文件从Shell拉到Scribble窗口,Scribble 程序一样会读它并为它准备一个窗口。想当然耳,那会是个无言的结局:

消息映射(Message Map)

每一个派生自 CCmdTarget 的类都可以有自己的Message Map以处理消息。首先你应该在类声明处加上DECLARE_MESSAGE_MAP 宏,然后在 .CPP文件中使用BEGIN_MESSAGE_MAP 和 END_MESSAGE_MAP 两个宏,宏中间夹带的就是「消息与函数对映关系」的一笔笔记录。

你可以从图 7-6 那个浓缩的Scribble原始代码中看到各类的Message Map。本例CScribbleApp类接受四个WM_COMMAND 消息:

除了ID_APP_ABOUT是由我们自己设计一个OnAppAbout 函数处理之,其它三个消息都交给CWinApp成员函数去处理,因为那些动作十分制式,没什么好改写的。到底有哪些制式动作呢?看下一节!

标准菜单 File / Edit / View / Window / Help

仔细观察你所能搜集到的各种 MDI 程序,你会发现它们几乎都有两组菜单。一组是当没有任何子窗口(文件窗口)存在时出现(本例代码是 IDR_MAINFRAME):

另一组则是当有任何子窗口(文件窗口)存在时出现(本例代码是 IDR_SCRIBTYPE):

前者多半只有【File】、【View】、【Help】等选项,后者就复杂了,程序所有的功能都在上面。本例的IDR_MAINFRAME和IDR_SCRIBTYPE 就代表RC文件中的两组菜单。当用户打开一份文件文件,程序应该把主窗口上的菜单换掉,这个动作在SDK程序中由程序员负责,在MFC程序中则由Framework代劳了。

拉下这些菜单仔细瞧瞧,你会发现 Framework 真的已经为我们做了不少琐事。凡是菜单项目会引起对话框的,像是 Open 对话框、Save As 对话框、Print 对话框、Print Setup 对话框、Find 对话框、Replace 对话框,都已经恭候差遣;Edit 菜单上的每一项功能都已经可以应用在由 CEditView 掌控的文字编辑器上;File 菜单最下方记录着最近使用过的(所谓 LRU)四个文件名称(个数可在 Appwizard 中更改),以方便再开启;View 选单允许你把工具栏和状态栏设为可见或隐藏;Window 菜单提供重新排列子窗口图标的能力,以及对子窗口的排列管理,包括卡片式(Cascade)或拼贴式(Tile)。

下表是预设之菜单命令项及其处理例程的摘要整理。最后一个字段「是否预有关联」如果是Yes,意指只要你的程序菜单中有此命令项,当它被选按,自然就会引发命令处理例程,应用程序不需要在任何类的 Message Map 中拦截此命令消息。但如果是No,表示你必须在应用程序中拦截此消息。

菜单内容           命令项 ID            预设的处理函数      预有关联

上表的最后一字段为No者有五笔,表示虽然那些命令项有预设的处理例程,但你必须在自己的Message Map中设定映射项目,它们才会起作用。噢,AppWizard 此时又表现出了它的善体人意,自动为我们做出了这些代码:

对话框

Scribble 可以启动许多对话框,前一节提了许多。唯一要程序员自己动手(我的意思是出现在我们的程序代码中)的只有 About 对话框。

为了拦截WM_COMMAND的ID_APP_ABOUT项目,首先我们必须设定其Message Map:

BEGIN_MESSAGE_MAP(CScribbleApp, CWinApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()

当消息送来,就由OnAppAbout 处理:

void CScribbleApp::OnAppAbout()

{
    CAboutDlg aboutDlg;
    aboutDlg.DoModal();
}

其中CAboutDlg是CDialog的派生类:

class CAboutDlg : public CDialog
{
    enum { IDD = IDD_ABOUTBOX }; //IDD_ABOUTBOX是RC文件中的对话框面板资源
    ...
    DECLARE_MESSAGE_MAP()
};

比之于SDK 程序中的对话框,这真是方便太多了。传统SDK程序要在RC文件中定义对话框面板(dialog template,也就是其外形),在C程序中设计对话框函数。现在只需从CDialog派生出一个类,然后产生该类之对象,并指定 RC 文件中的对话框面板资源,再调用对话框对象的DoModal成员函数即可。

第10章一整章将讨论所谓的对话框数据交换(DDX)与对话框数据确认(DDV)。

改用 CEditView

Scribble step0 除了把一个应用程序的空壳做好,不能再贡献些什么。如果我们在AppWizard 步骤六中把 CScribbleView 的基类从 CView 改为 CEditView,那可就有大妙用了:

CEditView 是一个已具备文字编辑能力的类,它所使用的窗口是Windows 的标准控制组件之一 Edit,其SerializeRaw 成员函数可以把Edit控制组件中的raw text(而非「对象」所持有的数据)写到文件中。当我们在AppWizard 步骤六选择了它,程序代码中所有的CView统统变成CEditView,而最重要的两个虚函数则变成:

就这样,我们不费吹灰之力获得了一个多窗口的文字编辑器,并拥有读写文件能力以及预览能力。