教程:轻松玩转MFC文档视图架构编程

Similar documents
ebook50-14

INTRODUCTION TO COM.DOC

Microsoft PowerPoint - ch6 [相容模式]

概述

ebook50-15

epub83-1

Important Notice SUNPLUS TECHNOLOGY CO. reserves the right to change this documentation without prior notice. Information provided by SUNPLUS TECHNOLO

Microsoft Word - template.doc

MFC 2/e PDF GBK mirror - anyway solution MFC 1/e MFC 2/e

Windows XP

untitled

无类继承.key

Serial ATA ( Silicon Image SiI3114)...2 (1) SATA... 2 (2) B I O S S A T A... 3 (3) RAID BIOS RAID... 5 (4) S A T A... 8 (5) S A T A... 10

Fun Time (1) What happens in memory? 1 i n t i ; 2 s h o r t j ; 3 double k ; 4 char c = a ; 5 i = 3; j = 2; 6 k = i j ; H.-T. Lin (NTU CSIE) Referenc

1.ai

K7VT2_QIG_v3

Windows RTEMS 1 Danilliu MMI TCP/IP QEMU i386 QEMU ARM POWERPC i386 IPC PC104 uc/os-ii uc/os MMI TCP/IP i386 PORT Linux ecos Linux ecos ecos eco

ebook50-11

Guide to Install SATA Hard Disks

AL-M200 Series

IP505SM_manual_cn.doc

Olav Lundström MicroSCADA Pro Marketing & Sales 2005 ABB - 1-1MRS755673

前言 C# C# C# C C# C# C# C# C# microservices C# More Effective C# More Effective C# C# C# C# Effective C# 50 C# C# 7 Effective vii

WWW PHP

WinMDI 28

基于UML建模的管理管理信息系统项目案例导航——VB篇

<4D F736F F D C4EAC0EDB9A4C0E04142BCB6D4C4B6C1C5D0B6CFC0FDCCE2BEABD1A15F325F2E646F63>

穨control.PDF

AL-MX200 Series

27 :OPC 45 [4] (Automation Interface Standard), (Costom Interface Standard), OPC 2,,, VB Delphi OPC, OPC C++, OPC OPC OPC, [1] 1 OPC 1.1 OPC OPC(OLE f

Chn 116 Neh.d.01.nis

C/C++ - 字符输入输出和字符确认

WTO

TX-NR3030_BAS_Cs_ indd

WebSphere Studio Application Developer IBM Portal Toolkit... 2/21 1. WebSphere Portal Portal WebSphere Application Server stopserver.bat -configfile..

Value Chain ~ (E-Business RD / Pre-Sales / Consultant) APS, Advanc

OOP with Java 通知 Project 4: 4 月 19 日晚 9 点

Microsoft Word - (web)_F.1_Notes_&_Application_Form(Chi)(non-SPCCPS)_16-17.doc

Strings

untitled

mvc

提问袁小兵:

概述

Microsoft Word - ch04三校.doc

SA-DK2-U3Rユーザーズマニュアル

Preface This guide is intended to standardize the use of the WeChat brand and ensure the brand's integrity and consistency. The guide applies to all d

1 Framework.NET Framework Microsoft Windows.NET Framework.NET Framework NOTE.NET NET Framework.NET Framework 2.0 ( 3 ).NET Framework 2.0.NET F

FY.DOC

新・解きながら学ぶJava

Applied Biosystems StepOne™ Real-Time PCR System Quick Reference Card for Installation

Microsoft Word - SupplyIT manual 3_cn_david.doc

Microsoft Word - 3D手册2.doc

Microsoft Word - 第3章.doc

K301Q-D VRT中英文说明书141009

第12讲 Windows API

新版 明解C++入門編

國立中山大學學位論文典藏.PDF

summerCampBookP1~16.pdf

1 1 大概思路 创建 WebAPI 创建 CrossMainController 并编写 Nuget 安装 microsoft.aspnet.webapi.cors 跨域设置路由 编写 Jquery EasyUI 界面 运行效果 2 创建 WebAPI 创建 WebAPI, 新建 -> 项目 ->

untitled

OOP with Java 通知 Project 3: 3 月 29 日晚 9 点 4 月 1 日上课

1505.indd

ebook140-8

BOOL EnumWindows(WNDENUMPROC lparam); lpenumfunc, LPARAM (Native Interface) PowerBuilder PowerBuilder PBNI 2

Chapter 9: Objects and Classes

BC04 Module_antenna__ doc

f2.eps

99 學年度班群總介紹 第 370 期 班群總導 陳怡靜 G45 班群總導 陳怡靜(河馬) A 家 惠如 家浩 T 格 宜蓁 小 霖 怡 家 M 璇 均 蓁 雴 家 數學領域 珈玲 國燈 英領域 Kent

Microsoft PowerPoint - string_kruse [兼容模式]

ebook140-9

PowerPoint Presentation

Microsoft Word - 01.DOC

IBM Rational ClearQuest Client for Eclipse 1/ IBM Rational ClearQuest Client for Ecl

摘 要 互 联 网 的 勃 兴 为 草 根 阶 层 书 写 自 我 和 他 人 提 供 了 契 机, 通 过 网 络 自 由 开 放 的 平 台, 网 络 红 人 风 靡 于 虚 拟 世 界 近 年 来, 或 无 心 插 柳, 或 有 意 噱 头, 或 自 我 表 达, 或 幕 后 操 纵, 网 络

Bus Hound 5

<4D F736F F D205F FB942A5CEA668B443C5E9BB73A740B5D8A4E5B8C9A552B1D0A7F75FA6BFB1A4ACFC2E646F63>

ch_code_infoaccess

<4D F736F F D20B2F8A74AA4AF5FA578C657A175BCC6A6ECB6D7AC79A176BB50A46AB3B0A175A454BAF4A658A440A176AC46B5A6A641B1B4>

Microsoft Word - 11.doc

Lorem ipsum dolor sit amet, consectetuer adipiscing elit

國家圖書館典藏電子全文

星河33期.FIT)

RUN_PC連載_12_.doc

ebook39-6

ebook39-5


RunPC2_.doc

エスポラージュ株式会社 住所 : 東京都江東区大島 東急ドエルアルス大島 HP: ******************* * 关于 Java 测试试题 ******

Transcription:

教程 : 轻松玩转 MFC 文档视图架构编程 文档 / 视图 结构是 MFC 中结构最为复杂, 体系最为庞大, 而又最富有特色的部分, 其中涉及到应用 文档模板 文档 视图 SDI 窗口 MDI 框架窗口 MDI 子窗口等多种不同的类, 如果不了解这些类及其 盘根错节的内部联系的话, 就不可能编写出高水平的文档 / 视图程序 学习 " 文档 / 视图 " 结构的意义还不只于其本身, 通过该架构的学习, 一步步领略 MFC 设计者的神功奥妙, 也将进一步增强我们自身对庞大程序框架的把握能力 一个优秀的程序员是可以写出一个个优秀函数的程序员, 而一个优秀的系统设计师则需从全局把握软件的架构, 分析和学习 " 文档 / 视图 " 结构将是我们成为软件系统设计师之旅的一个重要阶段 理解了 MFC 文档视图类的意义及它们纵横交错的关系也就理解了 文档 / 视图 结构的基本概念, 在此基础上, 我们再进一步研究 文档 / 视图 结构的 MFC 程序消息流动的方向, 这样就完全彻底明白了基于 " 文档 / 视图 " 结构 MFC 程序的 生死因果 1

第一讲基本概念 深入浅出 MFC 文档 / 视图架构之基本概念 MFC 引入了 " 文档 / 视图 " 结构的概念, 理解这个结构是编写基于 MFC 编写复杂 Visual C++ 程序的关键 " 文档 / 视图 " 中主要涉及到四种类 : (1) 文档模板 : class CDocTemplate; // template for document creation class CSingleDocTemplate; // SDI support class CMultiDocTemplate; // MDI support (2) 文档 : class CDocument; // main document abstraction (3) 视图 : // views on a document class CView; // a view on a document class CScrollView; // a scrolling view (4) 框架窗口 : // frame windows class CFrameWnd; // standard SDI frame class CMDIFrameWnd; // standard MDI frame class CMDIChildWnd; // standard MDI child class CMiniFrameWnd; // half-height caption frame wnd 理解了这 4 个类各自的意义及它们纵横交错的关系也就理解了 " 文档 / 视图 " 结构的基本概念, 在此基础上, 我们还需要进一步研究 " 文档 / 视图 " 结构的 MFC 程序消息流动的方向, 这样就完全彻底明白了基于 " 文档 / 视图 " 结构 MFC 程序的 " 生死因果 " 出于以上考虑, 本文这样组织了各次连载的内容 : 第 1 次连载进行基本概念的介绍, 第 2~5 次连载分别讲述文档模板 文档 视图和框架窗口四个类的功能和主要函数, 连载 6 则综合阐述四个类之间的关系, 接着以连载 7 讲解消息流动的方向, 最后的连载 8 则以实例剖析连载 1~7 所讲述的所有内容 2

