VC 文档+视图 详细分析

                  深入浅出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所讲述的所有内容。

  本文所有的代码基于WIN32平台开发,调试环境为Visual C++6.0。在本文的连载过程中,您可以通过如下方式联系作者(热忱欢迎读者朋友对本文的内容提出质疑或给出修改意见):

  作者email:21cnbao@21cn.com(可以来信提问,笔者将力求予以回信解答);

  另外,对本文的转载请务必注明作者和出处。未经同意,不得用于任何形式的商业目的。

  架构

  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

  什么是Application Framework?Framework 这个字眼有组织、框架、体制的意思,Application Framework 不仅是一般性的泛称,它其实还是对象导向领域中的一个专有名词。 

  基本上你可以说,Application Framework 是一个完整的程序模型,具备标准应用软件所需的一切基本功能,像是档案存取、打印预视、数据交换...,以及这些功能的使用接口(工具列、状态列、选单、对话盒)。如果更以术语来说,Application Framework 就是由一整组合作无间的"对象"架构起来的大模型。喔不不,当它还没有与你的程序产生火花的时候,它还只是有形无体,应该说是一组合作无间的"类别"架构起来的大模型。

  ――侯捷


  最后,要强调的是,笔者之所以用一个较长的篇幅来连载关于"文档/视图"结构的内容,是因为"文档/视图"结构是MFC中结构最为复杂,体系最为庞大,而又最富有特色的部分,其中涉及到应用、文档模板、文档、视图、SDI窗口、MDI框架窗口、MDI子窗口等多种不同的类,如果不了解这些类及其盘根错节的内部联系的话,就不可能编写出高水平的文档/视图程序。当然,学习"文档/视图"结构的意义还不只于其本身,通过该架构的学习,一步步领略MFC设计者的神功奥妙,也将进一步增强我们自身对庞大程序框架的把握能力。一个优秀的程序员是可以写出一个个优秀函数的程序员,而一个优秀的系统设计师则需从全局把握软件的架构,分析和学习"文档/视图"结构相信将是我们成为系统设计师之旅的一个有利环节。

深入浅出MFC文档/视图架构之文档模板


  文档模板管理者类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:
  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();
  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)

  // 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链表的添加及遍历操作的封装,我们来看看这三个函数的源代码:

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
{
 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);

  // 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;

  #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;
 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)
{
 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,
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));
}

  由于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();
 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;

 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函数调用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;
}
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的深层机理刨根究底,"拨开云雾见月明"的过程是艰辛的!



深入浅出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
 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();

 // 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)
{
 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两个函数):

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这一组成员函数用于创建、打开、保存或关闭一个文档。在这一组函数中,上面的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);
  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);
 }
 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();
  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;
 }
}

  地球人都知道,文档与视图进行通信的方式是调用文档类的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相关函数:

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();
 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则给出了视图的创建过程。


图3.3视图的创建顺序

  图3.1~3.3既描述了文档/视图框架对ID_FILE_OPEN及ID_FILE_NEW命令的响应过程,又描述了文档、框架窗口及视图的创建。的确,是无法单独描述文档的New和Open行为的,因为它和其他对象的创建交错纵横。

  相信,随着我们进一步阅读后续连载,会对上述过程有更清晰的认识。



深入浅出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);

 // 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;
  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
  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);

 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派生类

  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类体系的继承关系:



