深入浅出MFC“文档/视图”架构(2)
――文档模板
作者:宋宝华 e-mail:21cnbao@21cn.com
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:
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);
…
}
读者朋友,看完本次连载,也许您有许多不明白的地方,这是正常的。因为其所讲解的内容与后续几次连载息息相关,我们愈往后看,就会愈加清晰。对于本次连载的内容,您只需要建立基本的印象。最初的浅尝辄止是为了最终的深入脊髓!
――文档模板
作者:宋宝华 e-mail:21cnbao@21cn.com
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:
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的深层机理刨根究底,“拨开云雾见月明”的过程是艰辛的
本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/120813,如需转载请自行联系原作者