本文所有的代码基于 WIN32 平台开发, 调试环境为 Visual C++6.0 架构 MFC" 文档 / 视图 " 结构被认为是一种架构, 关于什么是架构, 这是个 " 仁者见仁, 智者见智 " 的问题 在笔者看来, 成其为架构者, 必具备如下两个特性 : (1) 它是一种基础性平台, 是一个模型 通过这个平台 这个模型, 我们在上面进一步修饰, 可以得到无穷无尽的新事物 譬如, 建筑学上的钢筋混凝土结构 ISO( 国际标准化组织 ) 的 OSI( 开放式系统互连 ) 七层模型 架构只是一种基础性平台, 不同于用这个架构造出的实例 钢筋混凝土结构是架构, 而用钢筋混凝土结构造出的房子就不能称为架构 这个特性强调了架构的外部特征, 即架构具有可学习 可再生 可实例化的特点, 是所有基于该架构所构造实例的共性, 是贯串在它们体内的一根 " 筋 ", 但各个基于该架构所构造的实例彼此是存在差异的 (2) 它是一个由内部有联系的事物所组成的一个有机整体 架构中的内部成员不是彼此松散的, 并非各自 " 占山为王 ", 它们歃血为盟, 紧密合作, 彼此都有明确的责任和分工, 因此共同构筑了一个统一的基础性平台 一个统一的模型 譬如,OSI 模型从物理层到应用层进行了良好的合作, 虽然内部包含了复杂的多个层次, 但仍然脉络清晰 由此可见, 架构的第 2 个特性是服务于第 1 个特性的 理解架构, 关键是理解以上两个特性 而针对特定的 " 文档 / 视图 " 结构, 则需理解如下两个问题 : (1) 学习这个架构, 并学会在这个架构上造房子 ( 编写基于 " 文档 / 视图 " 结构的程序 ); (2) 理解这个架构内部的工作机理 ( 文档模板 文档 视图和框架窗口四个类是如何联系为一个有机整体的 ), 并在造房子时加以灵活应用 ( 重载相关的类 ) 在这里, 我们再引用几位专家 ( 或企业 ) 关于架构的定义以供读者进一步参考 : The key ideas of a commercial application framework : a generic app on steroids that provides a large amount of general-purpose functionality within a well-planned, welltested, cohesive structure. (Application framework is) an extended collection of classes that cooperate to support a complete application architecture or application model, providing more complete application development support than a simple set of class libraries. MacApp(Apple's C++ application framework) An application framework is an integrated object-oriented software system that offers all the application-level classes(documents, views, and commands)needed by a generic application. An application framework is meant to be used in its entirety, and fosters both design reuse and code reuse. An application framework embodies a particular philosophy for structuring an application, and in return for a large mass of prebuilt functionality, the programmer gives up control over many architectural-design decisions. Ray Valdes 3

什么是 Application Framework?Framework 这个字眼有组织 框架 体制的意思,Application Framework 不仅是一般性的泛称, 它其实还是对象导向领域中的一个专有名词 基本上你可以说,Application Framework 是一个完整的程序模型, 具备标准应用软件所需的一切基本功能, 像是档案存取 打印预视 数据交换..., 以及这些功能的使用接口 ( 工具列 状态列 选单 对话盒 ) 如果更以术语来说,Application Framework 就是由一整组合作无间的 " 对象 " 架构起来的大模型 喔不不, 当它还没有与你的程序产生火花的时候, 它还只是有形无体, 应该说是一组合作无间的 " 类别 " 架构起来的大模型 侯捷 最后, 要强调的是, 笔者之所以用一个较长的篇幅来连载关于 " 文档 / 视图 " 结构的内容, 是因为 " 文档 / 视图 " 结构是 MFC 中结构最为复杂, 体系最为庞大, 而又最富有特色的部分, 其中涉及到应用 文档模板 文档 视图 SDI 窗口 MDI 框架窗口 MDI 子窗口等多种不同的类, 如果不了解这些类及其盘根错节的内部联系的话, 就不可能编写出高水平的文档 / 视图程序 当然, 学习 " 文档 / 视图 " 结构的意义还不只于其本身, 通过该架构的学习, 一步步领略 MFC 设计者的神功奥妙, 也将进一步增强我们自身对庞大程序框架的把握能力 一个优秀的程序员是可以写出一个个优秀函数的程序员, 而一个优秀的系统设计师则需从全局把握软件的架构, 分析和学习 " 文档 / 视图 " 结构相信将是我们成为系统设计师之旅的一个有利环节 4

第二讲文档模板 深入浅出 MFC 文档 / 视图架构之文档模板 1. 文档模板管理者类 CDocManager 在 " 文档 / 视图 " 架构的 MFC 程序中, 提供了文档模板管理者类 CDocManager, 由它管理应用程序所包含的文档模板 我们先看看这个类的声明 : ///////////////////////////////////////////////////////////////////////////// // CDocTemplate manager object class CDocManager : public CObject DECLARE_DYNAMIC(CDocManager) public: // Constructor CDocManager(); //Document functions virtual void AddDocTemplate(CDocTemplate* ptemplate); virtual POSITION GetFirstDocTemplatePosition() const; virtual CDocTemplate* GetNextDocTemplate(POSITION& pos) const; virtual void RegisterShellFileTypes(BOOL bcompat); void UnregisterShellFileTypes(); virtual CDocument* OpenDocumentFile(LPCTSTR lpszfilename); // open named file virtual BOOL SaveAllModified(); // save before exit virtual void CloseAllDocuments(BOOL bendsession); // close documents before exiting virtual int GetOpenDocumentCount(); // helper for standard commdlg dialogs virtual BOOL DoPromptFileName(CString& filename, UINT nidstitle, DWORD lflags, BOOL bopenfiledialog, CDocTemplate* ptemplate); //Commands // Advanced: process async DDE request virtual BOOL OnDDECommand(LPTSTR lpszcommand); virtual void OnFileNew(); virtual void OnFileOpen(); // Implementation protected: 5

CPtrList m_templatelist; int GetDocumentCount(); // helper to count number of total documents public: static CPtrList* pstaticlist; // for static CDocTemplate objects static BOOL bstaticinit; // TRUE during static initialization static CDocManager* pstaticdocmanager; // for static CDocTemplate objects public: virtual ~CDocManager(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; #endif ; 从上述代码可以看出,CDocManager 类维护一个 CPtrList 类型的链表 m_templatelist( 即文档模板链表, 实际上,MFC 的设计者在 MFC 的实现中大量使用了链表这种数据结构 ),CPtrList 类型定义为 : class CPtrList : public CObject DECLARE_DYNAMIC(CPtrList) protected: struct CNode CNode* pnext; CNode* pprev; void* data; ; public: // Construction CPtrList(int nblocksize = 10); // Attributes (head and tail) // count of elements int GetCount() const; BOOL IsEmpty() const; // peek at head or tail void*& GetHead(); void* GetHead() const; void*& GetTail(); 6

void* GetTail() const; // Operations // get head or tail (and remove it) - don't call on empty list! void* RemoveHead(); void* RemoveTail(); // add before head or after tail POSITION AddHead(void* newelement); POSITION AddTail(void* newelement); // add another list of elements before head or after tail void AddHead(CPtrList* pnewlist); void AddTail(CPtrList* pnewlist); // remove all elements void RemoveAll(); // iteration POSITION GetHeadPosition() const; POSITION GetTailPosition() const; void*& GetNext(POSITION& rposition); // return *Position++ void* GetNext(POSITION& rposition) const; // return *Position++ void*& GetPrev(POSITION& rposition); // return *Position-- void* GetPrev(POSITION& rposition) const; // return *Position-- // getting/modifying an element at a given position void*& GetAt(POSITION position); void* GetAt(POSITION position) const; void SetAt(POSITION pos, void* newelement); void RemoveAt(POSITION position); // inserting before or after a given position POSITION InsertBefore(POSITION position, void* newelement); POSITION InsertAfter(POSITION position, void* newelement); // helper functions (note: O(n) speed) POSITION Find(void* searchvalue, POSITION startafter = NULL) const; // defaults to starting at the HEAD // return NULL if not found POSITION FindIndex(int nindex) const; // get the 'nindex'th element (may return NULL) 7

// Implementation protected: CNode* m_pnodehead; CNode* m_pnodetail; int m_ncount; CNode* m_pnodefree; struct CPlex* m_pblocks; int m_nblocksize; CNode* NewNode(CNode*, CNode*); void FreeNode(CNode*); public: ~CPtrList(); #ifdef _DEBUG void Dump(CDumpContext&) const; void AssertValid() const; #endif // local typedefs for class templates typedef void* BASE_TYPE; typedef void* BASE_ARG_TYPE; ; 很显然,CPtrList 是对链表结构体 struct CNode CNode* pnext; CNode* pprev; void* data; ; 本身及其 GetNext GetPrev GetAt SetAt RemoveAt InsertBefore InsertAfter Find FindIndex 等各种操作的封装 作为一个抽象的链表类型,CPtrList 并未定义其中节点的具体类型, 而以一个 void 指针 (struct CNode 中的 void* data) 巧妙地实现了链表节点成员具体类型的 " 模板 " 化 很显然, 在 Visual C++6.0 开发的年代, C++ 语言所具有的语法特征 " 模板 " 仍然没有得到广泛的应用 而 CDocManager 类的成员函数 virtual void AddDocTemplate(CDocTemplate* ptemplate); virtual POSITION GetFirstDocTemplatePosition() const; virtual CDocTemplate* GetNextDocTemplate(POSITION& pos) const; 则完成对 m_templatelist 链表的添加及遍历操作的封装, 我们来看看这三个函数的源代码 : 8

void CDocManager::AddDocTemplate(CDocTemplate* ptemplate) if (ptemplate == NULL) if (pstaticlist!= NULL) POSITION pos = pstaticlist->getheadposition(); while (pos!= NULL) CDocTemplate* ptemplate = (CDocTemplate*)pStaticList->GetNext(pos); AddDocTemplate(pTemplate); delete pstaticlist; pstaticlist = NULL; bstaticinit = FALSE; else ASSERT_VALID(pTemplate); ASSERT(m_templateList.Find(pTemplate, NULL) == NULL);// must not be in list ptemplate->loadtemplate(); m_templatelist.addtail(ptemplate); POSITION CDocManager::GetFirstDocTemplatePosition() const return m_templatelist.getheadposition(); CDocTemplate* CDocManager::GetNextDocTemplate(POSITION& pos) const return (CDocTemplate*)m_templateList.GetNext(pos); 2. 文档模板类 CDocTemplate 文档模板类 CDocTemplate 是一个抽象基类 ( 这意味着不能直接用它来定义对象而必须用它的派生类 ), 它定义了文档模板的基本处理函数接口 对一个单文档界面程序, 需使用单文档模板类 CSingleDocTemplate, 而对于一个多文档界面程序, 需使用多文档模板类 CMultipleDocTemplate 我们首先来看看 CDocTemplate 类的声明 : class CDocTemplate : public CCmdTarget 9

DECLARE_DYNAMIC(CDocTemplate) // Constructors protected: CDocTemplate(UINT nidresource, CRuntimeClass* pdocclass, CRuntimeClass* pframeclass, CRuntimeClass* pviewclass); public: virtual void LoadTemplate(); // Attributes public: // setup for OLE containers void SetContainerInfo(UINT nidoleinplacecontainer); // setup for OLE servers void SetServerInfo(UINT nidoleembedding, UINT nidoleinplaceserver = 0, CRuntimeClass* poleframeclass = NULL, CRuntimeClass* poleviewclass = NULL); // iterating over open documents virtual POSITION GetFirstDocPosition() const = 0; virtual CDocument* GetNextDoc(POSITION& rpos) const = 0; // Operations public: virtual void AddDocument(CDocument* pdoc); // must override virtual void RemoveDocument(CDocument* pdoc); // must override enum DocStringIndex windowtitle, // default window title docname, // user visible name for default document filenewname, // user visible name for FileNew // for file based documents: filtername, // user visible name for FileOpen filterext, // user visible extension for FileOpen // for file based documents with Shell open support: regfiletypeid, // REGEDIT visible registered file type identifier regfiletypename, // Shell visible registered file type name ; virtual BOOL GetDocString(CString& rstring, enum DocStringIndex index) const; // get one of the info strings CFrameWnd* CreateOleFrame(CWnd* pparentwnd, CDocument* pdoc,bool bcreateview); 10

// Overridables public: enum Confidence noattempt, maybeattemptforeign, maybeattemptnative, yesattemptforeign, yesattemptnative, yesalreadyopen ; virtual Confidence MatchDocType(LPCTSTR lpszpathname,cdocument*& rpdocmatch); virtual CDocument* CreateNewDocument(); virtual CFrameWnd* CreateNewFrame(CDocument* pdoc, CFrameWnd* pother); virtual void InitialUpdateFrame(CFrameWnd* pframe, CDocument* pdoc,bool bmakevisible = TRUE); virtual BOOL SaveAllModified(); // for all documents virtual void CloseAllDocuments(BOOL bendsession); virtual CDocument* OpenDocumentFile( LPCTSTR lpszpathname, BOOL bmakevisible = TRUE) = 0; // open named file // if lpszpathname == NULL => create new file with this type virtual void SetDefaultTitle(CDocument* pdocument) = 0; // Implementation public: BOOL m_bautodelete; virtual ~CDocTemplate(); // back pointer to OLE or other server (NULL if none or disabled) CObject* m_pattachedfactory; // menu & accelerator resources for in-place container HMENU m_hmenuinplace; HACCEL m_haccelinplace; // menu & accelerator resource for server editing embedding HMENU m_hmenuembedding; HACCEL m_haccelembedding; // menu & accelerator resource for server editing in-place HMENU m_hmenuinplaceserver; HACCEL m_haccelinplaceserver; 11

#ifdef _DEBUG virtual void Dump(CDumpContext&) const; virtual void AssertValid() const; #endif virtual void OnIdle(); // for all documents virtual BOOL OnCmdMsg(UINT nid, int ncode, void* pextra,afx_cmdhandlerinfo* phandlerinfo); protected: UINT m_nidresource; // IDR_ for frame/menu/accel as well UINT m_nidserverresource; // IDR_ for OLE inplace frame/menu/accel UINT m_nidembeddingresource; // IDR_ for OLE open frame/menu/accel UINT m_nidcontainerresource; // IDR_ for container frame/menu/accel CRuntimeClass* m_pdocclass; // class for creating new documents CRuntimeClass* m_pframeclass; // class for creating new frames CRuntimeClass* m_pviewclass; // class for creating new views CRuntimeClass* m_poleframeclass; // class for creating in-place frame CRuntimeClass* m_poleviewclass; // class for creating in-place view ; CString m_strdocstrings; // '\n' separated names // The document names sub-strings are represented as _one_ string: // windowtitle\ndocname\n... (see DocStringIndex enum) 文档模板挂接了后面要介绍的文档 视图和框架窗口, 使得它们得以互相关联 通过文档模板, 程序确定了创建或打开一个文档时, 以什么样的视图和框架窗口来显示 文档模板依靠保存相互对应的文档 视图和框架窗口的 CRuntimeClass 对象指针来实现上述挂接, 这就是文档模板类中的成员变量 m_pdocclass m_pframeclass m_pviewclass 的由来 实际上, 对 m_pdocclass m_pframeclass m_pviewclass 的赋值在 CDocTemplate 类的构造函数中实施 : CDocTemplate::CDocTemplate(UINT nidresource, CRuntimeClass* pdocclass, CRuntimeClass* pframeclass, CRuntimeClass* pviewclass) ASSERT_VALID_IDR(nIDResource); ASSERT(pDocClass == NULL pdocclass->isderivedfrom(runtime_class(cdocument))); ASSERT(pFrameClass == NULL pframeclass->isderivedfrom(runtime_class(cframewnd))); ASSERT(pViewClass == NULL pviewclass->isderivedfrom(runtime_class(cview))); m_nidresource = nidresource; 12

m_nidserverresource = NULL; m_nidembeddingresource = NULL; m_nidcontainerresource = NULL; m_pdocclass = pdocclass; m_pframeclass = pframeclass; m_pviewclass = pviewclass; m_poleframeclass = NULL; m_poleviewclass = NULL; m_pattachedfactory = NULL; m_hmenuinplace = NULL; m_haccelinplace = NULL; m_hmenuembedding = NULL; m_haccelembedding = NULL; m_hmenuinplaceserver = NULL; m_haccelinplaceserver = NULL; // add to pstaticlist if constructed as static instead of on heap if (CDocManager::bStaticInit) m_bautodelete = FALSE; if (CDocManager::pStaticList == NULL) CDocManager::pStaticList = new CPtrList; if (CDocManager::pStaticDocManager == NULL) CDocManager::pStaticDocManager = new CDocManager; CDocManager::pStaticList->AddTail(this); else m_bautodelete = TRUE; // usually allocated on the heap LoadTemplate(); 文档模板类 CDocTemplate 还保存了它所支持的全部文档类的信息, 包括所支持文档的文件扩展名 文档在框架窗口中的名字 图标等 CDocTemplate 类的 AddDocument RemoveDocument 成员函数使得 CDocument* pdoc 参数所指向的文档归属于本文档模板 ( 通过将 this 指针赋值给 pdoc 所指向 CDocument 对象的 m_pdoctemplate 成员变量 ) 或脱离与本文档模板的关系 : void CDocTemplate::AddDocument(CDocument* pdoc) 13

ASSERT_VALID(pDoc); ASSERT(pDoc->m_pDocTemplate == NULL); // no template attached yet pdoc->m_pdoctemplate = this; void CDocTemplate::RemoveDocument(CDocument* pdoc) ASSERT_VALID(pDoc); ASSERT(pDoc->m_pDocTemplate == this); // must be attached to us pdoc->m_pdoctemplate = NULL; 而 CDocTemplate 类的 CreateNewDocument 成员函数则首先调用 CDocument 运行时类的 CreateObject 函数创建一个 CDocument 对象, 再调用 AddDocument 成员函数将其归属于本文档模板类 : CDocument* CDocTemplate::CreateNewDocument() // default implementation constructs one from CRuntimeClass if (m_pdocclass == NULL) TRACE0("Error: you must override CDocTemplate::CreateNewDocument.\n"); ASSERT(FALSE); return NULL; CDocument* pdocument = (CDocument*)m_pDocClass->CreateObject(); if (pdocument == NULL) TRACE1("Warning: Dynamic create of document type %hs failed.\n",m_pdocclass->m_lpszclassname); return NULL; ASSERT_KINDOF(CDocument, pdocument); AddDocument(pDocument); return pdocument; 文档类对象由文档模板类构造生成, 单文档模板类 CSingleDocTemplate 只能生成一个文档类对象, 并用成员变量 m_ponlydoc 指向该对象 ; 多文档模板类可以生成多个文档类对象, 用成员变量 m_doclist 指向文档对象组成的链表 CSingleDocTemplate 的构造函数 AddDocument 及 RemoveDocument 成员函数都在 CDocTemplate 类相应函数的基础上增加了对 m_ponlydoc 指针的处理 : CSingleDocTemplate::CSingleDocTemplate(UINT nidresource, 14

CRuntimeClass* pdocclass, CRuntimeClass* pframeclass, CRuntimeClass* pviewclass) : CDocTemplate(nIDResource, pdocclass, pframeclass, pviewclass) m_ponlydoc = NULL; void CSingleDocTemplate::AddDocument(CDocument* pdoc) ASSERT(m_pOnlyDoc == NULL); // one at a time please ASSERT_VALID(pDoc); CDocTemplate::AddDocument(pDoc); m_ponlydoc = pdoc; void CSingleDocTemplate::RemoveDocument(CDocument* pdoc) ASSERT(m_pOnlyDoc == pdoc); // must be this one ASSERT_VALID(pDoc); CDocTemplate::RemoveDocument(pDoc); m_ponlydoc = NULL; 同样,CMultiDocTemplate 类的相关函数也需要对 m_doclist 所指向的链表进行操作 ( 实际上 AddDocument 和 RemoveDocument 成员函数是文档模板管理其所包含文档的函数 ): // CMultiDocTemplate document management (a list of currently open documents) void CMultiDocTemplate::AddDocument(CDocument* pdoc) ASSERT_VALID(pDoc); CDocTemplate::AddDocument(pDoc); ASSERT(m_docList.Find(pDoc, NULL) == NULL); // must not be in list m_doclist.addtail(pdoc); void CMultiDocTemplate::RemoveDocument(CDocument* pdoc) ASSERT_VALID(pDoc); CDocTemplate::RemoveDocument(pDoc); m_doclist.removeat(m_doclist.find(pdoc)); 15

由于 CMultiDocTemplate 类可包含多个文档, 依靠其成员函数 GetFirstDocPosition 和 GetNextDoc 完成对文档链表 m_doclist 的遍历 : POSITION CMultiDocTemplate::GetFirstDocPosition() const return m_doclist.getheadposition(); CDocument* CMultiDocTemplate::GetNextDoc(POSITION& rpos) const return (CDocument*)m_docList.GetNext(rPos); 而 CSingleDocTemplate 的这两个函数实际上并无太大的意义, 仅仅是 MFC 要玩的某种 " 招数 ", 这个 " 招数 " 高明吗? 相信看完 MFC 的相关源代码后你或许不会这么认为, 实际上 CSingleDocTemplate 的 GetFirstDocPosition GetNextDoc 函数仅仅只能判断 m_ponlydoc 的是否为 NULL: POSITION CSingleDocTemplate::GetFirstDocPosition() const return (m_ponlydoc == NULL)? NULL : BEFORE_START_POSITION; CDocument* CSingleDocTemplate::GetNextDoc(POSITION& rpos) const CDocument* pdoc = NULL; if (rpos == BEFORE_START_POSITION) // first time through, return a real document ASSERT(m_pOnlyDoc!= NULL); pdoc = m_ponlydoc; rpos = NULL; // no more return pdoc; 笔者认为,MFC 的设计者们将 GetFirstDocPosition GetNextDoc 作为基类 CDocTemplate 的成员函数是不合理的, 一种更好的做法是将 GetFirstDocPosition GetNextDoc 移至 CMultiDocTemplate 派生类 CDocTemplate 还需完成对其对应文档的关闭与保存操作 : BOOL CDocTemplate::SaveAllModified() POSITION pos = GetFirstDocPosition(); 16

while (pos!= NULL) CDocument* pdoc = GetNextDoc(pos); if (!pdoc->savemodified()) return FALSE; return TRUE; void CDocTemplate::CloseAllDocuments(BOOL) POSITION pos = GetFirstDocPosition(); while (pos!= NULL) CDocument* pdoc = GetNextDoc(pos); pdoc->onclosedocument(); 前文我们提到, 由于 MFC 的设计者将 CSingleDocTemplate 和 CMultiDocTemplate 的行为未进行规范的区分, 它对仅仅对应一个文档的 CSingleDocTemplate 也提供了所谓的 GetFirstDocPosition GetNextDoc 遍历操作, 所以基类 CDocTemplate 的 SaveAllModified 和 CloseAllDocuments 函数 ( 都是遍历 ) 就可统一 CSingleDocTemplate 和 CMultiDocTemplate 两个本身并不相同类的 SaveAllModified 和 CloseAllDocuments 行为 ( 实际上, 对于 CSingleDocTemplate 而言,SaveAllModified 和 CloseAllDocuments 中的 "All" 是没有太大意义的 教室里有 1 个老师和 N 个同学, 老师可以对同学们说 " 所有同学 ", 而学生对老师说 " 所有老师 " 相信会被当成神经病 ) MFC 的设计者们特意使用了 " 将错就错 " 的方法意图简化 CSingleDocTemplate 和 CMultiDocTemplate 类的设计, 读者朋友可以不认同他们的做法 CDocTemplate 还提供了框架窗口的创建和初始化函数 : ///////////////////////////////////////////////////////////////////////////// // Default frame creation CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pdoc, CFrameWnd* pother) if (pdoc!= NULL) ASSERT_VALID(pDoc); // create a frame wired to the specified document ASSERT(m_nIDResource!= 0); // must have a resource ID to load from CCreateContext context; context.m_pcurrentframe = pother; context.m_pcurrentdoc = pdoc; context.m_pnewviewclass = m_pviewclass; context.m_pnewdoctemplate = this; 17