深入浅出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类的层次

  (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)
{
 // 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)
 {
  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。


图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)
 {
  // 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
 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所示的类。


图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;
}







深入浅出MFC文档/视图架构之相互关系


  1、模板、文档、视图、框架的关系

  连载1~5我们各个击破地讲解了文档、文档模板、视图和框架类,连载1已经强调这些类有着亲密的内部联系,总结1~5我们可以概括其联系为:

  (1)文档保留该文档的视图列表和指向创建该文档的文档模板的指针;文档至少有一个相关联的视图,而视图只能与一个文档相关联。

  (2)视图保留指向其文档的指针,并被包含在其父框架窗口中;

  (3)文档框架窗口(即包含视图的MDI子窗口)保留指向其当前活动视图的指针;

  (4)文档模板保留其已打开文档的列表,维护框架窗口、文档及视图的映射;

  (5)应用程序保留其文档模板的列表。 

  我们可以通过一组函数让这些类之间相互可访问,表6-1给出这些函数。

  表6-1 文档、文档模板、视图和框架类的互相访问

从该对象如何访问其他对象
全局函数调用全局函数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 获取附加到当前视图的文档指针
MDI 框架窗口调用CMDIFrameWnd::MDIGetActive 获取当前活动的MDI子窗口(CMDIChildWnd)

  我们列举一个例子,综合应用上表中的函数,写一段代码,它完成遍历文档模板、文档和视图的功能:

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程序的组成


图6.2 文档/视图框架程序类的层次关系

  关于文档和视图的关系,我们可进一步细分为三类:

  (1)文档对应多个相同的视图对象,每个视图对象在一个单独的 MDI 文档框架窗口中;

  (2)文档对应多个相同类的视图对象,但这些视图对象在同一文档框架窗口中(通过"拆分窗口"即将单个文档窗口的视图空间拆分成多个单独的文档视图实现); 

  (3)文档对应多个不同类的视图对象,这些视图对象仅在一个单独的 MDI 文档框架窗口中。在此模型中,由不同的类构造成的多个视图共享单个框架窗口,每个视图可提供查看同一文档的不同方式。例如,一个视图以字处理模式显示文档,而另一个视图则以"文档结构图"模式显示文档。

  图6.3显示了对应三种文档与视图关系应用程序的界面特点。


图6.3文档/视图的三种关系

2. 消息流动机制

  在基于"文档/视图"架构的MFC程序中,用户消息(鼠标、键盘输入等)会先发往视图,如果视图未处理则会发往框架窗口。所以,一般来说,消息映射宜定义在视图中。另外,如果一个应用同时拥有多个视图而当前活动视图没有对消息进行处理则消息也会发往框架窗口。

  下面我们来看实例,我们利用Visual C++向导创建一个单文档/视图架构的MFC程序,在其中增加一个菜单项为"自定义"(ID为IDM_SELF,如图6.4)。


图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() 
{
 // 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();
  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的消息流动是一个很复杂的议题,陷于篇幅的原因,我们不可能对其进行更详尽的介绍,读者可自行寻找相关资料。


深入浅出MFC文档/视图架构之实例剖析


  为了能够把我们所学的所有知识都在实例中得以完整的体现,我们来写一个尽可能复杂的"文档/视图"架构MFC程序,这个程序复杂到:

  (1)是一个多文档/视图架构MFC程序;

  (2)支持多种文件格式(假设支持扩展名为BMP的位图和TXT的文本文件);

  (3)一个文档(BMP格式)对应多个不同类型的视图(图形和二进制数据)。

  相信上述程序已经是一个包含"最复杂"特性的"文档/视图"架构MFC程序了,搞定了这个包罗万象的程序,还有什么简单的程序搞不定呢?

  用Visual C++工程向导创建一个名为"Example"的多文档/视图框架MFC程序,最初的应用程序界面如图7.1。


图7.1 最初的Example工程界面

  这个时候的程序还不支持任何文档格式,我们需让它支持TXT(由于本文的目的是讲解框架而非具体的读写文档与显示,故将程序简化为只显示包含一行的TXT文件)和BMP文件。

  定义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
{
 …
 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文件的界面

由于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的对话框让用户选择打开文件的具体类型,这也是在应用程序中包含多个文档模板后出现的现象。


图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的界面,这证明程序已经支持位图文件了。


图7.5 打开位图的界面

  其实,在这个程序中,我们已经可以同时打开位图和文本文件了(图7.6)。


图7.6 同时打开位图和文本的界面

它已经是一个相当复杂的程序,并已经具有如下两个特征:为多文档/视图架构;支持多种文件格式(扩展名为BMP、TXT)。

  而本节开头提出的第三个目标,即一个文档(BMP格式)对应多个不同类型的视图(图形和二进制数据)仍然没有实现。为了实现此目标,我们需要用到"拆分窗口"了。

  我们需要修改类CBMPDocument使之读取出位图中的二进制数据:

class CBMPDocument : public CDocument
{
 …
 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), 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人的宿命,付出努力,终有回报!


图7.7 用两种视图来显示位图的界面

图7.8 "多视图+多文档"的界面

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页