第6章 MFC程序设计导论

MFC 程序的生死因果

理想如果不向实际做点妥协,理想就会归于尘土。

中华民国还得十次革命才得建立,面向对象怎能把一切传统都抛开。

以传统的C/SDK撰写Windows 程序,最大的好处是可以清楚看见整个程序的来龙去脉和消息动向,然而这些重要的动线在MFC应用程序中却隐晦不明,因为它们被Application Framework 包起来了。这一章主要目的除了解释MFC应用程序的长像,也要从MFC原始代码中检验出一个Windows 程序原本该有的程序进入点(WinMain)、窗口类注册(RegisterClass)、窗口产生(CreateWindow)、消息循环(Message Loop)、窗口函数(Window Procedure)等等动作,抽丝剥茧彻底了解一个 MFC 程序的诞生与结束,以及生命过程。

为什么要安排这一章?了解MFC 内部构造是必要的吗?看电视需要知道映射管的原理吗?开汽车需要知道传动轴与变速箱的原理吗?学习MFC不就是要一举超越烦琐的Windows API?啊,厂商(不管是哪一家)广告给我们的印象就是,藉由可视化的工具我们可以一步登天,基本上这个论点正确,只是有个但书:你得学会操控Application Framework。

想象你拥有一部保时捷,风驰电挚风光得很,但是引擎盖打开来全傻了眼。如果你懂汽车内部运作原理,那么至少开车时「脚不要老是含着离合器,以免来令片磨损」这个道理背后的原理你就懂了,「踩煞车时绝不可以同时踩离合器,以免失去引擎煞车力」这个道理背后的原理你也懂了,甚至你的保时捷要保养维修时或也可以不假外力自己来。

不要把自己想象成这场游戏中的后座车主,事实上作为这本技术书籍的读者的你,应该是车厂师傅。

好,这个比喻不见得面面俱到,但起代码你知道了自己的身份。

题外话:我的朋友曾铭源(现在纽约工作)写信给我说:『最近项目的压力大,人员纷纷离职。接连一个多礼拜,天天有人上门面谈。人事部门不知从哪里找来这些阿哥,号称有三年的SDK/MFC经验,结果对起话来是鸡同鸭讲,WinMain 和 Windows Procedure都搞不清楚。问他什么是message handler?只会在 ClassWizard 上 click、click、click !!! 拜Wizard 之赐,人力市场上多出了好几倍的 VC/MFC 程序员,但这些「Wizard 通」我们可不敢要』。

以 raw Windows API 开发程序,学习的路径是单纯的,条理分明的,你一定先从程序进入点开始,然后产生窗口类,然后产生窗口,然后取得消息,然后分辨消息,然后决定如何处理消息。虽然动作繁琐,学习却容易。

我希望你了解,本书之所以在各个主题中不厌其烦地挖MFC内部动作,解释骨干程序的每一条指令,每一个环节,是为了让你踏实地接受MFC,进而有能力役使MFC。你以为这是一条远路?呵呵,似远实近!

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

MFC在1.0 版时期的诉求是「一组将SDK API包装得更好用的类库」,从 2.0 版开始更进一步诉求是一个「Application Framework」,拥有重要的 Document-View 架构;随后又在更新版本上增加了 OLE 架构、DAO 架构...。为了让你有一个最轻松的起点,我把第一个程序简化到最小程度,舍弃 Document-View架构,使你能够尽快掌握C++/MFC 程序的面貌。这个程序并不以AppWizard制作出来,也不以ClassWizard 管理维护,而是纯手工打造。毕竟 Wizards 做出来的程序代码有一大堆批注,某些批注对Wizards有特殊意义,不能随便删除,却可能会混淆初学者的视听焦点;而且 Wizards所产生的程序骨干已具备 Document-View 架构,又有许多奇奇怪怪的宏,初学者暂避为妙。我们目前最想知道的是一个最阳春的 MFC 程序以什么面貌呈现,以及它如何开始运作,如何结束生命。

以MFC开发程序,一开始很快速,因为开发工具会为你产生一个骨干程序,一般该有的各种界面一应俱全。但是 MFC的学习曲线十分陡峭,程序员从骨干程式出发一直到有能力修改程序代码以符合个人的需要,是一段不易攀登的峭壁。

如果我们了解 Windows 程序的基本运作原理,并了解 MFC 如何把这些基础动作整合起来,我们就能够使 MFC 学习曲线的陡峭程度缓和下来。因此能够迅速接受 MFC,进而使用MFC。呵,一条似远实近的道路!

SDK 程序设计的第一要务是了解最重要的数个API函数的意义和用法,像 是RegisterClass、CreateWindow、GetMessage、DispatchMessage,以及消息的获得与分配。

MFC 程序设计的第一要务则是熟记MFC的类阶层架构,并清楚知晓其中几个一定会用到的类。本书最后面有一张MFC 4.2架构图,迭床架屋,令人畏惧,我将挑出单单两个类,组合成一个 "Hello MFC" 程序。这两个类在 MFC 的地位如图 6-1 所示。

图6-1 本章范例程序所用到的MFC类

需要什么函数库?

开始写代码之前,我们得先了解程序代码以外的外围环境。第一个必须知道的是,MFC 程序需要什么函数库?SDK 程序链接时期所需的函数库已在第一章显示,MFC 程序一样需要它们:

Windows C Runtime 函数库(VC++ 5.0)

*这些函数库不再区分Large/Medium/Small内存模式,因为32位操作系统不再有记忆体模式之分。这些函数库的多线程版本,请参考本书#38页。

DLL Import函数库(VC++ 5.0)

此外,应用程序还需要链接一个所谓的MFC函数库,或称为AFX 函数库,它也就是MFC这个application framework 的本体。你可以静态链接之,也可以动态链接之,AppWizard给你选择权。本例使用动态链接方式,所以需要一个对应的MFC import 函数库:

MFC 函数库(AFX 函数库)(VC++ 5.0,MFC 4.2)

我们如何在链接器( link.exe)中设定选项,把这些函数库都链接起来?稍后在HELLO.MAK 中可以一窥全貌。

如果在Visual C++整合环境中工作,这些设定不劳你自己动手,整合环境会根据我们圈选的项目自动做出一个合适的 makefile。这些 makefile 的内容看起来非常诘屈聱牙,事实上我们也不必太在意它,因为那是整合环境的工作。这一章我不打算依赖任何开发工具,一切自己来,你会在稍后看到一个简洁清爽的makefile。

需要什么包含文件?

SDK 程序只要包含WINDOWS.H 就好,所有API的函数声明、消息定义、常数定义、宏定义、都在WINDOWS.H 文件中。除非程序另调用了操作系统提供的新模块(如CommDlg、ToolHelp、DDEML...),才需要再各别包含对应的.H文件。