if (m_pframeclass == NULL) TRACE0("Error: you must override CDocTemplate::CreateNewFrame.\n"); ASSERT(FALSE); return NULL; CFrameWnd* pframe = (CFrameWnd*)m_pFrameClass->CreateObject(); if (pframe == NULL) TRACE1("Warning: Dynamic create of frame %hs failed.\n",m_pframeclass->m_lpszclassname); return NULL; ASSERT_KINDOF(CFrameWnd, pframe); if (context.m_pnewviewclass == NULL) TRACE0("Warning: creating frame with no default view.\n"); // create new from resource if (!pframe->loadframe(m_nidresource,ws_overlappedwindow FWS_ADDTOTITLE, // default frame styles NULL, &context)) TRACE0("Warning: CDocTemplate couldn't create a frame.\n"); // frame will be deleted in PostNcDestroy cleanup return NULL; // it worked! return pframe; void CDocTemplate::InitialUpdateFrame(CFrameWnd* pframe, CDocument* pdoc,bool bmakevisible) // just delagate to implementation in CFrameWnd pframe->initialupdateframe(pdoc, bmakevisible); 3. CWinApp 与 CDocManager/CDocTemplate 类 应用程序 CWinApp 类对象与 CDocManager 和 CDocTemplate 类的关系是 :CWinApp 对象中包含一个 CDocManager 指针类型的共有数据成员 m_pdocmanager,cwinapp::initinstance 函数调用 18

CWinApp::AddDocTemplate 函数向链表 m_templatelist 添加模板指针 ( 实际上是调用前文所述 CDocManager 的 AddDocTemplate 函数 ) 另外,CWinApp 也提供了 GetFirstDocTemplatePosition 和 GetNextDocTemplate 函数实现来对 m_templatelist 链表进行访问 ( 实际上也是调用了前文所述 CDocManager 的 GetFirstDocTemplatePosition GetNextDocTemplate 函数 ) 我们仅摘取 CWinApp 类声明的一小部分 : class CWinApp : public CWinThread CDocManager* m_pdocmanager; // Running Operations - to be done on a running application // Dealing with document templates void AddDocTemplate(CDocTemplate* ptemplate); POSITION GetFirstDocTemplatePosition() const; CDocTemplate* GetNextDocTemplate(POSITION& pos) const; // Dealing with files virtual CDocument* OpenDocumentFile(LPCTSTR lpszfilename); // open named file void CloseAllDocuments(BOOL bendsession); // close documents before exiting // Command Handlers protected: // map to the following for file new/open afx_msg void OnFileNew(); afx_msg void OnFileOpen(); int GetOpenDocumentCount(); ; 来看 CWinApp 派生类 CSDIExampleApp( 单文档 ) CMDIExampleApp( 多文档 ) 的 InitInstance 成员函数的例子 ( 仅仅摘取与文档模板相关的部分 ): BOOL CSDIExampleApp::InitInstance() CSingleDocTemplate* pdoctemplate; pdoctemplate = new CSingleDocTemplate(IDR_MAINFRAME,RUNTIME_CLASS(CSDIExampleDoc), RUNTIME_CLASS(CMainFrame), // main SDI frame window RUNTIME_CLASS(CSDIExampleView)); AddDocTemplate(pDocTemplate); return TRUE; 19

BOOL CMDIExampleApp::InitInstance() CMultiDocTemplate* pdoctemplate; pdoctemplate = new CMultiDocTemplate(IDR_MDIEXATYPE, RUNTIME_CLASS(CMDIExampleDoc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CMDIExampleView)); AddDocTemplate(pDocTemplate); 读者朋友, 看完本次连载, 也许您有许多不明白的地方, 这是正常的 因为其所讲解的内容与后续几次连载息息相关, 我们愈往后看, 就会愈加清晰 对于本次连载的内容, 您只需要建立基本的印象 最初的浅尝辄止是为了最终的深入脊髓! 我们试图对 MFC 的深层机理刨根究底," 拨开云雾见月明 " 的过程是艰辛的! 20

第三讲文档 深入浅出 MFC 文档 / 视图架构之文档 1 文档类 CDocument 在 " 文档 / 视图 " 架构的 MFC 程序中, 文档是一个 CDocument 派生对象, 它负责存储应用程序的数据, 并把这些信息提供给应用程序的其余部分 CDocument 类对文档的建立及归档提供支持并提供了应用程序用于控制其数据的接口, 类 CDocument 的声明如下 : ///////////////////////////////////////////////////////////////////////////// // class CDocument is the main document data abstraction class CDocument : public CCmdTarget DECLARE_DYNAMIC(CDocument) public: // Constructors CDocument(); // Attributes public: const CString& GetTitle() const; virtual void SetTitle(LPCTSTR lpsztitle); const CString& GetPathName() const; virtual void SetPathName(LPCTSTR lpszpathname, BOOL baddtomru = TRUE); CDocTemplate* GetDocTemplate() const; virtual BOOL IsModified(); virtual void SetModifiedFlag(BOOL bmodified = TRUE); // Operations void AddView(CView* pview); void RemoveView(CView* pview); virtual POSITION GetFirstViewPosition() const; virtual CView* GetNextView(POSITION& rposition) const; // Update Views (simple update - DAG only) void UpdateAllViews(CView* psender, LPARAM lhint = 0L, CObject* phint = NULL); // Overridables // Special notifications 21

virtual void OnChangedViewList(); // after Add or Remove view virtual void DeleteContents(); // delete doc items etc // File helpers virtual BOOL OnNewDocument(); virtual BOOL OnOpenDocument(LPCTSTR lpszpathname); virtual BOOL OnSaveDocument(LPCTSTR lpszpathname); virtual void OnCloseDocument(); virtual void ReportSaveLoadException(LPCTSTR lpszpathname, CException* e, BOOL bsaving, UINT nidpdefault); virtual CFile* GetFile(LPCTSTR lpszfilename, UINT nopenflags, CFileException* perror); virtual void ReleaseFile(CFile* pfile, BOOL babort); // advanced overridables, closing down frame/doc, etc. virtual BOOL CanCloseFrame(CFrameWnd* pframe); virtual BOOL SaveModified(); // return TRUE if ok to continue virtual void PreCloseFrame(CFrameWnd* pframe); // Implementation protected: // default implementation CString m_strtitle; CString m_strpathname; CDocTemplate* m_pdoctemplate; CPtrList m_viewlist; // list of views BOOL m_bmodified; // changed since last saved public: BOOL m_bautodelete; // TRUE => delete document when no more views BOOL m_bembedded; // TRUE => document is being created by OLE #ifdef _DEBUG virtual void Dump(CDumpContext&) const; virtual void AssertValid() const; #endif //_DEBUG virtual ~CDocument(); // implementation helpers virtual BOOL DoSave(LPCTSTR lpszpathname, BOOL breplace = TRUE); virtual BOOL DoFileSave(); virtual void UpdateFrameCounts(); void DisconnectViews(); void SendInitialUpdate(); 22

// overridables for implementation virtual HMENU GetDefaultMenu(); // get menu depending on state virtual HACCEL GetDefaultAccelerator(); virtual void OnIdle(); virtual void OnFinalRelease(); virtual BOOL OnCmdMsg(UINT nid, int ncode, void* pextra,afx_cmdhandlerinfo* phandlerinfo); friend class CDocTemplate; protected: // file menu commands //AFX_MSG(CDocument) afx_msg void OnFileClose(); afx_msg void OnFileSave(); afx_msg void OnFileSaveAs(); //AFX_MSG // mail enabling afx_msg void OnFileSendMail(); afx_msg void OnUpdateFileSendMail(CCmdUI* pcmdui); DECLARE_MESSAGE_MAP() ; 一个文档可以有多个视图, 每一个文档都维护一个与之相关视图的链表 (CptrList 类型的 m_viewlist 实例 ) CDocument::AddView 将一个视图连接到文档上, 并将视图的文档指针指向该文档 : void CDocument::AddView(CView* pview) ASSERT_VALID(pView); ASSERT(pView->m_pDocument == NULL); // must not be already attached ASSERT(m_viewList.Find(pView, NULL) == NULL); // must not be in list m_viewlist.addtail(pview); ASSERT(pView->m_pDocument == NULL); // must be un-attached pview->m_pdocument = this; OnChangedViewList(); // must be the last thing done to the document CDocument::RemoveView 则完成与 CDocument::AddView 相反的工作 : void CDocument::RemoveView(CView* pview) 23

ASSERT_VALID(pView); ASSERT(pView->m_pDocument == this); // must be attached to us m_viewlist.removeat(m_viewlist.find(pview)); pview->m_pdocument = NULL; OnChangedViewList(); // must be the last thing done to the document 从 CDocument::AddView 和 CDocument::RemoveView 函数可以看出, 在与文档关联的视图被移走或新加入时 CDocument::OnChangedViewList 将被调用 : void CDocument::OnChangedViewList() // if no more views on the document, delete ourself // not called if directly closing the document or terminating the app if (m_viewlist.isempty() && m_bautodelete) OnCloseDocument(); return; // update the frame counts as needed UpdateFrameCounts(); CDocument::DisconnectViews 将所有的视图都与文档 " 失连 ": void CDocument::DisconnectViews() while (!m_viewlist.isempty()) CView* pview = (CView*)m_viewList.RemoveHead(); ASSERT_VALID(pView); ASSERT_KINDOF(CView, pview); pview->m_pdocument = NULL; 实际上, 类 CDocument 对视图的管理与类 CDocManager 对文档模板的管理及 CDocTemplate 对文档的管理非常类似, 少不了的, 类 CDocument 中可遍历对应的视图 ( 出现 GetFirstXXX 和 GetNextXXX 两个函数 ): 24

POSITION CDocument::GetFirstViewPosition() const return m_viewlist.getheadposition(); CView* CDocument::GetNextView(POSITION& rposition) const ASSERT(rPosition!= BEFORE_START_POSITION); // use CDocument::GetFirstViewPosition instead! if (rposition == NULL) return NULL; // nothing left CView* pview = (CView*)m_viewList.GetNext(rPosition); ASSERT_KINDOF(CView, pview); return pview; CDocument::GetFile 和 CDocument::ReleaseFile 函数完成对参数 lpszfilename 指定文档的打开与关闭操作 : CFile* CDocument::GetFile(LPCTSTR lpszfilename, UINT nopenflags, CFileException* perror) CMirrorFile* pfile = new CMirrorFile; ASSERT(pFile!= NULL); if (!pfile->open(lpszfilename, nopenflags, perror)) delete pfile; pfile = NULL; return pfile; void CDocument::ReleaseFile(CFile* pfile, BOOL babort) ASSERT_KINDOF(CFile, pfile); if (babort) pfile->abort(); // will not throw an exception else pfile->close(); delete pfile; CDocument 类的 OnNewDocument OnOpenDocument OnSaveDocument 及 OnCloseDocument 25

这一组成员函数用于创建 打开 保存或关闭一个文档 在这一组函数中, 上面的 CDocument::GetFile 和 CDocument::ReleaseFile 两个函数得以调用 : BOOL CDocument::OnOpenDocument(LPCTSTR lpszpathname) if (IsModified()) TRACE0("Warning: OnOpenDocument replaces an unsaved document.\n"); CFileException fe; CFile* pfile = GetFile(lpszPathName, CFile::modeRead CFile::shareDenyWrite, &fe); if (pfile == NULL) ReportSaveLoadException(lpszPathName, &fe,false, AFX_IDP_FAILED_TO_OPEN_DOC); return FALSE; DeleteContents(); SetModifiedFlag(); // dirty during de-serialize CArchive loadarchive(pfile, CArchive::load CArchive::bNoFlushOnDelete); loadarchive.m_pdocument = this; loadarchive.m_bforceflat = FALSE; TRY CWaitCursor wait; if (pfile->getlength()!= 0) Serialize(loadArchive); // load me loadarchive.close(); ReleaseFile(pFile, FALSE); CATCH_ALL(e) ReleaseFile(pFile, TRUE); DeleteContents(); // remove failed contents TRY ReportSaveLoadException(lpszPathName, e,false, AFX_IDP_FAILED_TO_OPEN_DOC); END_TRY DELETE_EXCEPTION(e); 26

return FALSE; END_CATCH_ALL SetModifiedFlag(FALSE); // start off with unmodified return TRUE; 打开文档的函数 CDocument::OnOpenDocument 完成的工作包括如下几步 : (1) 打开文件对象 ; (2) 调用 DeleteDontents(); (3) 建立与此文件对象相关联的 CArchive 对象 ; (4) 调用应用程序文档对象的 Serialize() 函数 ; (5) 关闭 CArchive 对象 文件对象 BOOL CDocument::OnSaveDocument(LPCTSTR lpszpathname) CFileException fe; CFile* pfile = NULL; pfile = GetFile(lpszPathName, CFile::modeCreate CFile::modeReadWrite CFile::shareExclusive, &fe); if (pfile == NULL) ReportSaveLoadException(lpszPathName, &fe,true, AFX_IDP_INVALID_FILENAME); return FALSE; CArchive savearchive(pfile, CArchive::store CArchive::bNoFlushOnDelete); savearchive.m_pdocument = this; savearchive.m_bforceflat = FALSE; TRY CWaitCursor wait; Serialize(saveArchive); // save me savearchive.close(); ReleaseFile(pFile, FALSE); 27

CATCH_ALL(e) ReleaseFile(pFile, TRUE); TRY ReportSaveLoadException(lpszPathName, e,true, AFX_IDP_FAILED_TO_SAVE_DOC); END_TRY DELETE_EXCEPTION(e); return FALSE; END_CATCH_ALL SetModifiedFlag(FALSE); // back to unmodified return TRUE; // success 保存文档的函数 CDocument::OnSaveDocument 完成的工作包括如下几步 : (1) 创建或打开文件对象 ; (2) 建立相对应的 CArchive 对象 ; (3) 调用应用程序文档对象的序列化函数 Serialize(); (4) 关闭文件对象 CArchive 对象 ; (5) 设置文件未修改标志 void CDocument::OnCloseDocument() // must close all views now (no prompting) - usually destroys this // destroy all frames viewing this document // the last destroy may destroy us BOOL bautodelete = m_bautodelete; m_bautodelete = FALSE; // don't destroy document while closing views while (!m_viewlist.isempty()) // get frame attached to the view CView* pview = (CView*)m_viewList.GetHead(); ASSERT_VALID(pView); CFrameWnd* pframe = pview->getparentframe(); 28

ASSERT_VALID(pFrame); // and close it PreCloseFrame(pFrame); pframe->destroywindow(); // will destroy the view as well m_bautodelete = bautodelete; // clean up contents of document before destroying the document itself DeleteContents(); // delete the document if necessary if (m_bautodelete) delete this; CDocument::OnCloseDocument 函数的程序流程为 : (1) 通过文档对象所对应的视图, 得到显示该文档视图的框架窗口的指针 ; (2) 关闭并销毁这些框架窗口 ; (3) 判断文档对象的自动删除变量 m_bautodelete 是否为真, 如果为真, 则以 delete this 语句销毁文档对象本身 实际上, 真正实现文档存储和读取 ( 相对于磁盘 ) 的函数是 Serialize, 这个函数通常会被 CDocument 的派生类重载 ( 加入必要的代码, 用以保存对象的数据成员到 CArchive 对象以及从 CArchive 对象载入对象的数据成员状态 ): void CExampleDoc::Serialize(CArchive& ar) if (ar.isstoring()) // TODO: add storing code here ar << var1 << var2; else // TODO: add loading code here var2 >> var1 >> ar; 29

