珠海珠峰大道中学:·教程:轻松玩转MFC文档视图架构编程

来源:百度文库 编辑:中财网 时间:2024/05/11 04:59:07
 引言

  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设计者的神功奥妙,也将进一步增强我们自身对庞大程序框架的把握能力。一个优秀的程序员是可以写出一个个优秀函数的程序员,而一个优秀的系统设计师则需从全局把握软件的架构,分析和学习"文档/视图"结构相信将是我们成为系统设计师之旅的一个有利环节。
文档模板管理者类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);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

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


/////////////////////////////////////////////////////////////////////////////////////

视图类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提供二种类型的框架窗口:单文档窗口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;
}


///////////////////////////////////////////////////////////////////////////////////////


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文档/视图的三种关系

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

为了能够把我们所学的所有知识都在实例中得以完整的体现,我们来写一个尽可能复杂的"文档/视图"架构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文件的界面