WINDOWS.H 过去是一个巨大文件,大约在 5000 行上下。现在已拆分内容为数十个较小的.H 文件,再由WINDOWS.H 包含进来。也就是说它变成一个 "Master included file for Windows applications"。

MFC 程序不这么单纯,下面是它常常需要面对的另外一些.H 文件:

  • STDAFX.H——这个文件用来做为Precompiled header file(请看稍后的方块说明 ) ,其内只是含入其它的MFC表头文件。应用程序通常会准备自己的STDAFX.H,例如本章的Hello程序就在STDAFX.H 中包含AFXWIN.H。

  • AFXWIN.H——每一个Windows MFC 程序都必须包含它,因为它以及它所包含的文件声明了所有的MFC 类。此文件内含AFX.H,后者又包含AFXVER_.H,后者又包含 AFXV_W32.H,后者又包含 WINDOWS.H(啊呼,终于现身)。

  • AFXEXT.H——凡使用工具栏、状态栏之程序必须包含这个文件。

  • AFXDLGS.H——凡使用通用型对话框(Common Dialog)之MFC程序需包含此文件,其内部包含COMMDLG.H。

  • AFXCMN.H——凡使用Windows 95新增之通用型控制组件(Common Control) 之MFC 程序需包含此文件。

  • AFXCOLL.H——凡使用Collections Classes(用以处理数据结构如数组、串列)之程序必须包含此文件。

  • AFXDLLX.H——凡MFC extension DLLs均需包含此文件。

  • AFXRES.H——MFC 程序的RC文件必须包含此文件。MFC 对于标准资源(例如File、Edit 等菜单项目)的ID 都有默认值,定义于此文件中,例如:

// File commands
#define ID_FILE_NEW 0xE100
#define ID_FILE_OPEN 0xE101
#define ID_FILE_CLOSE 0xE102
#define ID_FILE_SAVE 0xE103
#define ID_FILE_SAVE_AS 0xE104
...
// Edit commands
#define ID_EDIT_COPY 0xE122
#define ID_EDIT_CUT 0xE123
...

这些菜单项目都有预设的说明文字(将出现在状态栏中),但说明文字并不会事先定义于此文件,AppWizard为我们制作骨干程序时才把说明文字加到应用程序的RC文件中。第4章的骨干程序Scribble step0的RC文件中就有这样的字符串表格:

所有MFC包含文件均置于\MSVC\MFC\INCLUDE 中。这些文件连同Windows SDK的包含文件 WINDOWS.H、COMMDLG.H、TOOLHELP.H、DDEML.H... 每每在编译过程中耗费大量的时间,因此你绝对有必要设定 Precompiled header。

Precompiled Header

一个应用程序在发展过程中常需要不断地编译。Windows 程序包含的标准.H 文件非常巨大但内容不变,编译器浪费在这上面的时间非常多。Precompiled header 就是将.H 文件第一次编译后的结果贮存起来,第二次再编译时就可以直接从磁盘中取出来用。这种观念在 Borland C/C++ 早已行之,Microsoft 这边则是一直到 Visual C++ 1.0 才具备。

简化的 MFC 程序架构-以 Hello MFC 为例

现在我们正式进入 MFC 程序设计。由于 Document/View 架构复杂,不适合初学者,所以我先把它略去。这里所提的程序观念是一般的 MFC Application Framework 的子集合。

本章程序名为Hello,运行时会在窗口中从天而降"Hello, MFC"字样。Hello 是一个非常简单而具代表性的程序,它的代表性在于:

  • 每一 个MFC程序都想从MFC中派生出适当的类来用(不然又何必以MFC 写程序呢),其中两个不可或缺的类 CWinApp和 CFrameWnd在 Hello程序中会表现出来,它们的意义如图 6-2。

  • MFC类中某些函数一定得被应用程序改写(例如 CWinApp::InitInstance),这在Hello程序中也看得到。

  • 菜单和对话框,Hello 也都具备。

图 6-3 是 Hello 源文件的组成。第一次接触MFC程序,我们常常因为不熟悉MFC的类分类、类命名规则,以至于不能在脑中形成具体印象,于是细部讨论时各种信息及说明彷如过眼烟云。相信我,你必须多看几次,并且用心熟记 MFC 命名规则。

图 6-3 之后是 Hello 程序的原始代码。由于MFC已经把Windows API都包装起来了,原始代码再也不能够「说明一切」。你会发现 MFC 程序很有点见林不见树的味道:

  • 看不到WinMain,因此不知程序从哪里开始执行。

  • 看不到RegisterClass 和 CreateWindow,那么窗口是如何做出来的呢?

  • 看不到Message Loop(GetMessage/DispatchMessage),那么程序如何推动?

  • 看不到Window Procedure,那么窗口如何运作?

我的目的就在铲除这些困惑。

Hello 程序原始代码

    HELLO.MAK - makefile
    RESOURCE.H - 所有资源ID 都在这里定义。本例只定义一个IDM_ABOUT。
    JJHOUR.ICO - 图标文件,用于主窗口和对话框。
   HELLO.RC - 资源描述文件。本例有一份菜单、一个图示、和一个对话框。
    STDAFX.H - 包含 AFXWIN.H。
    STDAFX.CPP  - 包含STDAFX.H,为的是制造出Precompiled header。
    HELLO.H - 声明CMyWinApp和CMyFrameWnd。
    HELLO.CPP  - 定义CMyWinApp和CMyFrameWnd。

注意:没有模块定义文件 .DEF?是的,如果你不指定模块定义文件,链接器就使用默认值。

图6-3 Hello程序的基本文件架构。一般习惯为每个类准备

一个 .H(声明 )和一个.CPP(实现),本例把两类集中在一起是为了简化。

HELLO.MAK(请在 DOS 窗口中执行 nmake hello.mak。环境设定请参考 p.224)

RESOURCE.H

#0001  // resource.h
#0002  #define IDM_ABOUT 100

HELLO.RC

STDAFX.H

STDAFX.CPP

HELLO.H

HELLO.CPP