地球人都知道, 文档与视图进行通信的方式是调用文档类的 UpdateAllViews 函数 : void CDocument::UpdateAllViews(CView* psender, LPARAM lhint, CObject* phint) // walk through all views ASSERT(pSender == NULL!m_viewList.IsEmpty()); // must have views if sent by one of them POSITION pos = GetFirstViewPosition(); while (pos!= NULL) CView* pview = GetNextView(pos); ASSERT_VALID(pView); if (pview!= psender) pview->onupdate(psender, lhint, phint); UpdateAllViews 函数遍历视图列表, 对每个视图都调用其 OnUpdate 函数实现视图的更新显示 2. 文档的 OPEN/NEW 从连载 2 可以看出, 在应用程序类 CWinapp 的声明中包含文件的 New 和 Open 函数 : afx_msg void OnFileNew(); afx_msg void OnFileOpen(); 而在文档模板管理者类 CDocManager 中也包含文件的 New 和 Open 函数 : virtual void OnFileNew(); virtual void OnFileOpen(); virtual CDocument* OpenDocumentFile(LPCTSTR lpszfilename); // open named file 而文档模板类 CDocTemplate 也不例外 : virtual CDocument* OpenDocumentFile( LPCTSTR lpszpathname, BOOL bmakevisible = TRUE) = 0; // open named file // if lpszpathname == NULL => create new file with this type virtual CDocument* CreateNewDocument(); 复杂的是, 我们在 CDocument 类中再次看到了 New 和 Open 相关函数 : 30

virtual BOOL OnNewDocument(); virtual BOOL OnOpenDocument(LPCTSTR lpszpathname); 在这众多的函数中, 究竟文档的创建者和打开者是谁?" 文档 / 视图 " 框架程序 "File" 菜单上的 "New" 和 "Open" 命令究竟对应着怎样的函数调用行为? 这一切都使我们陷入迷惘! 实际上 " 文档 / 视图 " 框架程序新文档及其关联视图和框架窗口的创建是应用程序对象 文档模板 新创建的文档和新创建的框架窗口相互合作的结果 具体而言, 应用程序对象创建了文档模板 ; 文档模板则创建了文档及框架窗口 ; 框架窗口创建了视图 在用户按下 ID_FILE_OPEN 及 ID_FILE_NEW 菜单 ( 或工具栏 ) 命令后,CWinApp( 派生 ) 类的 OnFileNew OnFileOpen 函数首先被执行, 其进行的行为是选择合适的文档模板, 如图 3.1 所示 图 3.1 文档模板的选择 实际上, 图 3.1 中所示的 " 使用文件扩展名选择文档模板 " " 是一个文档模板吗?" 的行为都要借助于 CDocManager 类的相关函数, 因为只有 CDocManager 类才维护了文档模板的列表 CDocManager::OnFileNew 的行为可描述为 : void CDocManager::OnFileNew() if (m_templatelist.isempty())... return ; // 取第一个文档模板的指针 CDocTemplate *ptemplate = (CDocTemplate*)m_templateList.GetHead(); 31

if (m_templatelist.getcount() > 1) // 如果多于一个文档模板, 弹出对话框提示用户选择 CNewTypeDlg dlg(&m_templatelist); int nid = dlg.domodal(); if (nid == IDOK) ptemplate = dlg.m_pselectedtemplate; else return ; // none - cancel operation // 参数为 NULL 的时候 OpenDocument File 会新建一个文件 ptemplate->opendocumentfile(null); 之后, 文档模板类的 virtual CDocument* OpenDocumentFile(LPCTSTR lpszpathname, BOOL bmakevisible = TRUE) = 0 函数进行文档的创建工作, 如果 lpszpathname == NULL, 是文档 New 行为 ; 相反, 则是 Open 行为 在创建框架后, 文档模板根据是 Open 还是 New 行为分别调用 CDocument 的 OnOpenDocument OnNewDocument 函数 图 3.2 描述了整个过程 图 3.2 文档 框架窗口的创建顺序 而图 3.3 则给出了视图的创建过程 32

图 3.3 视图的创建顺序 图 3.1~3.3 既描述了文档 / 视图框架对 ID_FILE_OPEN 及 ID_FILE_NEW 命令的响应过程, 又描述了文档 框架窗口及视图的创建 的确, 是无法单独描述文档的 New 和 Open 行为的, 因为它和其他对象的创建交错纵横 相信, 随着我们进一步阅读后续连载, 会对上述过程有更清晰的认识 33

第四讲视图 深入浅出 MFC 文档 / 视图架构之视图 视图类 CView 在 MFC" 文档 / 视图 " 架构中,CView 类是所有视图类的基类, 它提供了用户自定义视图类的公共接口 在 " 文档 / 视图 " 架构中, 文档负责管理和维护数据 ; 而视图类则负责如下工作 : (1) 从文档类中将文档中的数据取出后显示给用户 ; (2) 接受用户对文档中数据的编辑和修改 ; (3) 将修改的结果反馈给文档类, 由文档类将修改后的内容保存到磁盘文件中 文档负责了数据真正在永久介质中的存储和读取工作, 视图呈现只是将文档中的数据以某种形式向用户呈现, 因此一个文档可对应多个视图 下面我们来看看 CView 类的声明 : class CView : public CWnd DECLARE_DYNAMIC(CView) // Constructors protected: CView(); // Attributes public: CDocument* GetDocument() const; // Operations public: // for standard printing setup (override OnPreparePrinting) BOOL DoPreparePrinting(CPrintInfo* pinfo); // Overridables public: virtual BOOL IsSelected(const CObject* pdocitem) const; // support for OLE // OLE scrolling support (used for drag/drop as well) virtual BOOL OnScroll(UINT nscrollcode, UINT npos, BOOL bdoscroll = TRUE); virtual BOOL OnScrollBy(CSize sizescroll, BOOL bdoscroll = TRUE); 34

// OLE drag/drop support virtual DROPEFFECT OnDragEnter(COleDataObject* pdataobject,dword dwkeystate, CPoint point); virtual DROPEFFECT OnDragOver(COleDataObject* pdataobject,dword dwkeystate, CPoint point); virtual void OnDragLeave(); virtual BOOL OnDrop(COleDataObject* pdataobject,dropeffect dropeffect, CPoint point); virtual DROPEFFECT OnDropEx(COleDataObject* pdataobject, DROPEFFECT dropdefault, DROPEFFECT droplist, CPoint point); virtual DROPEFFECT OnDragScroll(DWORD dwkeystate, CPoint point); virtual void OnPrepareDC(CDC* pdc, CPrintInfo* pinfo = NULL); virtual void OnInitialUpdate(); // called first time after construct protected: // Activation virtual void OnActivateView(BOOL bactivate, CView* pactivateview,cview* pdeactiveview); virtual void OnActivateFrame(UINT nstate, CFrameWnd* pframewnd); // General drawing/updating virtual void OnUpdate(CView* psender, LPARAM lhint, CObject* phint); virtual void OnDraw(CDC* pdc) = 0; // Printing support virtual BOOL OnPreparePrinting(CPrintInfo* pinfo); // must override to enable printing and print preview virtual void OnBeginPrinting(CDC* pdc, CPrintInfo* pinfo); virtual void OnPrint(CDC* pdc, CPrintInfo* pinfo); virtual void OnEndPrinting(CDC* pdc, CPrintInfo* pinfo); // Advanced: end print preview mode, move to point virtual void OnEndPrintPreview(CDC* pdc, CPrintInfo* pinfo, POINT point,cpreviewview* pview); // Implementation public: virtual ~CView(); #ifdef _DEBUG virtual void Dump(CDumpContext&) const; 35

virtual void AssertValid() const; #endif //_DEBUG // Advanced: for implementing custom print preview BOOL DoPrintPreview(UINT nidresource, CView* pprintview,cruntimeclass* ppreviewviewclass, CPrintPreviewState* pstate); virtual void CalcWindowRect(LPRECT lpclientrect,uint nadjusttype = adjustborder); virtual CScrollBar* GetScrollBarCtrl(int nbar) const; static CSplitterWnd* PASCAL GetParentSplitter(const CWnd* pwnd, BOOL banystate); protected: CDocument* m_pdocument; public: virtual BOOL OnCmdMsg(UINT nid, int ncode, void* pextra,afx_cmdhandlerinfo* phandlerinfo); protected: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); virtual void PostNcDestroy(); // friend classes that call protected CView overridables friend class CDocument; friend class CDocTemplate; friend class CPreviewView; friend class CFrameWnd; friend class CMDIFrameWnd; friend class CMDIChildWnd; friend class CSplitterWnd; friend class COleServerDoc; friend class CDocObjectServer; //AFX_MSG(CView) afx_msg int OnCreate(LPCREATESTRUCT lpcs); afx_msg void OnDestroy(); afx_msg void OnPaint(); afx_msg int OnMouseActivate(CWnd* pdesktopwnd, UINT nhittest, UINT message); // commands afx_msg void OnUpdateSplitCmd(CCmdUI* pcmdui); afx_msg BOOL OnSplitCmd(UINT nid); afx_msg void OnUpdateNextPaneMenu(CCmdUI* pcmdui); afx_msg BOOL OnNextPaneCmd(UINT nid); // not mapped commands - must be mapped in derived class 36

afx_msg void OnFilePrint(); afx_msg void OnFilePrintPreview(); //AFX_MSG DECLARE_MESSAGE_MAP() ; CView 类首先要维护文档与视图之间的关联, 它通过 CDocument* m_pdocument 保护性成员变量记录关联文档的指针, 并提供 CView::GetDocument 接口函数以使得应用程序可得到与视图关联的文档 而在 CView 类的析构函数中, 需将对应文档类视图列表中的本视图删除 : CView::~CView() if (m_pdocument!= NULL) m_pdocument->removeview(this); CView 中地位最重要的函数是 virtual void OnDraw(CDC* pdc) = 0; 从这个函数的声明可以看出, CView 是一个纯虚基类 这个函数必须被重载, 它通常执行如下步骤 : (1) 以 GetDocument() 函数获得视图对应文档的指针 ; (2) 读取对应文档中的数据 ; (3) 显示这些数据 以 MFC 向导建立的一个初始 " 文档 / 视图 " 架构工程将这样重载 OnDraw() 函数, 注意注释中的 "add draw code for native data here( 添加活动数据的绘制代码 )": ///////////////////////////////////////////////////////////////////////////// // CExampleView drawing void CExampleView::OnDraw(CDC* pdc) CExampleDoc* pdoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here CView::PreCreateWindow 负责 View 的初始化 : ///////////////////////////////////////////////////////////////////////////// // CView second phase construction - bind to document BOOL CView::PreCreateWindow(CREATESTRUCT & cs) ASSERT(cs.style & WS_CHILD); 37

if (cs.lpszclass == NULL) VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG)); cs.lpszclass = _afxwndframeorview; // COLOR_WINDOW background if (afxdata.bwin4 && (cs.style & WS_BORDER)) cs.dwexstyle = WS_EX_CLIENTEDGE; cs.style &= ~WS_BORDER; return TRUE; CView::OnUpdate 函数在文档的数据被改变的时候被调用 ( 即它被用来通知一个视图的关联文档的内容已经被修改 ), 它预示着我们需要重新绘制视图以显示变化后的数据 其中的 Invalidate(TRUE) 将整个窗口设置为需要重绘的无效区域, 它会产生 WM_PAINT 消息, 这样 OnDraw 将被调用 : void CView::OnUpdate(CView* psender, LPARAM /*lhint*/, CObject* /*phint*/) ASSERT(pSender!= this); UNUSED(pSender); // unused in release builds // invalidate the entire pane, erase background too Invalidate(TRUE); 假如文档中的数据发生了变化, 必须通知所有链接到该文档的视图, 这时候文档类的 UpdateAllViews 函数需要被调用 此外,CView 类包含一系列函数用于进行文档的打印及打印预览工作 : (1)CView::OnBeginPrinting 在打印工作开始时被调用, 用来分配 GDI 资源 ; (2)CView::OnPreparePrinting 函数在文档打印或者打印预览前被调用, 可用来初始化打印对话框 ; (3)CView::OnPrint 用来打印或打印预览文档 ; (4)CView::OnEndPrinting 函数在打印工作结束时被调用, 用以释放 GDI 资源 ; (5)CView::OnEndPrintPreview 在退出打印预览模式时被调用 CView 派生类 38

MFC 提供了丰富的 CView 派生类, 各种不同的派生类实现了对不同种类控件的支持, 以为用户提供多元化的显示界面 这些 CView 派生类包括 : (1)CScrollView: 提供滚动支持 ; (2)CCtrlView: 支持 tree list 和 rich edit 控件 ; (3)CDaoRecordView: 在 dialog-box 控件中显示数据库记录 ; (4)CEditView: 提供了一个简单的多行文本编辑器 ; (5)CFormView: 包含 dialog-box 控件, 可滚动, 基于对话框模板资源 ; (6)CListView: 支持 list 控件 ; (7)CRecordView: 在 dialog-box 控件中显示数据库记录 ; (8)CRichEditView: 支持 rich edit 控件 ; (9)CTreeView: 支持 tree 控件 其中,CRichEditView CTreeView 及 CListView 均继承自 CCtrlView 类 ;CFormView 继承自 CScrollView 类 ;CRecordView CDaoRecordView 则进一步继承自 CFormView 类 下图描述了 CView 类体系的继承关系 : 39

第五讲框架 深入浅出 MFC 文档 / 视图架构之框架 从前文可知, 在 MFC 中, 文档是真正的数据载体, 视图是文档的显示界面, 对应同一个文档, 可能 存在多个视图界面, 我们需要另外一种东东来将这些界面管理起来, 这个东东就是框架 MFC 创造框架类的初衷在于 : 把界面管理工作独立出来! 框架窗口为应用程序的用户界面提供结构框架, 它是应用程序的主窗口, 负责管理其包容的窗口 一个应用程序启动时会创建一个最顶层的框架窗口 MFC 提供二种类型的框架窗口 : 单文档窗口 SDI 和多文档窗口 MDI( 你可以认为对话框是另一种框架窗口 ) 单文档窗口一次只能打开一个文档框架窗口, 而多文档窗口应用程序中可以打开多个文档框架窗口, 即子窗口 (Child Window) 这些子窗口中的文档可以为同种类型, 也可以为不同类型 在 Visual C++ AppWizard 的第一个对话框中, 会让用户选择应用程序是基于单文档 多文档还是基于对话框的, 如图 5.1 图 5.1 在 AppWizard 中选择框架窗口 MFC 提供了三个类 CFrameWnd CMDIFrameWnd CMDIChildWnd 用于支持单文档窗口和多文档窗口, 这些类的层次结构如图 5.2 图 5.2 CFrameWnd CMDIFrameWnd CMDIChildWnd 类的层次 40

(1)CFrameWnd 类用于 SDI 应用程序的框架窗口,SDI 框架窗口既是应用程序的主框架窗口, 也是当前文档对应的视图的边框 ;CFrameWnd 类也作为 CMDIFrameWnd 和 CMDIChildWnd 类的父类, 而在基于 SDI 的应用程序中,AppWizard 会自动为我们添加一个继承自 CFrameWnd 类的 CMainFrame 类 CFrameWnd 类中重要的函数有 Create( 用于创建窗口 ) LoadFrame( 用于从资源文件中创建窗口 ) PreCreateWindow( 用于注册窗口类 ) 等 Create 函数第一个参数为窗口注册类名, 第二个参数为窗口标题, 其余几个参数指定了窗口的风格 大小 父窗口 菜单名等, 其源代码如下 : BOOL CFrameWnd::Create(LPCTSTR lpszclassname, LPCTSTR lpszwindowname, DWORD dwstyle, const RECT &rect, CWnd *pparentwnd, LPCTSTR lpszmenuname, DWORD dwexstyle, CCreateContext *pcontext) HMENU hmenu = NULL; if (lpszmenuname!= NULL) // load in a menu that will get destroyed when window gets destroyed HINSTANCE hinst = AfxFindResourceHandle(lpszMenuName, RT_MENU); if ((hmenu = ::LoadMenu(hInst, lpszmenuname)) == NULL) TRACE0("Warning: failed to load menu for CFrameWnd.\n"); PostNcDestroy(); // perhaps delete the C++ object return FALSE; m_strtitle = lpszwindowname; // save title for later if (!CreateEx(dwExStyle, lpszclassname, lpszwindowname, dwstyle, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, pparentwnd ->GetSafeHwnd(), hmenu, (LPVOID)pContext)) TRACE0("Warning: failed to create CFrameWnd.\n"); if (hmenu!= NULL) DestroyMenu(hMenu); return FALSE; return TRUE; LoadFrame 函数用于从资源文件中创建窗口, 我们通常只需要给其指定一个参数,LoadFrame 使用该参数从资源中获取主边框窗口的标题 图标 菜单 加速键等, 其源代码为 : BOOL CFrameWnd::LoadFrame(UINT nidresource, DWORD dwdefaultstyle, CWnd *pparentwnd, CCreateContext *pcontext) 41

// only do this once ASSERT_VALID_IDR(nIDResource); ASSERT(m_nIDHelp == 0 m_nidhelp == nidresource); m_nidhelp = nidresource; // ID for help context (+HID_BASE_RESOURCE) CString strfullstring; if (strfullstring.loadstring(nidresource)) AfxExtractSubString(m_strTitle, strfullstring, 0); // first sub-string VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG)); // attempt to create the window LPCTSTR lpszclass = GetIconWndClass(dwDefaultStyle, nidresource); LPCTSTR lpsztitle = m_strtitle; if (!Create(lpszClass, lpsztitle, dwdefaultstyle, rectdefault, pparentwnd, MAKEINTRESOURCE(nIDResource), 0L, pcontext)) return FALSE; // will self destruct on failure normally // save the default menu handle ASSERT(m_hWnd!= NULL); m_hmenudefault = ::GetMenu(m_hWnd); // load accelerator resource LoadAccelTable(MAKEINTRESOURCE(nIDResource)); if (pcontext == NULL) // send initial update SendMessageToDescendants(WM_INITIALUPDATE, 0, 0, TRUE, TRUE); return TRUE; 在 SDI 程序中, 如果需要修改窗口的默认风格, 程序员需要修改 CMainFrame 类的 PreCreateWindow 函数, 因为 AppWizard 给我们生成的 CMainFrame::PreCreateWindow 仅对其基类的 PreCreateWindow 函数进行调用,CFrameWnd::PreCreateWindow 的源代码如下 : BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT &cs) if (cs.lpszclass == NULL) 42

VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG)); cs.lpszclass = _afxwndframeorview; // COLOR_WINDOW background if ((cs.style &FWS_ADDTOTITLE) && afxdata.bwin4)cs.style = FWS_PREFIXTITLE; if (afxdata.bwin4) cs.dwexstyle = WS_EX_CLIENTEDGE; return TRUE; (2)CMDIFrameWnd 类用于 MDI 应用程序的主框架窗口, 主框架窗口是所有 MDI 文档子窗口的容器, 并与子窗口共享菜单 ;CMDIFrameWnd 类相较 CFrameWnd 类增加的重要函数有 :MDIActivate( 激活另一个 MDI 子窗口 ) MDIGetActive( 得到目前的活动子窗口 ) MDIMaximize( 最大化一个子窗口 ) MDINext( 激活目前活动子窗口的下一子窗口并将当前活动子窗口排入所有子窗口末尾 ) MDIRestore( 还原 MDI 子窗口 ) MDISetMenu( 设置 MDI 子窗口对应的菜单 ) MDITile( 平铺子窗口 ) MDICascade ( 重叠子窗口 ) Visual C++ 开发环境是典型的 MDI 程序, 其执行 MDI Cascade 的效果如图 5.3 图 5.3 MDI Cascade 的效果 而执行 MDI Tile 的效果则如图 5.4 43

图 5.4 MDI Tile 的效果 MDISetMenu 函数的重要意义体现在一个 MDI 程序可以为不同类型的文档 ( 与文档模板关联 ) 显示不同的菜单, 例如下面的这个函数 Load 一个菜单, 并将目前主窗口的菜单替换为该菜单 : void CMdiView::OnReplaceMenu() // Load a new menu resource named IDR_SHORT_MENU CMdiDoc* pdoc = GetDocument(); pdoc->m_defaultmenu = ::LoadMenu(AfxGetResourceHandle(), MAKEINTRESOURCE(IDR_SHORT_MENU)); if (pdoc->m_defaultmenu == NULL) return; // Get the parent window of this view window. The parent window is // a CMDIChildWnd-derived class. We can then obtain the MDI parent // frame window using the CMDIChildWnd*. Then, replace the current // menu bar with the new loaded menu resource. CMDIFrameWnd* frame = ((CMDIChildWnd *) GetParent())->GetMDIFrame(); frame->mdisetmenu(cmenu::fromhandle(pdoc->m_defaultmenu), NULL); frame->drawmenubar(); CMDIFrameWnd 类另一个不讲 " 不足以服众 " 的函数是 OnCreateClient, 它是子框架窗口的创造者, 其实现如下 : BOOL CMDIFrameWnd::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext*) CMenu* pmenu = NULL; if (m_hmenudefault == NULL) 44

// default implementation for MFC V1 backward compatibility pmenu = GetMenu(); ASSERT(pMenu!= NULL); // This is attempting to guess which sub-menu is the Window menu. // The Windows user interface guidelines say that the right-most // menu on the menu bar should be Help and Window should be one // to the left of that. int imenu = pmenu->getmenuitemcount() - 2; // If this assertion fails, your menu bar does not follow the guidelines // so you will have to override this function and call CreateClient // appropriately or use the MFC V2 MDI functionality. ASSERT(iMenu >= 0); pmenu = pmenu->getsubmenu(imenu); ASSERT(pMenu!= NULL); return CreateClient(lpcs, pmenu); 从 CMDIFrameWnd::OnCreateClient 的源代码可以看出, 其中真正起核心作用的是对函数 CreateClient 的调用 : BOOL CMDIFrameWnd::CreateClient(LPCREATESTRUCT lpcreatestruct, CMenu* pwindowmenu) ASSERT(m_hWnd!= NULL); ASSERT(m_hWndMDIClient == NULL); DWORD dwstyle = WS_VISIBLE WS_CHILD WS_BORDER WS_CLIPCHILDREN WS_CLIPSIBLINGS MDIS_ALLCHILDSTYLES; // allow children to be created invisible DWORD dwexstyle = 0; // will be inset by the frame if (afxdata.bwin4) // special styles for 3d effect on Win4 dwstyle &= ~WS_BORDER; dwexstyle = WS_EX_CLIENTEDGE; CLIENTCREATESTRUCT ccs; ccs.hwindowmenu = pwindowmenu->getsafehmenu(); // set hwindowmenu for MFC V1 backward compatibility // for MFC V2, window menu will be set in OnMDIActivate 45

ccs.idfirstchild = AFX_IDM_FIRST_MDICHILD; if (lpcreatestruct->style & (WS_HSCROLL WS_VSCROLL)) // parent MDIFrame's scroll styles move to the MDICLIENT dwstyle = (lpcreatestruct->style & (WS_HSCROLL WS_VSCROLL)); // fast way to turn off the scrollbar bits (without a resize) ModifyStyle(WS_HSCROLL WS_VSCROLL, 0, SWP_NOREDRAW SWP_FRAMECHANGED); // Create MDICLIENT control with special IDC if ((m_hwndmdiclient = ::CreateWindowEx(dwExStyle, _T("mdiclient"), NULL, dwstyle, 0, 0, 0, 0, m_hwnd, (HMENU)AFX_IDW_PANE_FIRST, AfxGetInstanceHandle(), (LPVOID)&ccs)) == NULL) TRACE(_T("Warning: CMDIFrameWnd::OnCreateClient: failed to create MDICLIENT.") _T(" GetLastError returns 0x%8.8X\n"), ::GetLastError()); return FALSE; // Move it to the top of z-order ::BringWindowToTop(m_hWndMDIClient); return TRUE; (3)CMDIChildWnd 类用于在 MDI 主框架窗口中显示打开的文档 每个视图都有一个对应的子框架窗口, 子框架窗口包含在主框架窗口中, 并使用主框架窗口的菜单 CMDIChildWnd 类的一个重要函数 GetMDIFrame() 返回目前 MDI 客户窗口的父窗口, 其实现如下 : CMDIFrameWnd *CMDIChildWnd::GetMDIFrame() HWND hwndmdiclient = ::GetParent(m_hWnd); CMDIFrameWnd *pmdiframe; pmdiframe = (CMDIFrameWnd*)CWnd::FromHandle(::GetParent(hWndMDIClient)); return pmdiframe; 利用 AppWizard 生成的名为 "example" 的 MDI 工程包含如图 5.5 所示的类 46