上面这些程序代码中,你看到了一些MFC类如CWinApp和CFrameWnd,一些MFC数据类型如BOOL和VOID,一些MFC宏如 DECLARE_MESSAGE_MAP和BEGIN_MESSAGE_END 和END_MESSAGE_MAP。这些都曾经在第5章的「纵览 MFC」一节中露过脸。但是单纯从C++ 语言的角度来看,还有一些是我们不能理解的,如HELLO.H 中的afx_msg(#23 行)和 CALLBACK(#28 行)。

你可以在WINDEF.H 中发现CALLBACK 的意义:

#define CALLBACK  __stdcall // 一种函数调用习惯

可以在AFXWIN.H中发现afx_msg 的意义:

#define afx_msg // intentional placeholder
// 故意安排的一个空位置。也许以后版本会用到。

MFC 程序的来龙去脉(causal relations)

让我们从第1章的C/SDK 观念出发,看看MFC程序如何运作。

第一件事情就是找出 MFC 程序的进入点。MFC 程序也是 Windows 程序,所以它应该也有一个WinMain,但是我们在 Hello 程序看不到它的踪影。是的,但先别急,在程序进入点之前,更有一个(而且仅有一个)全局对象(本例名为 theApp),这是所谓的application object,当操作系统将程序加载并启动,这个全局对象获得配置,其构造函数会先执行,比 WinMain 更早。所以以时间顺序来说,我们先看看这个 application object。

我只借用两个类:CWinApp 和 CFrameWnd

你已经看过了图 6-2,作为一个最最粗浅的MFC程序,Hello是如此单纯,只有一个窗口。回想第一章 Generic 程序的写法,其主体在于 WinMain 和 WndProc,而这两个部分其实都有相当程度的不变性。好极了,MFC 就把有着相当固定行为之 WinMain 内部动作包装在 CWinApp 中,把有着相当固定行为之 WndProc 内部动作包装在 CFrameWnd 中。也就是说:

  • CWinApp 代表程序本体

  • CFrameWnd 代表一个框架窗口(Frame Window)

但虽然我说,WinMain 内部动作和 WndProc 内部动作都有着相当程度的固定行为,它们毕竟需要面对不同应用程序而有某种变化。所以,你必须以这两个类为基础,派生自己的类,并改写其中一部分成员函数。

class CMyWinApp : public CWinApp
{
    ...
};
class CMyFrameWnd : public CFrameWnd
{
    ...
};

本章对派生类的命名规则是:在基类名称的前面加上"My"。这种规则真正上战场时不见得适用,大型程序可能会自同一个基类派生出许多自己的类。不过以教学目的而言,这种命名方式使我们从字面就知道类之间的从属关系,颇为理想(根据我的经验,初学者会被类的命名搞得头昏脑胀)。

CwinApp——取代 WinMain 的地位

CWinApp 的派生对象被称为application object,可以想见,CWinApp 本身就代表一个程序本体。一个程序的本体是什么?回想第1章的SDK 程序,与程序本身有关而不与窗口有关的数据或动作有些什么?系统传进来的四个WinMain 参 数 算 不 算 ?InitApplication 和InitInstance 算不算?消息循环算不算?都算,是的,以下是MFC 4.x 的CWinApp声明(节录自AFXWIN.H):

几乎可以说CWinApp 用来取代WinMain在SDK程序中的地位。这并不是说MFC程序没有WinMain(稍后我会解释),而是说传统上 SDK 程序的 WinMain 所完成的工作现在由CWinApp 的三个函数完成:

virtual BOOL InitApplication();
virtual BOOL InitInstance();
virtual int  Run();

WinMain 只是扮演役使它们的角色。

会不会觉得CWinApp 的成员变量中少了点什么东西?是不是应该有个成员变量记录主窗口的handle(或是主窗口对应之C++ 对象)?的确,在MFC 2.5 中的确有m_pMainWnd 这么个成员变量(以下节录自MFC 2.5 的AFXWIN.H):

但从MFC 4.x开始,m_pMainWnd 已经被移往CWinThread 中了(它是 CWinApp 的父类)。以下内容节录自 MFC 4.x 的 AFXWIN.H:

熟悉Win32的朋友,看到CWinThread 类之中的SuspendThread 和 ResumeThread成员函数,可能会发出会心微笑。

CFrameWnd -取代 WndProc 的地位

CFrameWnd 主要用来掌握一个窗口,几乎你可以说它是用来取代SDK程序中的窗口函数的地位。传统的SDK窗口函数写法是:

MFC程序有新的作法,我们在Hello程序中也为CMyFrameWnd 准备了两个消息处理例程,声明如下:

OnPaint 处理什么消息?OnAbout又是处理什么消息?我想你很容易猜到,前者处理WM_PAINT,后者处理WM_COMMAND的IDM_ABOUT。这看起来十分利落,但让人搞不懂来龙去脉。程序中是不是应该有「把消息和处理函数关联在一起」的设定动作?是的,这些设定在HELLO.CPP才看得到。但让我先着一鞭:DECLARE_MESSAGE_MAP宏与此有关。

这种写法非常奇特,原因是 MFC 内建了一个所谓的Message Map机制,会把消息自动送到「与消息对映之特定函数」去;消息与处理函数之间的对映关系由程序员指定。

DECLARE_MESSAGE_MAP 另搭配其它宏,就可以很便利地将消息与其处理函数关联在一起:

BEGIN_MESSAGE_MAP(CMyFrameWnd, CFrameWnd)
ON_WM_PAINT()
ON_COMMAND(IDM_ABOUT, OnAbout)
END_MESSAGE_MAP()

稍后我就来探讨这些神秘的宏。

引爆器-Application object

我们已经看过HELLO.H声明的两个类,现在把目光转到HELLO.CPP 身上。这个文件将两个类实现出来,并产生一个所谓的application object。故事就从这里展开。

下面这张图包括右半部的Hello原始代码与左半部的MFC原始代码。从这一节以降,我将以此图解释MFC程序的启动、运行、与结束。不同小节的图将标示出当时的程序进行状况。

上图的theApp就是Hello程序的application object,每一个MFC应用程序都有一个,而且也只有这么一个。当你执行Hello,这个全局对象产生,于是构造函数执行起来。我们并没有定义CMyWinApp 构造函数;至于其父类 CWinApp 的构造函数内容摘要如下(摘录自 APPCORE.CPP):

CWinApp之中的成员变量将因为theApp这个全局对象的诞生而获得配置与初值。如果程序中没有theApp存在,编译链接还是可以顺利通过,但运行时会出现系统错误消息。

隐晦不明的 WinMain

theApp 配置完成后,WinMain 登场。我们并未撰写 WinMain 程序代码,这是 MFC 早已准备好并由链接器直接加到应用程序代码中的,其原始代码列于图 6-4。_tWinMain 函数的 是为了支持Unicode而准备的一个宏。

此外,在DLLMODUL.CPP中有一个DllMain函数。本书并未涵盖DLL程序设计。

图6-4 Windows 程序进入点。原始代码可从MFC的WINMAIN.CPP中获得

稍加整理去芜存菁,就可以看到这个「程序进入点」主要做些什么事:

其中,AfxGetApp是一个全局函数,定义于AFXWIN1.INL中:

_AFXWIN_INLINE CWinApp* AFXAPI AfxGetApp()
{ return afxCurrentWinApp; }

而afxCurrentWinApp 又定义于 AFXWIN.H 中:

#define afxCurrentWinApp  AfxGetModuleState()->m_pCurrentWinApp

再根据稍早所述 CWinApp::CWinApp 中的动作,我们于是知道,AfxGetApp 其实就是取得 CMyWinApp 对象指针。所以,AfxWinMain 中这样的动作:

CWinApp* pApp = AfxGetApp();
pApp->InitApplication();
pApp->InitInstance();
nReturnCode = pApp->Run();

其实就相当于调用:

CMyWinApp::InitApplication();
CMyWinApp::InitInstance();
CMyWinApp::Run();

因而导至调用:

CWinApp::InitApplication();//因为CMyWinApp并没有改写InitApplication
CMyWinApp::InitInstance(); //因为 CMyWinApp改写了InitInstance
CWinApp::Run();           //因为 CMyWinApp并没有改写 Run

根据第1章SDK程序设计的经验推测,InitApplication 应该是注册窗口类的场所?InitInstance 应该是产生窗口并显示窗口的场所?Run应该是攫取消息并分派消息的场所?有对有错!以下数节我将实际带你看看MFC的原始代码,如此一来就可以了解隐藏在MFC背后的玄妙了。我的终极目标并不在 MFC 原始代码(虽然那的确是学习设计一个application framework 的好教材),我只是想拿把刀子把MFC看似朦胧的内部运作来个大解剖,挑出其经脉;有这种扎实的根基,使用 MFC 才能知其然并知其所以然。下面小节分别讨论AfxWinMain的四个主要动作以及引发的行为。

AfxWinInit -AFX 内部初始化动作

我想你已经清楚看到了,AfxWinInit 是继 CWinApp 构造函数之后的第一个动作。以下是它的动作摘要(节录自 APPINIT.CPP):

其中调用的AfxInitThread函数的动作摘要如下(节录自 THRDCORE.CPP):

如果你曾经看过本书前身Visual C++ 面向对象MFC程序设计,我想你可能对这句话印象深刻:「WinMain 一开始即调用AfxWinInit,注册四个窗口类」。这是一个已成昨日黄花的事实。MFC 的确会为我们注册四个窗口类,但不再是在AfxWinInit 中完成。稍后我会把注册动作挖出来,那将是窗口诞生前一刻的行为。

CWinApp::InitApplication

AfxWinInit之后的动作是pApp->InitApplication。稍早我说过了,pApp 指向CMyWinApp对象(也就是本例的theApp),所以,当程序调用:

pApp->InitApplication();

相当于调用:

CMyWinApp::InitApplication();

但是你要知道,CMyWinApp继承自 CWinApp,而InitApplication又是 CWinApp 的一个虚函数;我们并没有改写它(大部分情况下不需改写它),所以上述动作相当于调用:

CWinApp::InitApplication();

此函数之原始代码出现在APPCORE.CPP 中:

这些动作都是MFC为了内部管理而做的。

关于Document Template和CDocManager,第7章和第8章另有说明。

CMyWinApp::InitInstance

继InitApplication 之后,AfxWinMain调用 pApp->InitInstance。稍早我说过了,pApp 指向CMyWinApp 对象(也就是本例的 theApp),所以,当程序调用:

pApp->InitInstance();

相当于调用

CMyWinApp::InitInstance();

但是你要知道,CMyWinApp继承自CWinApp,而InitInstance 又是CWinApp 的一个虚函数。由于我们改写了它,所以上述动作的的确确就是调用我们自己(CMyWinApp)的这个InitInstance 函数。我们将在该处展开我们的主窗口生命。

注意:应用程序一定要改写虚函数 InitInstance,因为它在 CWinApp 中只是个空函数,没有任何内建(预设)动作。

CFrameWnd::Create 产生主窗口(并先注册窗口类)

CMyWinApp::InitInstance 一开始new 了一个CMyFrameWnd 对象,准备用作框架窗口的 C++ 对象。new 会引发构造函数:

CMyFrameWnd::CMyFrameWnd
{
    Create(NULL, "Hello MFC", WS_OVERLAPPEDWINDOW, rectDefault, NULL, "MainMenu");
}

其中Create是CFrameWnd 的成员函数,它将产生一个窗口。但,使用哪一个窗口类呢?

这里所谓的「窗口类」是由 RegisterClass 所注册的一份数据结构,不是 C++ 类。

根据 CFrameWnd::Create 的规格:

八个参数中的后六个参数都有默认值,只有前两个参数必须指定。第一个参 数lpszClassName指定WNDCLASS 窗口类,我们放置NULL 究竟代表什么意思?意思是要以MFC内建的窗口类产生一个标准的外框窗口。但,此时此刻Hello程序中根本不存在任何窗口类呀!噢,Create 函数在产生窗口之前会引发窗口类的注册动作,稍后再解释。

第三个参数 dwStyle 指定窗口风格,预设是 WS_OVERLAPPEDWINDOW,也正是最常用的一种,它 被定义为(在 WINDOWS.H 之中):

因此如果你不想要窗口右上角的极大极小钮,就得这么做:

如果你希望窗口有垂直滚动条,就得在第三个参数上再加增WS_VSCROLL风格。第二个参数lpszWindowName指定窗口标题,本例指定 "Hello MFC"。第三除了上述标准的窗口风格,另有所谓的扩充风格,可以在Create 的第七个参数dwExStyle指定之。扩充风格唯有以::CreateWindowEx(而非::CreateWindow)函数才能完成。事实上稍后你就会发现,CFrameWnd::Create 最终调用的正是::CreateWindowEx。Windows 3.1 提供五种窗口扩充风格:

WS_EX_DLGMODALFRAME
WS_EX_NOPARENTNOTIFY
WS_EX_TOPMOST
WS_EX_ACCEPTFILES
WS_EX_TRANSPARENT

Windows 95有更多选择,包括 WS_EX_WINDOWEDGE和WS_EX_CLIENTEDGE,让窗口更具3D立体感。Framework已经自动为我们指定了这两个扩充风格。

Create 的第四个参数rect指定窗口的位置与大小。默认值rectDefault是CFrameWnd的一个static 成员变量,告诉Windows以预设方式指定窗口位置与大小,就好像在SDK 程序中以CW_USEDEFAULT 指定给CreateWindow 函数一样。如果你很有主见,

希望窗口在特定位置有特定大小,可以这么做:

第五个参数pParentWnd 指定父窗口。对于一个top-level 窗口而言,此值应为NULL,表示没有父窗口(其实是有的,父窗口就是 desktop 窗口)。

第六个参数lpszMenuName指定选单。本例使用一份在RC中准备好的选单MainMenu。第八个参数利用它,在具备 Document/View 架构的程序中初始化外框窗口(第8章的「CDocTemplate管理CDocument /CView/CFrameWnd」一节中将谈到此一主题)。本例不具备Document/View 架构,所以不必指定 pContext 参数,默认值为 NULL。

第八个参数 pContext 是一个指向 CCreateContext 结构的指针,framework前面提过,CFrameWnd::Create 在产生窗口之前,会先引发窗口类的注册动作。让我再扮一次 MFC 向导,带你寻幽访胜。你会看到 MFC 为我们注册的窗口类名称,及注册动作。

WINFRM.CPP

函数中调用 CreateEx。注意,CWnd有成员函数CreateEx,但其派生类 CFrameWnd 并无,所以这里虽然调用的是CFrameWnd::CreateEx,其实乃是从父类继承下来的CWnd::CreateEx。

WINCORE.CPP

函数中调用的PreCreateWindow 是虚函数,CWnd 和CFrameWnd之中都有定义。由于this指针所指对象的缘故,这里应该调用的是 CFrameWnd::PreCreateWindow(还记得第2章我说过虚函数常见的那种行为模式吗?)

WINFRM.CPP

其中AfxDeferRegisterClass是一个定义于AFXIMPL.H中的宏。

AFXIMPL.H

这个宏表示,如果变量 afxRegisteredClasses 的值显示系统已经注册了 fClass 这种窗口类,MFC就啥也不做;否则就调用 AfxEndDeferRegisterClass(fClass),准备注册之。afxRegisteredClasses 定义于AFXWIN.H,是一个旗标变量,用来记录已经注册了哪些窗口类:

WINCORE.CPP :

出现在上述函数中的六个窗口类卷标代码,分别定义于AFXIMPL.H 中:

出现在上述函数中的五个窗口类名称,分别定义于WINCORE.CPP中:

而等号右手边的那些AFX_ 常数又定义于AFXIMPL.H 中:

所以,如果在 Windows 95(non-Unicode)中使用 MFC 动态链接版和除错版,五个窗口类的名称将是:

"AfxWnd42d"
"AfxControlBar42d"
"AfxMDIFrame42d"
"AfxFrameOrView42d"
"AfxOleControl42d"

如果在Windows NT(Unicode 环境)中使用 MFC静态链接版和除错版,五个窗口类的名称将是:

"AfxWnd42sud"
"AfxControlBar42sud"
"AfxMDIFrame42sud"
"AfxFrameOrView42sud"
"AfxOleControl42sud"

这五个窗口类的使用时机为何?稍后再来一探究竟。

让我们再回顾 AfxEndDeferRegisterClass 的动作。它调用两个函数完成实际的窗口类注册动作,一个是 RegisterWithIcon,一个是AfxRegisterClass:

注意,不同类的 PreCreateWindow 成员函数都是在窗口产生之前一刻被调用,准备用来注册窗口类。如果我们指定的窗口类是 NULL,那么就使用系统预设类。从 CWnd及其各个派生类的PreCreateWindow 成员函数可以看出,整个Framework 针对不同功能的窗口使用了哪些窗口类:

题外话:「Create 是一个比较粗糙的函数,不提供我们对图标(icon)或鼠标光标的设定,所以在 Create 函数中我们看不到相关参数」。这样的说法对吗?虽然「不能够让我们指定窗口图标以及鼠标光标」是事实,但这本来就与 Create 无关。回忆SDK程序,指定图标和光标形状实为RegisterClass 的责任而非CreateWindow 的责任!

MFC 程序的RegisterClass 动作并非由程序员自己来做,因此似乎难以改变图示。不过,MFC 还是开放了一个窗口,我们可以在HELLO.RC这么设定图示:

AFX_IDI_STD_FRAME  ICON  DISCARDABLE  "HELLO.ICO"

你可以从AfxEndDeferRegisterClass的第55 行看出,当它调用 RegisterWithIcon时,指定的icon 正是AFX_IDI_STD_FRAME。

鼠标光标的设定就比较麻烦了。要改变光标形状,我们必须调用 AfxRegisterWndClass(其中有¨Cursor¨参数)注册自己的窗口类;然后再将其传回值(一个字符串)做为 Create 的第一个参数。

奇怪的窗口类名称 Afx:b:14ae:6:3e8f

当应用程序调用 CFrameWnd::Create(或 CMDIFrameWnd::LoadFrame,第7章)准备产生窗口时,MFC 才会在Create或LoadFrame内部所调用的 PreCreateWindow 虚函数中为你产生适当的窗口类。你已经在上一节看到了,这些窗口类的名称分别是(假设在Win95中使用MFC 4.2动态链接版和除错版):

"AfxWnd42d"
"AfxControlBar42d"
"AfxMDIFrame42d"
"AfxFrameOrView42d"
"AfxOleControl42d"

然而,当我们以Spy++(VC++ 所附的一个工具)观察窗口类的名称,却发现:

窗口类名称怎么会变成像Afx:b:14ae:6:3e8f这副奇怪模样呢?原来是 Application Framework玩了一些把戏,它把这些窗口类名称转换为 Afx:x:y:z:w 的型式,成为独一无二的窗口类名称:

x: 窗口风格(window style)的hex值

y: 窗口鼠标光标的hex值

z: 窗口背景颜色的hex值

w: 窗口图标(icon)的hex值

如果你要使用原来的(MFC 预设的)那些个窗口类,但又希望拥有自己定义的一个有意义的类名称,你可以改写PreCreateWindow 虚函数(因为Create 和LoadFrame的内部都会调用它),在其中先利用 API 函数 GetClassInfo 获得该类的一个副本,更改其类结构中的lpszClassName 栏位( 甚至更改其 hIcon栏位),再以AfxRegisterClass 重新注册之,例如:

本书附录D「以MFC重建Debug Window(DBWIN)」会运用到这个技巧。

窗口显示与更新

CMyFrameWnd::CMyFrameWnd 结 束 后 , 窗口已经诞生出来 ; 程序流程 又回到CMyWinApp::InitInstance , 于是调用ShowWindow 函数令窗口显示出来 ,并调用UpdateWindow 函数令 Hello 程序送出 WM_PAINT 消息。

我们很关心这个 WM_PAINT 消息如何送到窗口函数的手中。而且,窗口函数又在哪里?MFC程序是不是也像SDK 程序一样,有一个 GetMessage/DispatchMesage 循环?是否每个窗口也都有一个窗口函数,并以某种方式进行消息的判断与处理?两者都是肯定的。我们马上来寻找证据。

CWinApp::Run - 程序生命的活水源头

Hello 程序进行到这里,窗口类注册好了,窗口诞生并显示出来了,UpdateWindow 被调用,使得消息队列中出现了一个 WM_PAINT 消息,等待被处理。现在,执行的脚步到达 pApp->Run。

稍早我说过了,pApp 指向 CMyWinApp 对象(也就是本例的 theApp),所以,当程序调用:

pApp->Run();

相当于调用:

CMyWinApp::Run();

要知道,CMyWinApp 继承自CWinApp,而 Run又是CWinApp 的一个虚函数。我们并没有改写它(大部分情况下不需改写它),所以上述动作相当于调用:

CWinApp::Run();

其原始代码出现在 APPCORE.CPP 中:

32位MFC与16位MFC的巨大差异在于CWinApp与CCmdTarget之间多出了一个 CWinThread,事情变得稍微复杂一些。CWinThread 定义于THRDCORE.CPP:

获得的消息如何交给适当的例程去处理呢?SDK 程序的作法是调用 DispatchMessage,把消息丢给窗口函数;MFC 也是如此。但我们并未在 Hello 程序中提供任何窗口函数,是的,窗口函数事实上由 MFC 提供。回头看看前面 AfxEndDeferRegisterClass 原始代码,它在注册四种窗口类之前已经指定窗口函数为:

wndcls.lpfnWndProc = DefWindowProc;

注意,虽然窗口函数被指定为DefWindowProc成员函数,但事实上消息并不是被唧往该处,而是一个名为AfxWndProc的全局函数去。这其中牵扯到MFC暗中做了大挪移的手脚(利用hook和subclassing),我将在第9章详细讨论这个「乾坤大挪移」。

你看,WinMain 已由 MFC 提供,窗口类已由 MFC 注册完成、连窗口函数也都由 MFC提供。那么我们(程序员)如何为特定的消息设计特定的处理例程?MFC 应用程序对消息的辨识与判别是采用所谓的「Message Map 机制」。

把消息与处理函数串接在一起:Message Map 机制

文本框: ...

基本上Message Map机制是为了提供更方便的程序接口(例如宏或表格),让程序员很方便就可以建立起消息与处理例程的对应关系。这并不是什么新发明,我在第1章示范了一种风格简明的 SDK 程序写法,就已经展现出这种精神。MFC 提供给应用程序使用的「很方便的接口」是两组宏。以 Hello 的主窗口为例,第一个动作是在HELLO.H 的CMyFrameWnd 加上DECLARE_MESSAGE_MAP:

第二个动作是在HELLO.CPP的任何位置(当然不能在函数之内)使用宏如下:

BEGIN_MESSAGE_MAP(CMyFrameWnd, CFrameWnd)
ON_WM_PAINT()
ON_COMMAND(IDM_ABOUT, OnAbout)
END_MESSAGE_MAP()

这么一来就把消息WM_PAINT导到OnPaint函数,把WM_COMMAND(IDM_ABOUT)导到OnAbout 函数去了。但是,单凭一个ON_WM_PAINT 宏,没有任何参数,如何使 WM_PAINT 流到 OnPaint 函数呢?

MFC 把消息主要分为三大类,Message Map 机制中对于消息与函数间的对映关系也明定以下三种 :

标准Windows 消息(WM_xxx)的对映规则:

命令消息(WM_COMMAND)的一般性对映规则是:

ON_COMMAND(<id>,<memberFxn>)

例如:

ON_COMMAND(IDM_ABOUT,    OnAbout)
ON_COMMAND(IDM_FILENEW,  OnFileNew)
ON_COMMAND(IDM_FILEOPEN, OnFileOpen)
ON_COMMAND(IDM_FILESAVE, OnFileSave)

「Notification 消息」(由控制组件产生,例如BN_xxx)的对映机制的宏分为好几种(因为控制组件本就分为好几种),以下各举一例做代表:

各个消息处理函数均应以 afx_msg void 为函数型式。

为什么经过这样的宏之后,消息就会自动流往指定的函数去呢?谜底在于 Message Map的结构设计。如果你把第3章的Message Map仿真程序好好研究过,现在应该已是成竹在胸。我将在第9章再讨论MFC的Message Map。

好奇心摆两旁,还是先把实用上的问题放中间吧。如果某个消息在Message Map中找不到对映记录,消息何去何从?答案是它会往基类流窜,这个消息流窜动作称为「Message Routing」。如果一直窜到最基础的类仍找不到对映的处理例程,自会有预设函数来处理,就像 SDK 中的 DefWindowProc 一样。

MFC的CCmdTarget所派生下来的每一个类都可以设定自己的Message Map,因为它们都可能(可以)收到消息。

消息流动是个颇为复杂的机制,它和 Document/View、动态生成(Dynamic Creation),文件读写(Serialization)一样,都是需要特别留心的地方。

来龙去脉总整理

前面各节的目的就是如何将表面上看来不知所以然的MFC程序对映到我们在SDK程序设计中学习到的消息流动观念,从而清楚地掌握MFC程序的诞生与死亡。让我对MFC程序的来龙去脉再做一次总整理。

程序的诞生:

  • Application object 产生,内存于是获得配置,初值亦设立了。

  • AfxWinMain 执行AfxWinInit,后者又调用AfxInitThread,把消息队列尽量加大到96。

  • AfxWinMain执行InitApplication。这是CWinApp 的虚函数,但我们通常不改写它。

  • AfxWinMain执行InitInstance。这是CWinApp的虚函数,我们必须改写它。

  • CMyWinApp::InitInstance 'new' 了一个CMyFrameWnd 对象。

  • CMyFrameWnd 构造函数调用Create,产生主窗口。我们在Create 参数中指定的窗口类是NULL, 于是MFC根据窗口种类 , 自行为我们注册一个名为"AfxFrameOrView42d" 的窗口类。

  • 回到InitInstance中继续执行ShowWindow,显示窗口。

  • 执行UpdateWindow,于是发出WM_PAINT。

  • 回到AfxWinMain,执行Run,进入消息循环。

程序开始运作:

  • 程序获得WM_PAINT 消息(藉由CWinApp::Run 中的::GetMessage 循环)。

  • WM_PAINT经由::DispatchMessage送到窗口函数CWnd::DefWindowProc 中。

  • CWnd::DefWindowProc 将消息循环过消息映射表格(Message Map)。

  • 循环过程中发现有吻合项目,于是调用项目中对应的函数。此函数是应用程序利用BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间的宏设立起来的。

  • 标准消息的处理例程亦有标准命名,例如WM_PAINT必然由OnPaint处理。

以下是程序的死亡:

  • 用户选按【File/Close】,于是发出WM_CLOSE。

  • CMyFrameWnd 并没有设置 WM_CLOSE 处理例程,于是交给预设之处理例程。

  • 预设函数对于WM_CLOSE 的处 理方 式是调用::DestroyWindow,并因而发 出WM_DESTROY。

  • 预设之WM_DESTROY处理方式是调用::PostQuitMessage,因此发出 WM_QUIT。

  • CWinApp::Run 收到WM_QUIT后会结束其内部之消息循环,然后调用ExitInstance,这是CWinApp 的一个虚函数。

  • 如果CMyWinApp 改写了ExitInstance ,那么CWinApp::Run所调用的就是CMyWinApp::ExitInstance,否则就是CWinApp::ExitInstance。

  • 最后回到AfxWinMain,执行AfxWinTerm,结束程序。

Callback 函数

Hello的OnPaint在程序收到 WM_PAINT之后开始运作。为了让"Hello, MFC" 字样从天而降并有动画效果,程序采用LineDDA API 函数。我的目的一方面是为了示范消息的处理,一方面也为了示范 MFC 程序如何调用 Windows API 函数。许多人可能不熟悉LineDDA,所以我也一并介绍这个有趣的函数。

首先介绍 LineDDA:

void WINAPI LineDDA(int, int, int, int, LINEDDAPROC, LPARAM);

这个函数用来做动画十分方便,你可以利用前四个参数指定屏幕上任意两点的(x,y) 座标,此函数将以 Bresenham 算法(注) 计算出通过两点之直线中的每一个屏幕图素座标;每计算出一个坐标,就通知由LineDDA 第五个参数所指定的 callback 函数。这个callback 函数的型式必须是:

typedef void (CALLBACK* LINEDDAPROC)(int, int, LPARAM);

通常我们在这个 callback 函数中设计绘图动作。玩过Windows的接龙游戏吗?接龙成功后扑克牌的跳动效果就可以利用LineDDA 完成。虽然扑克牌的跳动路径是一条曲线,但将曲线拆成数条直线并不困难。LineDDA 的第六个(最后一个)参数可以视应用程序的需要传递一个32位指针,本例中Hello传的是一个Device Context。

Bresenham 算法是计算机图学中为了「显示器(屏幕或打印机)系由图素构成」的这个特性而设计出来的算法,使得求直线各点的过程中全部以整数来运算,因而大幅提升计算速度。

图6-6 LineDDA函数说明

你可以指定两个坐标点,LineDDA将以Bresenham 算法计算出通过两点之直线中每一个屏幕图素的坐标。每计算出一个坐标,就以该坐标为参数,调用你所指定的callback函数。

LineDDA 并不属于任何一个MFC 类,因此调用它必须使用C++ 的 "scope operator" (也就是 ::):

其中LineDDACallback是我们准备的callback 函数,必须在类中先有声明:

请注意,如果类的成员函数是一个callback 函数,你必须声明它为 "static",才能把C++ 编译器加诸于函数的一个隐藏参数this去掉(请看方块批注)。

以类的成员函数作为Windows callback函数

虽然现在来讲这个题目,对初学者而言恐怕是过于艰深,但我想毕竟还是个好机会--- 我可以在介绍如何使用callback 函数的场合,顺便介绍一些C++的重要观念。

首先我要很快地解释一下什么是callback 函数。凡是由你设计而却由 Windows系统调用的函数,统称为callback函数。这些函数都有一定的类型,以配合Windows的调用动作。

某些Windows API函数会要求以callback 函数作为其参数之一,这些API 例如SetTimer、LineDDA、EnumObjects。通常这种 API 会在进行某种行为之后或满足某种状态之时调用该 callback 函数。图 6-6 已解释过 LineDDA调用 callback 函数的时机;下面即将示范的 EnumObjects 则是在发现某个 Device Context 的 GDI object 符合我们的指定类型时,调用 callback 函数。

好,现在我们要讨论的是,什么函数有资格在 C++ 程序中做为 callback 函数?这个问题的背后是:C++ 程序中的 callback 函数有什么特别的吗?为什么要特别提出讨论?

是的,特别之处在于,C++ 编译器为类成员函数多准备了一个隐藏参数(程序代码中看不到),这使得函数类型与 Windows callback 函数的预设类型不符。

假设我们有一个CMyclass 如下:

C++ 编译器针对CMyclass::enumIt实际做出来的代码相当于:

你所看到的最后一个参数,(CDC *)&dc,其实就是this指针。类成员函数靠着this指针才得以抓到正确对象的数据。你要知道,内存中只会有一份类成员函数,但却可能有许多份类成员变量——每个对象拥有一份。

C++ 以隐晦的this指针指出正确的对象。当你这么做:

nCount = 0;

其实是:

this->nCount = 0;

基于相同的道理,上例中的 EnumObjectsProc 既然是一个成员函数,C++ 编译器也会为它多准备一个隐藏参数。

好,问题就出在这个隐藏参数。callback函数是给Windows调用用的,Windows 并不经由任何对象调用这个函数,也就无由传递this指针给callback 函数,于是导至堆栈中有一个随机变量会成为this指针,而其结果当然是程序的崩溃了。

要把某个函数用作callback函数,就必须告诉C++编译器,不要放this指针作为该函数的最后一个参数。两个方法可以做到这一点:

1. 不要使用类的成员函数(也就是说,要使用全局函数)做为callback 函数。

2. 使用static 成员函数。也就是在函数前面加上static修饰词。

第一种作法相当于在 C 语言中使用callback 函数。第二种作法比较接近 OO的精神。

我想更进一步提醒你的是,C++中的static成员函数特性是,即使对象还没有产生,static 成员也已经存在(函数或变量都如此)。换句话说对象还没有产生之前你已经可以调用类的static函数或使用类的static变量了。请参阅第二章。

也就是说,凡声明为static 的东西(不管函数或变量)都并不和对象结合在一起,它们是类的一部分,不属于对象。

闲置时间(idle time)的处理:OnIdle

为了让Hello 程序更具体而微地表现一个MFC应用程序的水平,我打算为它加上闲置时间(idle time)的处理。

我已经在第1章介绍过了闲置时间,也简介了Win32程序如何以 PeekMessage「偷闲」。Microsoft 业已把这个观念落实到CWinApp(不,应该是CWinThread)中。请你回头看看本章的稍早的「CWinApp::Run - 程序生命的活水源头」一节,那一节已经揭露了MFC消息循环的秘密:

CThread::OnIdle 做些什么事情呢?CWinApp 改写了OnIdle 函数,CWinApp::OnIdle 又做些什么事情呢?你可以从THRDCORE.CPP和 APPCORE.CPP 中找到这两个函数的原始代码,原始代码可以说明一切。当然基本上我们可以猜测OnIdle 函数中大概是做一些系统(指的是 MFC 本身)的维护工作。这一部分的功能可以说日趋式微,因为低优先权的线程可以替代其角色。

如果你的MFC 程序也想处理idle time,只要改写CWinApp 派生类的 OnIdle 函数即可。这个函数的类型如下:

virtual BOOL OnIdle(LONG lCount);

lCount是系统传进来的一个值,表示自从上次有消息进来,到现在,OnIdle 已经被调用了多少次。稍后我将改写Hello 程序,把这个值输出到窗口上,你就可以知道闲置时间是多么地频繁。lCount会持续累增,直到 CWinThread::Run 的消息循环又获得了一个消息,此值才重置为0。

注意:Jeff Prosise 在他的Programming Windows 95 with MFC一书第7章谈到OnIdle函数时,曾经说过有几个消息并不会重置lCount为0,包括鼠标消息、WM_SYSTIMER、WM_PAINT。不过根据我实测的结果,至少鼠标消息是会的。稍后你可在新版的Hello程序移动鼠标,看看lCount会不会重设为0。

我如何改写Hello 呢?下面是几个步骤:

1. 在CMyWinApp中增加OnIdle 函数的声明:

2. 在CMyFrameWnd中增加一个IdleTimeHandler函数声明。这么做是因为我希望在窗口中显示lCount值,所以最好的作法就是在OnIdle中调用CMyFrameWnd 成员函数,这样才容易获得绘图所需的DC。

3. 在HELLO.CPP 中定义CMyWinApp::OnIdle 函数如下:

BOOL CMyWinApp::OnIdle(LONG lCount)
{
    CMyFrameWnd* pWnd = (CMyFrameWnd*)m_pMainWnd;
    pWnd->IdleTimeHandler(lCount);
    return TRUE;
}

4. 在HELLO.CPP 中定义CMyFrameWnd::IdleTimeHandler 函数如下:

void CMyFrameWnd::IdleTimeHandler(LONG lCount)
{
    CString str;
    CRect rect(10,10,200,30);
    CDC* pDC = new CClientDC(this);
    str.Format("%010d", lCount);
    pDC->DrawText(str, &rect, DT_LEFT | DT_TOP);
}

为了输出lCount,我又动用了三个MFC 类:CString、CRect 和 CDC。前两者非常简单,只是字符串与四方形结构的一层C++包装而且,后者是在 Windows 系统中绘图所必须的 DC(Device Context)的一个包装。

新版Hello执行结果如下。左上角的lCount以飞快的速度更迭。移动鼠标看看,看lCount 会不会重置为0。

Dialog 与 Control

回忆SDK程序中的对话框作法:RC 文件中要准备一个对话框的Template,C 程序中要设计一个对话框函数。MFC 提供的CDialog已经把对话框的窗口函数设计好了,因此在MFC 程序中使用对话框非常地简单:

当使用者按下【 File/About】选单 , 根据Message Map的设定 , WM_COMMAND(IDM_ABOUT)被送到OnAbout函数去。我们首先在OnAbout中产生一个CDialog 对象,名为about。CDialog 构造函数容许两个参数,第一个参数是对话框的面板资源,第二个参数是about对象的主人。由于我们的"About" 对话框是如此地简单,不需要改写CDialog 中的对话框函数,所以接下来直接调用CDialog::DoModal,对话框就开始运作了。

通用对话框(Common Dialogs)

有些对话框,例如【File Open】或【Save As】对话框,出现在每一个程序中的频率是如此之高,使微软公司不得不面对此一事实。于是,自从Windows 3.1 之后,Windows API多了一组通用对话框(Common Dialogs)API函数,系统也 多了一个对应的COMMDLG.DLL(32 位版则为COMDLG32.DLL)。

MFC 也支持通用对话框,下面是其类与其类型:

在C/SDK 程序中,使用通用对话框的方式是,首先填充一块特定的结构如

OPENFILENAME,然后调用API函数如GetOpenFileName。当函数回返,结构中的某些字段便持有了用户输入的值。

MFC 通用对话框类,使用之简易性亦不输 Windows API。下面这段代码可以启动【Open】对话框并最后获得文件完整路径:

opendlg构造函数的第一个参数被指定为TRUE,表示我们要的是一个【Open】对话框而不是【Save As】对话框。第二参数"txt"指定预设扩展名;如果用户输入的文件没有扩展名,就自动加上此一扩展名。第三个参数"*.txt" 出现在一开始的【file name】字段中。OFN_ 参数指定文件的属性。第五个参数 szFilters 指定用户可以选择的文件类型,最后一个参数是父窗口。

当DoModal回返,我们可以利用 CFileDialog的成员函数GetPathName 取得完整的文件路径。也可以使用另一个成员函数 GetFileName 取其不含路径的文件名称,或GetFileTitle 取得既不含路径亦不含扩展名的文件名称。

这便是 MFC 通用对话框类的使用。你几乎不必再从其中派生出子类,直接用就好了。

本章回顾

乍看MFC应用程序代码,实在很难推想程序的进行。一开始是一个派生自 CWinApp 的全局对象application object,然后是一个隐藏的WinMain函数,调用 application object 的InitInstance 函数,将程序初始化。初始化动作包括构造一个窗口对象(CFrameWnd 对象),而其构造函数又调用 CFrameWnd::Create 产生真正的窗口(并在产生之前要求MFC注册窗口类)。窗口产生后 WinMain 又调用Run启动消息循环,将WM_COMMAND(IDM_ABOUT)和WM_PAINT分别交给成员函数OnAbout和OnPaint处理。

虽然刨根究底不易,但是我们都同意,MFC 应用程序代码的确比SDK应用程序代码精简许多。事实上,MFC并不打算让应用程序代码比较容易理解,毕竟 raw Windows API才是最直接了当的动作。许许多多细碎动作被包装在MFC类之中,降低了你写程序的负担,当然,这必须建立在一个事实之上:你永远可以改变MFC的预设行为。这一点是无庸置疑的,因为所有你可能需要改变的性质,都被设计为MFC 类中的虚函数了,你可以从MFC派生出自己的类,并改写那些虚函数。

MFC 的好处在更精巧更复杂的应用程序中显露无遗。至于复杂如OLE者,那就更是非MFC不为功了。本章的Hello程序还欠缺许多Windows程序完整功能,但它毕竟是一个好起点,有点晦涩但不太难。下一章范例将运用MDI、Document/View、各式各样的UI对象...。