图 5.5 一个 MDI 工程包含的类 其中的 CMainFrame 继承自 CMDIFrameWnd,CChildFrame 类继承自 CMDIChildWnd 类, CExampleView 视图类则负责在 CMDIChildWnd 类对应的子框架窗口中显示文档的数据 文中只是对 CMDIFrameWnd 的 CreateClient 成员函数进行了介绍, 实际上,CFrameWnd CMDIChildWnd 均包含 CreateClient 成员函数 我们经常通过重载 CFrameWnd:: CreateClient CMDIChildWnd:: CreateClient 函数的方法来实现 " 窗口分割 ", 例如 : BOOL CChildFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext *pcontext) if (!m_wndsplitter.create(this, 2, 2, // 分割的行 列数 CSize(10, 10), // 最小化尺寸 pcontext)) TRACE0(" 创建分割失败 "); return FALSE; return TRUE; 47

第六讲模板 文档 视图 框架的关系及消息流动机 深入浅出 MFC 文档 / 视图架构之相互关系 1 模板 文档 视图 框架的关系 连载 1~5 我们各个击破地讲解了文档 文档模板 视图和框架类, 连载 1 已经强调这些类有着亲密的内部联系, 总结 1~5 我们可以概括其联系为 : (1) 文档保留该文档的视图列表和指向创建该文档的文档模板的指针 ; 文档至少有一个相关联的视图, 而视图只能与一个文档相关联 (2) 视图保留指向其文档的指针, 并被包含在其父框架窗口中 ; (3) 文档框架窗口 ( 即包含视图的 MDI 子窗口 ) 保留指向其当前活动视图的指针 ; (4) 文档模板保留其已打开文档的列表, 维护框架窗口 文档及视图的映射 ; (5) 应用程序保留其文档模板的列表 我们可以通过一组函数让这些类之间相互可访问, 表 6-1 给出这些函数 表 6-1 文档 文档模板 视图和框架类的互相访问 从该对象全局函数应用文档文档模板视图文档框架窗口 MDI 框架窗口 如何访问其他对象调用全局函数 AfxGetApp 可以得到 CWinApp 应用类指针 AfxGetApp()->m_pMainWnd 为框架窗口指针 ; 用 CWinApp::GetFirstDocTemplatePostion CWinApp::GetNextDocTemplate 来遍历所有文档模板调用 CDocument::GetFirstViewPosition,CDocument::GetNextView 来遍历所有和文档关联的视图 ; 调用 CDocument:: GetDocTemplate 获取文档模板指针调用 CDocTemplate::GetFirstDocPosition CDocTemplate::GetNextDoc 来遍历所有对应文档调用 CView::GetDocument 得到对应的文档指针 ; 调用 CView::GetParentFrame 获取框架窗口调用 CFrameWnd::GetActiveView 获取当前得到当前活动视图指针 ; 调用 CFrameWnd::GetActiveDocument 获取附加到当前视图的文档指针调用 CMDIFrameWnd::MDIGetActive 获取当前活动的 MDI 子窗口 (CMDIChildWnd) 我们列举一个例子, 综合应用上表中的函数, 写一段代码, 它完成遍历文档模板 文档和视图的功能 : 48

CMyApp *pmyapp = (CMyApp*)AfxGetApp(); // 得到应用程序指针 POSITION p = pmyapp->getfirstdoctemplateposition();// 得到第 1 个文档模板 while (p!= NULL) // 遍历文档模板 CDocTemplate *pdoctemplate = pmyapp->getnextdoctemplate(p); POSITION p1 = pdoctemplate->getfirstdocposition();// 得到文档模板对应的第 1 个文档 while (p1!= NULL) // 遍历文档模板对应的文档 CDocument *pdocument = pdoctemplate->getnextdoc(p1); POSITION p2 = pdocument->getfirstviewposition(); // 得到文档对应的第 1 个视图 while (p2!= NULL) // 遍历文档对应的视图 CView *pview = pdocument->getnextview(p2); 由此可见, 下面的管理关系和实现途径都是完全类似的 : (1) 应用程序之于文档模板 ; (2) 文档模板之于文档 ; (3) 文档之于视图 图 6.1 6.2 分别给出了一个多文档 / 视图框架 MFC 程序的组成以及其中所包含类的层次关系 图 6.1 多文档 / 视图框架 MFC 程序的组成 49

图 6.2 文档 / 视图框架程序类的层次关系 关于文档和视图的关系, 我们可进一步细分为三类 : (1) 文档对应多个相同的视图对象, 每个视图对象在一个单独的 MDI 文档框架窗口中 ; (2) 文档对应多个相同类的视图对象, 但这些视图对象在同一文档框架窗口中 ( 通过 " 拆分窗口 " 即将单个文档窗口的视图空间拆分成多个单独的文档视图实现 ); (3) 文档对应多个不同类的视图对象, 这些视图对象仅在一个单独的 MDI 文档框架窗口中 在此模型中, 由不同的类构造成的多个视图共享单个框架窗口, 每个视图可提供查看同一文档的不同方式 例如, 一个视图以字处理模式显示文档, 而另一个视图则以 " 文档结构图 " 模式显示文档 图 6.3 显示了对应三种文档与视图关系应用程序的界面特点 50

图 6.3 文档 / 视图的三种关系 2. 消息流动机制 在基于 " 文档 / 视图 " 架构的 MFC 程序中, 用户消息 ( 鼠标 键盘输入等 ) 会先发往视图, 如果视图未处理则会发往框架窗口 所以, 一般来说, 消息映射宜定义在视图中 另外, 如果一个应用同时拥有多个视图而当前活动视图没有对消息进行处理则消息也会发往框架窗口 下面我们来看实例, 我们利用 Visual C++ 向导创建一个单文档 / 视图架构的 MFC 程序, 在其中增加一个菜单项为 " 自定义 "(ID 为 IDM_SELF, 如图 6.4) 51

图 6.4 含 " 自定义 " 菜单的单文档 / 视图架构 MFC 程序 我们分别在视图类和框架窗口类中为 " 自定义 " 菜单添加消息映射, 代码如下 : // 视图中的消息映射和处理函数 BEGIN_MESSAGE_MAP(CExampleView, CView) //AFX_MSG_MAP(CExampleView) ON_COMMAND(IDM_SELF, OnSelf) //AFX_MSG_MAP END_MESSAGE_MAP() void CExampleView::OnSelf() // TODO: Add your command handler code here AfxMessageBox(" 消息在视图中处理 "); // 框架窗口中的消息映射和处理函数 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) //AFX_MSG_MAP(CMainFrame) ON_COMMAND(IDM_SELF, OnSelf) //AFX_MSG_MAP END_MESSAGE_MAP() void CMainFrame::OnSelf() 52

// TODO: Add your command handler code here AfxMessageBox(" 消息在框架窗口中处理 "); 这时候, 我们单击 " 自定义 " 菜单, 弹出对话框显示 " 消息在视图中处理 "; 如果我们删除框架窗口中的消息映射, 再单击 " 自定义 " 菜单, 弹出对话框也显示 " 消息在视图中处理 "; 但是, 若我们将视图中的消息映射删除了, 就会显示 " 消息在框架窗口中处理 "! 这验证了我们关于消息处理顺序论述的正确性 欲深入理解消息流动过程, 还需认真分析 CFrameWnd::OnCmdMsg CView::OnCmdMsg 函数的源代码 : BOOL CFrameWnd::OnCmdMsg(UINT nid, int ncode, void* pextra, AFX_CMDHANDLERINFO* phandlerinfo) // pump through current view FIRST CView* pview = GetActiveView(); if (pview!= NULL && pview->oncmdmsg(nid, ncode, pextra, phandlerinfo)) return TRUE; // then pump through frame if (CWnd::OnCmdMsg(nID, ncode, pextra, phandlerinfo)) return TRUE; // last but not least, pump through app CWinApp* papp = AfxGetApp(); if (papp!= NULL && papp->oncmdmsg(nid, ncode, pextra, phandlerinfo)) return TRUE; return FALSE; BOOL CView::OnCmdMsg(UINT nid, int ncode, void* pextra, AFX_CMDHANDLERINFO* phandlerinfo) // first pump through pane if (CWnd::OnCmdMsg(nID, ncode, pextra, phandlerinfo)) return TRUE; // then pump through document BOOL bhandled = FALSE; if (m_pdocument!= NULL) // special state for saving view before routing to document _AFX_THREAD_STATE* pthreadstate = AfxGetThreadState(); 53

CView* poldroutingview = pthreadstate->m_proutingview; pthreadstate->m_proutingview = this; bhandled = m_pdocument->oncmdmsg(nid, ncode, pextra, phandlerinfo); pthreadstate->m_proutingview = poldroutingview; return bhandled; 分析上述源代码可知,WM_COMMAND 消息的实际流动顺序比前文叙述的 " 先视图, 后框架窗口 " 要复杂得多, 文档和应用程序都参与了消息的处理过程 如果我们再为文档和应用添加消息映射和处理函数 : // 文档的消息映射和处理函数 BEGIN_MESSAGE_MAP(CExampleDoc, CDocument) //AFX_MSG_MAP(CExampleDoc) ON_COMMAND(IDM_SELF, OnSelf) //AFX_MSG_MAP END_MESSAGE_MAP() void CExampleDoc::OnSelf() // TODO: Add your command handler code here AfxMessageBox(" 消息在文档中处理 "); // 应用的消息映射和处理函数 BEGIN_MESSAGE_MAP(CExampleApp, CWinApp) //AFX_MSG_MAP(CExampleApp) ON_COMMAND(IDM_SELF, OnSelf) //AFX_MSG_MAP END_MESSAGE_MAP() void CExampleApp::OnSelf() // TODO: Add your command handler code here AfxMessageBox(" 消息在应用中处理 "); 屏蔽掉视图和框架窗口的消息映射, 再单击 " 自定义 " 菜单, 弹出对话框显示 " 消息在文档中处理 "; 再屏蔽掉文档中的消息映射, 弹出对话框显示 " 消息在应用中处理 "! 由此可见, 完整的 WM_COMMAND 消息的处理顺序是 " 视图 文档 框架窗口 应用 "! 实际上, 关于 MFC 的消息流动是一个很复杂的议题, 陷于篇幅的原因, 我们不可能对其进行更详尽的介绍, 读者可自行寻找相关资料 54

第七讲实例剖析 深入浅出 MFC 文档 / 视图架构之实例剖析 为了能够把我们所学的所有知识都在实例中得以完整的体现, 我们来写一个尽可能复杂的 " 文档 / 视图 " 架构 MFC 程序, 这个程序复杂到 : (1) 是一个多文档 / 视图架构 MFC 程序 ; (2) 支持多种文件格式 ( 假设支持扩展名为 BMP 的位图和 TXT 的文本文件 ); (3) 一个文档 (BMP 格式 ) 对应多个不同类型的视图 ( 图形和二进制数据 ) 相信上述程序已经是一个包含 " 最复杂 " 特性的 " 文档 / 视图 " 架构 MFC 程序了, 搞定了这个包罗万象的程序, 还有什么简单的程序搞不定呢? 用 Visual C++ 工程向导创建一个名为 "Example" 的多文档 / 视图框架 MFC 程序, 最初的应用程序界面如图 7.1 图 7.1 最初的 Example 工程界面 这个时候的程序还不支持任何文档格式, 我们需让它支持 TXT( 由于本文的目的是讲解框架而非具体的读写文档与显示, 故将程序简化为只显示包含一行的 TXT 文件 ) 和 BMP 文件 55

定义 IDR_TEXTTYPE IDR_BMPTYPE 宏, 并在资源文件中增加对应 IDR_TEXTTYPE IDR_BMPTYPE 文档格式的字符串 : //NO_DEPENDENCIES // Microsoft Visual C++ generated include file. // Used by EXAMPLE.RC // #define IDD_ABOUTBOX 100 #define IDR_MAINFRAME 128 //#define IDR_EXAMPLTYPE 129 #define IDR_TEXTTYPE 10001 #define IDR_BMPTYPE 10002 #endif STRINGTABLE PRELOAD DISCARDABLE BEGIN IDR_MAINFRAME "Example" IDR_EXAMPLTYPE "\nexampl\nexampl\n\n\nexample.document\nexampl Document" IDR_TEXTTYPE "\ntext\ntext\nexampl 文件 (*.txt)\n.txt\ntext\ntext Document" IDR_BMPTYPE "\nbmp\nbmp\nexampl 文件 (*.bmp)\n.bmp\nbmp\nbmp Document" END 我们让第一个文档模板 ( 由 VC 向导生成 ) 对应 TXT 格式, 修改 CExampleApp::InitInstance 函数 : BOOL CExampleApp::InitInstance() CMultiDocTemplate* pdoctemplate; pdoctemplate = new CMultiDocTemplate( IDR_TEXTTYPE, // 对应文本文件的字符串 RUNTIME_CLASS(CExampleDoc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CExampleView)); AddDocTemplate(pDocTemplate); 为了让程序支持 TXT 文件的读取和显示, 我们需要重载 CexampleDoc 文档类和 CExampleView 视图类 因为从文档模板 new CMultiDocTemplate 中的参数可以看出,CExampleDoc 和 CExampleView 分别为对应 TXT 文件的文档类和视图类 : class CExampleDoc : public CDocument 56

CString m_text; // 在文档类中定义成员变量用于存储 TXT 文件中的字符串 // 重载文档类的 Serialize, 读取字符串到 m_text 中 void CExampleDoc::Serialize(CArchive& ar) if (ar.isstoring()) // TODO: add storing code here else // TODO: add loading code here ar.readstring(m_text); // 重载视图类的 OnDraw 函数, 显示文档中的字符串 ///////////////////////////////////////////////////////////////////////////// // CExampleView drawing void CExampleView::OnDraw(CDC* pdc) CExampleDoc* pdoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pdc->textout(0,0,pdoc->m_text); 这个时候的程序已经支持 TXT 文件了, 例如我们打开一个 TXT 文件, 将出现如图 7.2 的界面 图 7.2 打开 TXT 文件的界面 57

由于 CExampleDoc 和 CExampleView 支持的是对应 TXT 文件的文档类和视图类, 为了使程序支持 BMP 文件的显示, 我们还需要为 BMP 信建文档类 CBMPDoc 和视图类 CBMPView 在 example.cpp 中包含头文件 : #include "BMPDocument.h" #include "BMPView.h" 再在 CExampleApp::InitInstance 函数添加一个对应 BMP 格式的文档模板 : pdoctemplate = new CMultiDocTemplate( //IDR_EXAMPLTYPE, IDR_BMPTYPE, RUNTIME_CLASS(CBMPDocument), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CBMPView)); AddDocTemplate(pDocTemplate); 这个时候再点击程序的 " 新建 " 菜单, 将弹出如图 7.3 的对话框让用户选择新建文件的具体类型, 这就是在应用程序中包含多个文档模板后出现的现象 图 7.3 包含多个文档模板后的 " 新建 " 这个时候再点击 " 打开 " 菜单, 将弹出如图 7.4 的对话框让用户选择打开文件的具体类型, 这也是在应用程序中包含多个文档模板后出现的现象 58

图 7.4 包含多个文档模板后的 " 打开 " 对于新添加的视图类 CBMPView, 我们需要重载其 GetDocument() 函数 : class CBMPView : public CView CBMPDocument* GetDocument(); // 头文件中声明 // 重载 CBMPView::GetDocument 函数 CBMPDocument* CBMPView::GetDocument() ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CBMPDocument))); return (CBMPDocument*)m_pDocument; 而 CBMPView::OnDraw 则利用第三方类 CDib 来完成图形的绘制 : void CBMPView::OnDraw(CDC* pdc) CBMPDocument* pdoc = GetDocument(); // TODO: add draw code here CDib dib; dib.load(pdoc->getpathname()); dib.setpalette(pdc); dib.draw(pdc); 我们打开李连杰主演电影 霍元甲 的剧照, 将呈现如图 7.5 的界面, 这证明程序已经支持位图文件了 59

图 7.5 打开位图的界面 其实, 在这个程序中, 我们已经可以同时打开位图和文本文件了 ( 图 7.6) 图 7.6 同时打开位图和文本的界面 它已经是一个相当复杂的程序, 并已经具有如下两个特征 : 为多文档 / 视图架构 ; 支持多种文件格式 ( 扩展名为 BMP TXT) 而本节开头提出的第三个目标, 即一个文档 (BMP 格式 ) 对应多个不同类型的视图 ( 图形和二进制数据 ) 仍然没有实现 为了实现此目标, 我们需要用到 " 拆分窗口 " 了 我们需要修改类 CBMPDocument 使之读取出位图中的二进制数据 : class CBMPDocument : public CDocument 60

public: unsigned char bmpbit[max_bitmap]; void CBMPDocument::Serialize(CArchive& ar) if (ar.isstoring()) // TODO: add storing code here else // TODO: add loading code here CFile *file = ar.getfile(); for(int i=0;i<file->getlength();i++) ar >> bmpbit[i]; 程序中现有的子框架窗口类 ( 文档框架窗口类 )CChildFrame 并不支持窗口的拆分, 我们不能再沿用这个类来处理 BMP 文件了, 需要重新定义一个新的类 CBMPChildFrame 并通过重载其 CBMPChildFrame::OnCreateClient 函数来对其进行窗口拆分 : class CBMPChildFrame : public CMDIChildWnd public: CSplitterWnd m_wndsplitter; // 定义拆分 重载 CBMPChildFrame::OnCreateClient 函数 : BOOL CBMPChildFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext *pcontext) // TODO: Add your specialized code here and/or call the base class CRect rect; GetClientRect(&rect); m_wndsplitter.createstatic(this, 1, 2); m_wndsplitter.createview(0, 0, pcontext->m_pnewviewclass, CSize(rect.right /2, 0), 61

pcontext); m_wndsplitter.createview(0, 1, RUNTIME_CLASS(CBMPDataView), CSize(0, 0),pContext); m_wndsplitter.setactivepane(0, 0); return true; 上述代码将文档框架窗口一分为二 ( 分为一行二列 ), 第二个视图使用了 CBMPDataView 类 CBMPDataView 是我们新定义的一个视图类, 用来以 16 进制数字方式显示位图中的数据信息, 我们也需要为其重新定义 GetDocument 函数, 与 CBMPDocument 类中的定义完全相同 为了支持以二进制方式显示位图, 我们需要重载 CBMPDataView 类的 OnDraw 函数 这里也简化了, 仅仅显示 10 行 20 列数据 ( 前文已经提到, 我们的目的是讲解框架而非显示和读取文档的细节 ), 而且代码也不是很规范 ( 在程序中出现莫名其妙的数字一向是被鄙视的程序风格 ): void CBMPDataView::OnDraw(CDC* pdc) CBMPDocument* pdoc = GetDocument(); // TODO: add draw code here CString str; char tmp[3]; for(int i=0;i<20;i++)// 假设只显示 20 行, 每行 20 个字符 str = ""; for (int j =0;j<20;j++) memset(tmp,0,4); itoa(pdoc->bmpbit[10*i+j],tmp,16); str+=cstring(tmp)+" "; pdc->textout(0,20*i,str); 好的, 大功告成! 这个程序很牛了, 打开位图看看, 界面如图 7.7 打开位图后再打开文本, 界面如图 7.8, 成为一个 " 多视图 + 多文档 " 的界面 就这样, 我们逐步让这个实例程序具备了最复杂 MFC 程序的特征! 本系列文章的连载到此结束, 最后赠送广大研发人员一句话 : 无尽地学习, 乃是 IT 人的宿命, 付出努力, 终有回报! 62

图 7.7 用两种视图来显示位图的界面 图 7.8 " 多视图 + 多文档 " 的界面 63

实用技巧之一 利用 MFC 文档视图框架编写打印程序 本文首先介绍了利用 MFC 提供的文档视图框架来实现一个打印程序, 实现打印预览, 在此基础上, 同时通过对 MFC 源代码的深入探讨, 提出了利用该方法在对话框上用 MFC 实现打印功能, 结果表明, 利用 MFC 实现打印不仅方便, 而且功能很强大, 能够根据不同的需求很方便的打印出所需要的格式 本文还实现了一个在对话框中利用 MFC 实现打印功能的一个框架结构, 对于使用者只要使用该结构就可以按照自己的要求打印任何内容 关键词 :Visual C++,MFC, 对话框, 打印, 打印预览 引言 打印程序的编写在 windows 程序设计中非常有用, 针对不同的用户需要, 通常用 sdk 方式实现打印代码量比较大, 而且要对打印流程的底层有非常清楚的了解, 需要一个程序员有非常深入的打印方面的知识, 利用 MFC 提供的文档视图结构, 不但可以实现一些常用的标准界面元素, 把数据的处理的界面的处理分离出来, 而且其提供的打印功能更是方便快捷, 功能强大 打印程序的编写本质是是一种 GDI 绘图, 只是绘图的对象是在打印机的设备描述表, 如果对于屏幕的 GDI 绘图比较熟悉的读者, 相信掌握打印程序的编写应该比较容易 1 文档视图结构的程序的打印程序的编写 通常情况下, 一个结构组织的比较好的 MFC 程序是基于文档视图结构的, 这一框架结构给我们提供了很多功能, 比如菜单, 注册表的关联, 文件类型的注册, 打印功能, 只要我们善于发掘, 这些都可以为我们所用, 但我们现在只关心如何使用 MFC 提供的结构来实现打印功能 在编写打印程序之前, 有必要先介绍一下 MFC 的框架结构, 其中的文档视图结构又是这个框架的重点, 我们通过分析 MFC 实现的视图类的原代码就可以看到一个打印程序的执行流程 读者也可以看侯俊杰的 深入浅出 MFC, 上面有关于 MFC 打印的详细流程解释, 下面是 MFC 的打印的函数的实现, 该函数名为 OnFilePrint 它不是一个虚函数, 而是响应缺省的 COMMAND 消息的处理函数, 因为 MFC 提供了向导生成的菜单和工具栏, 关于打印的命令 ID 为 ID_FILE_PRINT, 而在视图类的 MessageMap 里有这样一项,ON_COMMAND (ID_FILE_PRINT, CView::OnFilePrint), 因此实际使用的过程中可以不用原来的 ID, 而使用自己的 ID 如 ID_MYPRINT, 再在 MessageMap 里加入 ON_COMMAND (ID_MYPRINT, CView::OnFilePrint) 即可完成原来一样的功能 ViewPrnt.cpp 中有 CView 的 OnFilePrint 的函数的具体实现,ViewPrnt.cpp 的位置读者自己用 windows 查找就能找到, 这是 MFC 的源代码, 本文把其中的主要代码列出放在下面, 直接看下面的分析 : void CView::OnFilePrint() // get default print info 64

if (OnPreparePrinting(&printInfo)) if (dlg.domodal()!= IDOK) return; OnBeginPrinting(&dcPrint, &printinfo); OnPrepareDC(&dcPrint, &printinfo); OnPrint(&dcPrint, &printinfo); OnEndPrinting(&dcPrint, &printinfo); // clean up after printing 其中加粗的代码行为可以重载的虚函数, 根据不同的用户, 其内容会不同 对于 OnPreparePrinting() 函数的具体内容必须有 return DoPreparePrinting(pInfo); 这是在一个打印过程中最先调用的 当然也可以包含一些其它的打印初始化操作 我们最主要的是要重载三个函数 : OnBeginPrinting(); OnPrint(); OnEndPrinting(); 而以 OnPrint 最为复杂, 它是我们要写大量代码实现我们打印功能的地方 对于默认的 OnPrint 实现是调用 CView 的 OnDraw, 也就是和绘制视图类的客户区的内容完全相同的方法来在打印机上绘图 实际中我们在两种地方绘图的内容是完全不同的, 可能用户在客户区绘的是一个曲线, 而在打印机上要绘制表格和数据 OnPrint(CDC* pdc, CPrintInfo* pinfo) 的第二个参数是一个 CPrintInfo 类型的指针, 我们可以从这个指针指向的对象中获得很多信息, 如总共的页数, 当前的页数, 这在打印页眉页脚时可能是很有用的信息 CPrintInfo 的定义如下 : struct structcprintinfo // Printing information structure CPrintInfo(); ~CPrintInfo(); CPrintDialog* m_ppd; // pointer to print dialog BOOL m_bdocobject; // TRUE if printing by IPrint interface BOOL m_bpreview; // TRUE if in preview mode BOOL m_bdirect; // TRUE if bypassing Print Dialog BOOL m_bcontinueprinting;// set to FALSE to prematurely end printing UINT m_ncurpage; // Current page UINT m_nnumpreviewpages; // Desired number of preview pages CString m_strpagedesc; // Format string for page number display LPVOID m_lpuserdata; // pointer to user created struct CRect m_rectdraw; // rectangle defining current usable page area // these only valid if m_bdocobject UINT m_noffsetpage; // offset of first page in combined IPrint job 65

DWORD m_dwflags; // flags passed to IPrint::Print void SetMinPage(UINT nminpage); void SetMaxPage(UINT nmaxpage); UINT GetMinPage() const; UINT GetMaxPage() const; UINT GetFromPage() const; UINT GetToPage() const; UINT GetOffsetPage() const; ; OnBeginPrinting() 通常用来设定要打印的总页数, 以及一些和页面尺寸有关的初始化工作, 在 OnBeginPrinting() 中设定打印的页数是必要的, 默认的页数是只有一页, 如果开发人员打印的页数大于 1, 则必须在此函数中设定打印的页数 然后在 OnPrint(CDC* pdc, CPrintInfo* pinfo) 中用 pinfo-> m_ncurpage 获取当前的页码, 根据当前的页码打印该页相应的内容 OnEndPrinting 用来释放在 OnBeginPrinting 中申请的资源, 如果没有申请, 则不需重载该函数 关于打印预览只需要将自己的执行打印预览功能的命令 ID 和 CView::OnFilePrintPreview 关联起来就行了, 具体方法是在用户的视图类的 MessageMap 中加入 :ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview); 其中 ID_FILE_PRINT_PREVIEW 是默认的 ID, 开发人员也可以使用自己的 ID 其实只要重载了 OnPrint 函数, 在打印和打印预览中就可以重用该函数了 到现在为止, 相信读者已经对利用 MFC 的文档视图结构来实现一个包含打印和打印预览功能的程序有了一个总体的认识了, 本文还针对该方法给出了一个示例代码, 代码来自 Jeff Prosise 的 MFC windows 程序设计, 见参考文献 [1] 2 没有文档视图结构的程序中利用 MFC 进行打印程序的编写 如果程序不是文档视图结构的, 我们要使用 MFC 来进行打印, 则可以通过建立一个虚拟的文档视图结构来进行打印, 其实 MFC 的打印的强大功能是在 CView 里提供的, 而 CView 类的对象是一个子窗口, 它必须是某一个框架窗口的子窗口, 而在对话框程序中, 我们只需要打印, 而不需要显示这个框架窗口和视图 我们以按下按钮 " 打印 " 来执行打印程序, 例如按钮为 ID 为 IDC_PNT, 消息相应函数为 OnPnt(), 即 : ON_BN_CLICKED(IDC_PNT, OnPnt); 需要在 OnPnt 中建立一个框架窗口, 同时使某个 CView 类的对象为该窗口的子窗口 因此笔者建立了两个类, 一个为框架窗口类 CPrintFrame, 另一个为 CPrintView, 具体的内容见示例代码 在新建一个用于打印的虚拟框架窗口时, 需要将执行打印的对话框的指针传给框架窗口, 这以便在对话框来响应 WM_BEGIN_PRINTING 和 WM_END_PRINTING 消息, 使对话框可以完成打印的初始化和释放操作 在执行一个打印任务时, 将打印的流程交给 CView 来进行, 而这个 CView 是虚拟的, 只是用来完成打印一些操作, 其它内容则完全不负责处理, 而当要执行 CView::OnPrint 时, 则又将处理的具体内容传回到对话框, 而对话框则只需要响应 WM_MY_PRINT 消息即可 : pframe->m_pcallerdlg->sendmessage(wm_my_print,(wparam) pdc, (LPARAM) 66

pinfo); 使打印的具体处理又传回到对话框中, 使开发人员根据具体的需要写 WM_MY_PRINT 的处理函数就可以实现打印, 而 CView::OnPrint(CDC* pdc, CPrintInfo* pinfo) 的参数也从 WM_MY_PRINT 的消息参数传出来, 在用户的对话框程序中, 需要写的代码就很少, 主要有以下几个步骤, 建立一个 CPrintFrame 的对象, 设该对象的指针为 pframe, 并将对话框的指针传给该对象的 m_pcallerdlg, 即 pframe->m_pcallerdlg = this; 调用对象的 Create 函数创建框架窗口 ; 例如 pframe->create(null," 频谱打印 ",WS_OVERLAPPEDWINDOW,CRect(0,0,0,0)); 如果要执行打印, 则调用 pframe->m_pview->onmyprint(); 如果要执行打印预览, 则调用 : pframe->m_pview->onmyprintpreview(); 例如 : void CDlgPrintDlg::OnPrint() // 执行打印功能 CPrintFrame *pframe = new CPrintFrame; pframe->m_pcallerdlg = this; pframe->create(null,"curve Print",WS_OVERLAPPEDWINDOW,CRect(0,0,0,0)); pframe->m_pview->onmyprint(); void CDlgPrintDlg::OnPrintPreview() // 执行打印预览功能 CPrintFrame *pframe = new CPrintFrame; pframe->m_pcallerdlg = this; pframe->create(null,"curve Print Preview",WS_OVERLAPPEDWINDOW,CRect(0,0,0,0)); pframe->m_pview->onmyprintpreview(); 在对话框中响应 WM_BEGIN_PRINTING, WM_END_PRINTING,WM_MY_PRINT 消息, 分别完成打印的初始化, 释放和具体的打印操作 ; 如在示例程序中添加了三个消息响应函数来执行该功能 ON_MESSAGE(WM_BEGIN_PRINTING,OnBeginPrinting) ON_MESSAGE(WM_END_PRINTING,OnEndPrinting) ON_MESSAGE(WM_MY_PRINT,OnMyPrint) 其中 OnMyPrint 是跟具体要打印什么内容有关的开发人员要重点完成的代码, 可以打印表格, 图片, 67

数据, 只要 GDI 绘图可以进行的操作在这里都可以完成 由于打印预览的一部分工作在 CView 类里完成, 因此在用户程序中只需要相应 WM_MY_PRINT 消息就可以执行打印预览的功能, 而不需要另外编写打印 预览代码 本文提供的 CPrintFrame 和 CPrintView 类是连个可重用的类, 开发者只需要把这两个类对应的四个文件拷贝到工程文件所在目录中 (PrintFrame.h, PringtView.h,PrintFrame.cpp, PrintView.cpp), 并将这四个文件加入工程, 并在需要执行打印功能的代码处加入 #include "PrintFrame.h" #include "PrintView.h" 然后按照上述 5 个步骤进行即可以实现一个功能完整的打印程序, 利用上述类实现对话框打印不但节省开发者许多时间, 而且功能很强大, 能达到很专业的水平, 但是该方法有一个缺点, 笔者发现如果开发者使用静态连接的 MFC 库时则会出错, 只适用于 Use MFC in a Shelled DLL 情况, 而且必须使程序为 Debug 版本的 3 示例代码的执行效果 图 1 执行打印功能的对话框 当按下打印预览后则会产生一个框架窗口, 显示打印预览的内容, 如图 2 所示 : 图 2 打印预览效果图 可以在上图的界面上按两页同时对两页预览, 如图 3 所示 : 图 3 两页同时预览效果图 但有一点需要注意, 在预览界面上的打印按钮不可用, 如果按该 " 打印 " 钮则直接等于将预览窗口关掉, 因此要执行打印功能必须另外在对话框的界面上放一个打印按钮, 如果执行了 " 打印 " 功能, 则会弹出一个选择打印机的对话框, 如图 4 所示 这个对话框是 MFC 的打印结构内置的, 不可以消除, 当用户选择了正确的打印机后则可以打印出具体的内容了 图 4 打印机选择对话框 4 结束语 本文从分析 MFC 的原代码入手, 利用 MFC 的 CView 类提供的打印和打印预览功能进行了在对话框中的打印和打印预览 利用面向对象的 C++ 写了两个可重用类 CPrintFrame 和 CPrintView, 实现在对话框中的打印和打印预览功能, 极大的简化了对话框打印程序的编写 68

实用技巧之二 用 VC++ 构建树视图控件 树视图控件具有层次分明 结构化强 美观 灵活等特点, 在各种操作系统中广为应用, 是人们最熟悉 最常应用的控件 从树视图控件出现到现在, 它们一直被认为非常复杂并难于编程, 与其它如编辑框 单选钮 复选框等控件进行比较, 要使其正常运行, 开发人员需要多做一些工作 然而, 在使用复杂的同时, 树视图控件又提供给开发人员更多的能力与空间 这里笔者就 VC++ 中树视图控件的编程使用作一些介绍 MFC 提供的树视图控件 CTreeCtrl 类用于封装树视图控件的功能, 同时它只是一个很 瘦 的包装器 它应用在对话框中或视图窗体中, 同其他控件一样, 可直接拖放到窗口中, 改变其位置 大小和一些基本属性 下面开始建立一个 CTreeCtrl, 步骤如下 : 1. 将 CtreeCtrl 拖到视图窗口中, 调整位置 大小, 并定义其对象标识为 IDC_TREE 2. 改变其属性, 选中 Has buttons Has lines 复选框, 这样用起树视图控件就同 Windows 中资源管理器中的一样了 3. 定义一个从 CtreelCtrl 继承的类 CNewTree, 在 MFC ClassWizard 中建立对新定义类的成员变量为 m_mytree, 以后程序中对该控件的控制通过此成员变量来实现 这么做是为了以后方便对其添加其他用户自定义的功能 做完以上几步, 我们就可以开始编写代码了 首先, 初始化树视图控件, 为其关联一个图像列表 ; 然后, 用 InsertItem 函数增加节点 在视图窗口 CMyFormView 中的 OnInitialUpdate() 事件中加入下面代码 : 同 CtreeView 相比,CtreeCtrl 是 CtreeView 的一个 轻巧 版本, 编程也相对简单 void CMyFormView::OnInitialUpdate() HICON hicon[7]; CImageList m_imagelist; m_imagelist.create(16,16,0,7,7); // 建立一个图像列表 m_imagelist.setbkcolor (RGB(255,255,255));hIcon[0]=AfxGetApp()- LoadIcon (IDI_ BMP0); hicon[1]=afxgetapp()- LoadIcon (IDI_ BMP1); hicon[6]=afxgetapp()- LoadIcon (IDI_ BMP6); for(int i=0;i =6;i++) 69

m_imagelist.add (hicon[i]); m_mytree.setimagelist (&&m_imagelist,tvsil_normal) // 为 m_mytree 设置一个图像列表, 使 CtreeCtrl 的不同节点显示不同的图标 HTREEITEM m_item m_item=m_mytree.insertitem ("Root",0,0,0,0); // 根节点的图标为 IDI_BMP0 if (m_item!=null) // 根节点建立成功 m_mytree.insertitem("subitem1",1,1,m_item) // 在根节点下建立一个子节点名为 SubItem1, 所显示的图标为 IDI_BMP1 同理, 可建立其它节点, 同一层次的节点显示相同的图标 CtreeCtrl 类没有提供节点查找的函数, 所以要求程序员自己编写特定条件的查找函数 通常点击不同节点所触发的事件是不同的, 此时, 要增加 OnSelchangedTree 事件 在 ClassWiard 窗口中, 选择 CmyFormView 类, 对象标识为 IDC_TREE, 消息为 TVN_SELCHANGED, 添加函数, 然后编辑代码 void CMyFormView::OnSelchangedTree(NMHDR pnmhdr, LRESULT presult) HTREEITEM SelItem; MyStructure ItemData; //MyStructure 为用户定义的结构类型 SelItem=m_MyTree.GetSelectedItem (); ItemData=GetItemData(SelItem); // 获得该节点的数据指针 Switch (ItemData- value1) case 0: // 用户指定的操作 case 1: 在实际编程中, 可能不仅仅是为了显示, 树视图控件上的每一个节点都对应特定的值, 所以要将指向具体数据的指针赋给对应的节点 具体做法是在用户自定义类 CNewTree 中新增一过程 SetValue (HTREEITEM) 具体代码如下: void CNewTree::SetValue(HTREEITEM Item_parm,int Value1,int value2..) 70

MyStructure ItemData ItemData= new MyStructure;; ItemData- value1=value1; ItemData- value2=value2; SetItemData(Item_parm,(DWORD)ItemData); 调用时, 传入对应的参数, 即可对给定的节点赋值 当然这里用了动态分配地址 new, 因此, 在程序结束前, 一定不要忘记删除这些空间 void CNewTree::DeleteData(HTREEITEM Item) MyStructure ItemData; ItemData=GetItemData(Item); // 获得该节点的数据指针 if (ItemData!=NULL) delete[] (char )ItemData; // 删除所占用的空间 根据树视图的结构特点, 我们采用递归遍历的方法来查找节点, 当然你可根据条件缩小遍历的范围 这里笔者以节点值匹配为条件, 编写自定义的函数 FindNode(), 返回第一个符合条件的节点的句柄, 具体代码如下 : HTREEITEM CNewTree::FindNode(HTREEITEM NodeItem, int &&NodeValue) MyStructure ItemData; HTREEITEM NextItem; if(nodeitem= =NULL) return NULL; // 递归出口 else while(nodeitem!=null) ItemData=GetItemData(NodeItem); If (ItemData- value1= =NodeValue) return NodeItem; NodeItem=GetChildItem(NodeItem); // 得到当前节点的第一个子节点的句柄 If(FindNode(NodeItem, NodeValue)= =NULL); // 递归查找 71

NodeItem=GetNextSiblingItem(NodeItem); // 得到当前节点的兄弟节点的句柄 到此为止, 笔者介绍了一些树视图控件编程方法, 包括树视图控件的建立 节点值的赋予和删除 查找 当然, 它应用的方面很广, 使用方法也很多 这里提供了构建树视图控件的基本框架, 在此基础上, 可进行扩展, 从而完成更强大的功能, 如同列表视图控件结合, 为其加上弹出式选单等等 感兴趣的读者不妨自己扩展该控件试试 72

实用技巧之三 用 MFC 创建通用窗体分割框架 目前基于分割视图的应用开发十分流行, 分割视图技术是在同一个框架窗口下同时显示多个视图的一 项技术 运用分割视图, 可以在较短时间内给用户更多的信息量, 从而使得用户界面更加的友好, 增强了软 件的可操作性 本文提出一个分割视图的通用创建框架 1. 分割视图创建框架 分割视图的创建大体上分为两个步骤 : 其一是创建分割窗体 ; 然后就是处理鼠标和键盘等消息 创建分割窗体 MFC 提供分割窗体类 CsplitterWnd, 它提供了很多对于分割窗体操作的成员函数, 每一个分割窗体都是一个 CsplitterWnd 的对象 本文提出的框架由于需要对定制的分割窗体进行扩充处理, 所以首先从 CsplitterWnd 继承一个子类 CFixSplitterWnd, 然后每个分割窗体是一个 CfixSplitterWnd 的对象, 这样以后只需要对 CfixSplitterWnd 进行改写后就可以增强分割窗体的功能 ( 后面将提出这种改写 ) 创建分割窗体最重要的函数是主框架类的 OnCreateClient 函数, 它将在主框架创建的时候调用, 本文将创建一个如下显示的分割窗体 : 则可以如下实现 : // 成员变量声明 CFixSplitterWnd m_wndsplitterh; // 用于横向切割 CFixSplitterWnd m_wndsplitterv; // 用于纵向切割 BOOL m_bcreatesplitter; // 分割窗体的实现 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pcontext) // 对整个主框架进行混合分割视图 73

BOOL bresult=m_wndsplitterv.createstatic(this,1,2); ASSERT(bResult); m_wndsplitterh.createstatic(&m_wndsplitterv,4,1,ws_child WS_VISIBLE,m_wndSplitterV.IdFromRowCol(0,1)); // 创建各自子窗片的对应的视图 m_wndsplitterv.createview(0,0,runtime_class(csceneview),csize(600,600),pcontext); m_wndsplitterh.createview(0,0,runtime_class(cpitchview),csize(100,100),pcontext); m_wndsplitterh.createview(1,0,runtime_class(cyawview),csize(100,100),pcontext); m_wndsplitterh.createview(2,0,runtime_class(crollview),csize(100,100),pcontext); m_wndsplitterh.createview(3,0,runtime_class(ccontrolview),csize(100,100),pcontext); // 设置窗格的初始化的大小 m_wndsplitterv.setrowinfo(0,ideal_rawheight,0); m_bcreatesplitter=true; // 激活 sceneview 使得其可以接受命令消息 m_wndsplitterv.setactivepane(0,0,null); return bresult; // 主框架窗体大小发生变化, 调节相应的窗体大小 void CMainFrame::OnSize(UINT ntype, int cx, int cy) CMDIFrameWnd::OnSize(nType, cx, cy); CRect rect; GetClientRect(rect); if (m_bcreatesplitter) m_wndsplitterv.setcolumninfo(0,rect.width() *3/4,10); m_wndsplitterv.setcolumninfo(1,rect.width() *1/4,10); m_wndsplitterh.setrowinfo(0,rect.height() /6,10); m_wndsplitterh.setrowinfo(1,rect.height() /6,10); m_wndsplitterh.setrowinfo(2,rect.height() /6,10); m_wndsplitterh.setrowinfo(3,rect.height()/2,10); m_wndsplitterv.recalclayout(); m_wndsplitterh.recalclayout(); 注意 :m_wndsplitterh.createview 中的第二个参数, 这个参数将分割窗体和相应的视图类相对应 通过上述的程序代码即可创建图 1 所示的分割窗体, 那么由于这里每个分割窗体都是一个 CfixSplitterWnd 对象, 所以可以通过改写 CfixSplitterWnd 类的虚函数或消息处理函数来完成自己特定的应用实现 ( 注意, 如果需要对定制有特定属性的分割窗体, 一定要派生自己的分割窗体类而不能是 MFC 的 74

CsplitterWnd 类 ) 这里我们需要分割窗体不能随鼠标拖动而改变其大小, 即所有窗格的大小都是一定的, 不能在运行时刻改变 所以必须在 CfixSplitterWnd 类的实现中加入如下代码 : void CFixSplitterWnd::OnMouseMove(UINT nflags, CPoint point) CWnd::OnMouseMove(nFlags, point); // 防止鼠标出现拖动状 // CSplitterWnd::OnMouseMove(nFlags, point); // 鼠标会在窗体边界出现拖动状 至此, 分割窗体已经创建完毕, 下面需要在分割窗体里处理消息 分割窗体处理消息 在分割窗体里处理消息和一般的文档视图模型处理消息大致一样, 但它也有其特殊之处 具体来说, 由于各个分割窗体已经与具体的视图类相联系了, 所以在需要处理各个分割窗体中的消息时, 可以直接到相应的视图类中进行处理 ; 另外, 多视图之间的切换会导致目标焦点之间的变更, 这样会影响菜单中与视图有关的命令的执行 比如在图 1 中所示的分割窗体中, 有一个 开始 命令必须是焦点在 CsceneView 视图上时才能执行, 否则就应该让该命令不能执行 ( 即该菜单呈现灰色 ), 则实现时可以首先对鼠标进行点击测试, 判断是否在 CsceneView 视图范围内, 如果是的话就允许执行, 否则就不允许执行 2. 结论 通过本文提出的分割视图创建框架, 可以满足对视图进行复杂控制的需求, 希望本文可以给大家一个启发, 从而能够创建更为完美的分割视图应用程序 75

实用技巧之四 MFC 应用程序框架入门 摘要 : 本文主要对 VC++ 6.0 的 MFC 编程方法及 MFC 应用程序框架进行简要介绍 关键词 : VC++6.0;MFC; 程序框架 1 MFC 概述 顾名思意,MFC 应用程序框架是以 MFC 作为框架基础的, 以此程序框架模式搭建起来的应用程序在程序结构组织上是完全不同于以前的 Win32 SDK 编程方式的 自 20 世纪 90 年代初问世以来,MFC 一直试图把 Windows API 函数封装到类库中个各个逻辑类中 MFC 的这种封装并非简单地对 API 函数进行分组与打包, 而是更多地通过类来试图实现全部的系统策略 随着越来越多系统功能的加入,MFC 的规模也在不断拓展, 目前已包括有 200 多个类, 涵盖了通用 Windows 类 文档 / 视框架 OLE 数据库 Internet 以及分布式功能等多方面的基本内容 这样一个坚实的程序开发基础无疑从很大程度上方便了程序设计人员对 Windows 程序的开发 MFC 提供了相当多不同功能的类以适合尽可能广泛的需求 这里绝大多数的 MFC 类都是直接或间接从 CObject 类派生出来的,CObject 类为其派生类提供了三个重要的特性支持 : 持久性 (Serialization) 支持 运行时 (Run-time) 类信息支持和诊断 (Diagnostic) 调试支持等 其中持久性是以流的方式将某个类对象中的持久性数据输出或输入到外部存储介质如磁盘文件等的过程 ; 运行时类信息 (Run-time Class Information,RTCI) 则可以重新获取一个对象的类名及其他一些有关对象在运行时的信息 RTCI 也是 C++ 中除运行时类型信息 (Run-time Type Information,RTTI) 机制外的另一个重要工具 ; 诊断和调试支持作为 CObject 类的一个组成部分, 可以在实现 CObject 派生类时执行有效性检查并可向调试窗口输出状态信息 并非 MFC 提供的所有函数都是类成员函数,MFC 也提供了一系列以 Afx 为前缀的全局函数 类成员函数只能在其所属类对象所在的上下文中使用, 但是这些 AFX 函数却可以在任何时候的任何地方直接使用 下表列出的是几个比较重要 AFX 函数 : 函数名 AfxAbout AfxBeginThread AfxEndThread AfxMessageBox AfxGetApp AfxGetAppName AfxGetMainWnd AfxGetInstanceHandle AfxRegisterWndClass 函数说明无条件终止一个应用程序 ; 通常在发生无法回复的错误时使用创建一个新的线程并开始执行终止当前正在执行的线程显示一个 Windows 消息窗口返回一个指向应用程序对象的指针返回应用程序名返回一个指向应用程序主窗口的指针返回一个标识当前应用程序实例的句柄为一个 MFC 应用程序注册一个用户自定义的窗口类 76

2 MFC 对 API 函数的封装 如果读者曾经有过 SDK 的开发经历, 一定会对其烦琐的编程方式和大量的 Win32 API 函数调用深有感触 所有不同功能的 API 函数均是以全局函数的形式放在一起的, 由于 API 函数数目比较庞大, 因此无论是学习还是使用都是有一定难度的 相比而言, 建立在 API 函数基础之上的 MFC 类库则通过把相关 API 函数的分类封装而可以大大简化编程的难度, 用 MFC 类编写的 Windows 应用程序完成相同的任务只需要进行少量的工作 众多的 API 函数根据功能的不同而被 MFC 封装到 200 多个类中, 这些类基本涵盖了进行 Windows 编程大部分可能用到的功能 由于封装后的 MFC 类太多, 这里不能一一介绍, 下面就以其中比较重要的 CObject 类和 CWnd 类为例对 API 函数的封装情况做一简要介绍 CObject 类是 MFC 中最主要也是最基本的类之一, 该类不支持多重继承, 派生的类只能有一个 CObject 基类 CObject 类是位于类层次结构最顶层的, 绝大多数 MFC 类都是从 CObject 类派生出来的 CObject 类包含了所有 MFC 类必须具备的几个基本功能 : 持久性支持 运行时类信息支持和诊断调试支持 其中持久性支持功能由成员函数 IsSerializable() 和 Serialize() 提供 前者用于检测对象是否支持序列化 如果一个类能够被序列化, 就必须在声明时包含 DECLARE_SERIAL 宏 在实现时包含 IMPLEMENT_SERIAL 宏 Serialize() 函数则可以将对象写入档案文件 (Archive) 或从档案文件读出对象 成员函数 GetRuntimeClass() 可以获取到一个指向 CruntimeClass 类对象的指针, 通过该指针可以得到对象的运行时类信息 CObject 类在诊断调试支持方面提供了成员函数 AssertValid() 和 Dump(), 前者可对对象内存状态的有效性进行检查, 后者负责将对象的内容转储到一个 CdumpContext 对象中, 并可以提供诊断服务及一些有用的调试信息 在 MFC 中,CWnd 类提供了所有窗口类的基本功能, 是一个非常重要的类, 大约三分之一的 MFC 类都是以此为基类 该类主要对创建 操纵窗口类的 API 函数进行了封装, 而且通过消息映射机制隐藏了 SDK 编程中使用相当不便的窗口处理函数, 是消息的分发处理更加方便 CWnd 类最重要的一个封装是对 API 函数 CreateWindow() 的封装, 该函数被封装为 CWnd 类成员函数 Create() 从 VC 提供的 MFC 源文件 WinCore.cpp 中可以清楚看出 CWnd 类对 CreateWindow() 函数的封装过程, 下面给出相关部分的实现清单 : BOOL CWnd::Create(LPCTSTR lpszclassname, LPCTSTR lpszwindowname, DWORD dwstyle, const RECT& rect, CWnd* pparentwnd, UINT nid, CCreateContext* pcontext) // can't use for desktop or pop-up windows (use CreateEx instead) ASSERT(pParentWnd!= NULL); ASSERT((dwStyle & WS_POPUP) == 0); return CreateEx(0, lpszclassname, lpszwindowname, dwstyle WS_CHILD, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, pparentwnd->getsafehwnd(), (HMENU)nID, (LPVOID)pContext); 可以看出, 主要工作是在 CreateEx() 成员函数中完成的, 而该函数又是对 API 函数 CreateWindowEx () 的封装 封装后的代码在调用 CreateWindowEx() 前构造并填充了一个非常类似于 WNDCLASS 结构的 CREATESTRUCT 结构, 并调用了 PreCreateWindow() 77

BOOL CWnd::CreateEx(DWORD dwexstyle, LPCTSTR lpszclassname, LPCTSTR lpszwindowname, DWORD dwstyle, int x, int y, int nwidth, int nheight, HWND hwndparent, HMENU nidorhmenu, LPVOID lpparam) // allow modification of several common create parameters CREATESTRUCT cs; cs.dwexstyle = dwexstyle; cs.lpszclass = lpszclassname; cs.lpszname = lpszwindowname; cs.style = dwstyle; cs.x = x; cs.y = y; cs.cx = nwidth; cs.cy = nheight; cs.hwndparent = hwndparent; cs.hmenu = nidorhmenu; cs.hinstance = AfxGetInstanceHandle(); cs.lpcreateparams = lpparam; if (!PreCreateWindow(cs)) PostNcDestroy(); return FALSE; AfxHookWindowCreate(this); HWND hwnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszclass, cs.lpszname, cs.style, cs.x, cs.y, cs.cx, cs.cy, cs.hwndparent, cs.hmenu, cs.hinstance, cs.lpcreateparams); #ifdef _DEBUG if (hwnd == NULL) TRACE1("Warning: Window creation failed: GetLastError returns 0x%8.8X\n", GetLastError()); #endif if (!AfxUnhookWindowCreate()) PostNcDestroy(); // cleanup if CreateWindowEx fails too soon if (hwnd == NULL) return FALSE; ASSERT(hWnd == m_hwnd); // should have been set in send msg hook return TRUE; 看上去经过封装的窗口创建函数要比原 API 函数复杂许多, 但这并不说明 MFC 的封装将导致编程的效率低下, 恰恰相反, 由于 CWnd 在绝大多数场合中是以基类的形式出现的, 因此可在派生类中添加代码完成对 CWnd::Create() 的调用而比较方便的实现对派生类窗口的创建 78

3 MFC 应用程序框架 MFC 应用程序框架可以看作是 MFC 基本类库的一个超集 (Superset), 类库是众多可在任何程序中使用的类的集合, 而应用程序框架则定义了程序自身的结构 下面给出一个使用了 MFC 应用程序框架的简单例子, 通过这段例程可以比较清楚地了解 MFC 应用程序框架的一般结构 // Sample01.h 文件 // 应用程序类 class CSample01App : public CWinApp public: virtual BOOL InitInstance(); ; // 框架窗口类 class CSample01Frame : public CFrameWnd public: CSample01Frame(); protected: afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() ; // Sample01.cpp 文件 #include <afxwin.h> #include "Sample01.h" // 应用程序对象 CSample01App theapp; // 初始化应用程序实例 BOOL CSample01App::InitInstance() m_pmainwnd = new CSample01Frame(); m_pmainwnd->showwindow(m_ncmdshow); m_pmainwnd->updatewindow(); return TRUE; // 消息映射 BEGIN_MESSAGE_MAP(CSample01Frame, CFrameWnd) ON_WM_PAINT() END_MESSAGE_MAP() // 构造函数 CSample01Frame::CSample01Frame() Create(NULL, "MFC 应用程序框架程序 "); 79

// WM_PAINT 消息响应函数 void CSample01Frame::OnPaint() CPaintDC dc(this); dc.textout(100, 100, "Hello World!"); 仍象编写 Sample00 程序一样建立一个 Win32 应用程序工程 Sample01( 配套程序见 " 光盘 \ 配套程序 \Sample01\"), 然后分别向工程添加头文件 Sample01.h 和源文件 Sample01.cpp, 并将上述代码写入相应的文件 为了能顺利编译, 还需要修改一下编译命令, 通过 "Alt+F7" 快捷键呼出 Project Settings 对话框, 在 Preprocessor definitions 栏的最后添加选项"_AFXDLL", 前面用逗号分隔 接下来还需要在 Project Options 栏的最后添加命令行"/MD", 用空格同其他命令行参数进行分隔 编译运行, 可以看出效果同 SDK 方式编写的 Sample00 程序是一样的, 但在代码实现上更加结构化, 编写过程也更加简单 接下来, 对上述应用程序框架代码进行分析 首先从 MFC 应用程序的核心 --CWinApp 类的派生类 CSample01App 谈起 CWinApp 类提供了可以获取消息并将获取到的消息分发到应用程序窗口的消息循环和一些关键的虚函数, 通过对这些虚函数的重载可使开发人员对应用程序的一些固有行为进行扩展 当把头文件 Afxwin.h 包含进来后, 就可以在程序中使用包括 CWinApp 在内的一些 MFC 类了 一个 MFC 应用程序有且只能有一个应用程序对象而且必须被以全局方式进行声明, 所以该对象自程序开始运行起就一直驻留在内存 由于使用了 MFC 应用程序框架的程序在本质上仍是 Windows 应用程序, 因此必然需要在程序中存在作为 Windows 应用程序入口的 WinMain() 函数 在前面的示例代码中之所以没有看到 WinMain() 函数是由于该函数已经通过封装的手段隐藏到应用程序框架中了 除 WinMain() 外,CWinApp 类成员函数 Run() 也是隐含执行的, 这个函数也是非常重要的, 它负责把消息放进应用程序窗口的消息循环中, 由 WinMain() 函数完成对 Run() 的调用 当 WinMain() 函数寻找到应用程序对象后将立即调用 CWinApp 类的虚函数 InitInstance() 由于 CWinApp 基类是不知道究竟需要何种主框架窗口的, 因此在使用时必须在 CWinApp 的派生类中对 InitInstance() 函数进行重载 InitInstance() 函数是在应用程序已经开始运行但窗口尚未创建时被调用的, 若非由 InitInstance() 创建了窗口, 应用程序是无法拥有窗口的, 这也就意味着缺少了 InitInstance() 函数的应用程序将无法接收 处理消息, 对 Windows 程序而言这也就失去了存在的意义 由此可见, 从 CWinApp 类中进行派生, 并且对 InitInstance() 函数进行重载是编写 MFC 应用程序框架程序的必要条件 除应用程序类外, 从 CFrameWnd 派生的 CSample01Frame 类还对应用程序的主框架窗口做了描述 在构造函数中调用基类的 CFrameWnd 成员函数 Create(), 由 Windows 负责创建出实际的窗口结构, 并由应用程序框架将其链接到 C++ 对象 本示例程序的大部分功能实际是在 MFC 的 CWinApp 和 CFrameWnd 等基类中完成的, 在编程时, 只需在派生类中编写少量功能代码,C++ 允许以这样的方式借用基类中的大量代码而无须复制代码 应用程序框架负责提供程序的结构框架, 开发人员在此基础上为其添加相应的实现代码, 从而可以非常方便地完成一个完整的应用程序 应用程序框架不仅定义了应用程序的结构安排, 实际上还包含了更多的 C++ 基类 80

4 小结 SDK 的 API 编程方法 MFC 的编程方法以及本系列讲座后面将要介绍的 ATL 编程方法是 VC++ 程序设计中比较常用的几种编程方法, 其中 MFC 以其强大的功能和灵活的编程方式而成为大多数程序开发人员最经常使用的一种编程方式 本文从基础问题入手对 MFC 及其框架程序做了较为详细的论述, 使读者能够对 MFC 编程有一个基本的认识 81