MFC 文档/视图文档和增强功能






4.84/5 (51投票s)
对 MSDN 文档的扩展,以及一份关于如何利用 MFC 文档/视图架构增强应用程序的技巧和窍门列表。
目录
本文档尚在进行中。我想先发布出来,这样可以促使我做更多的工作。编辑请注意 - 请勿编辑本文档。
引言
MFC 文档/视图类是创建应用程序的强大工具。我在论坛上看到很多关于这些类如何工作以及如何修改/扩展其标准行为的问题。在这里,我想分享我对这个架构的一些了解,以及修改和扩展它的方法,使其能够满足你的需求。
MSDN 对一些 MFC 类及其工作方式的文档非常详尽,但有些类则完全缺失。了解这些类的唯一方法是直接深入 MFC 源代码,看看它们是如何工作的。我也会在这里提供一些关于这些类的额外文档。
相关文章
一份可能有帮助的相关文章列表。
- MFC 插件架构 [^]。
- 在 MFC 应用程序中以编程方式设置默认打印机 [^]。
- MFC 的增强打印预览 [^]。
- 对话框在 MFC 中的打印/预览 [^]。
MFC 类概述
关于 MFC 文档/视图架构,我们需要首先理解控制它的类之间的关系。下面是这些类如何组织的简要图示。
这是框架的一个简化视图,因为它没有考虑框架支持的不同基类视图类型等。
总的来说,我们可以看到有一个单一的 CWinApp
类对象。它包含一个 CDocManager
对象,MFC 用它来处理你向框架注册的所有 CDocTemplate
对象。CWinApp
对象还创建了一个 CMainFrame
对象,这是你的应用程序的主窗口。每次你在应用程序中打开/创建一个文档时,都会创建一个正确的 CDocument
对象。指向该对象的指针将存储在相应 CDocTemplate
对象下的列表中,该对象用于注册该文档类型。当你打开一个文档时,CMainFrame
会创建一个 CChildFrame
对象(一个 MDI 窗口),用于显示文档的视图。一个 CView
对象将在 CChildFrame
窗口内创建为子窗口,指向该视图的指针也将存储在刚打开的 CDocument
对象的视图列表中。
如果你为你的 CDocument
创建了额外的视图,将会创建额外的 CChildFrame
对象,并在其中包含正确的视图类型。指向这个新视图的指针也会被添加到相应 CDcoument
对象的视图列表中。
CDocManager
类
CDocManager
类是 MFC 的未文档化助手类之一。MFC 用它来管理你应用程序中的 CDocTemplate
/CMultiDocTemplate
对象列表。你的 MFC 应用程序中永远只有一个这样的对象。它位于 CWinApp
中,可以通过 m_pDocManager
成员变量访问。它通过指针访问,允许你用你自己派生的 CDocManager
类替换 MFC 的类(如果你需要的话),但是这样做,你必须在 MFC 创建自己的 m_pDocManager
变量之前创建并设置它。这在以下函数中完成:
void CWinApp::AddDocTemplate(CDocTemplate* pTemplate) { if (m_pDocManager == NULL) m_pDocManager = new CDocManager; m_pDocManager->AddDocTemplate(pTemplate); }
因此,只要你在 CWinApp::InitInstance()
派生函数中第一次调用 AddDocTemplate()
之前创建并分配了自己的 CDocManager
派生对象,MFC 代码就会自动使用你的对象。请注意,CDocManager
对象会在 CWinApp
析构函数中由 MFC 代码为你删除。
CWinApp::~CWinApp() { // free doc manager if (m_pDocManager != NULL) delete m_pDocManager; ...
CDocManager
未文档化的原因之一是它是一个 MFC 助手类,通常不供普通用户修改。随着 MFC 的进一步发展,它可能会在未来随时被移除或更改。
那么,让我们看看 CDocManager
类中可用的函数(来自 AFXWIN.H)
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(); // open named file virtual CDocument* OpenDocumentFile(LPCTSTR lpszFileName); virtual BOOL SaveAllModified(); // save before exit virtual void CloseAllDocuments(BOOL bEndSession); 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 };
那么,这些函数各自做什么呢?好吧,如果你查看代码,你会发现它们通常实现了 CWinApp
类具有相同名称/原型函数调用的实际功能,所以查阅标准 CWinApp
帮助文档中相同函数名的内容,可以很好地了解该函数的作用。我也尽我所知/经验,在下面对其进行了文档记录。
CDocManager()
类的构造函数。它不做任何特殊的事情,因为所有成员变量都知道如何自行构造。
virtual void AddDocTemplate(CDocTemplate* pTemplate)
一个你应该至少部分熟悉的函数。当你调用
CWinAPP::AddDocTemplate
在你的CWinApp::InitInstance()
派生函数中时,这个调用会直接传递给CDocManager
对象。CDocTemplate
对象列表由这个类持有。接下来的两个函数可以用来获取任何已注册的CDocTemplate
对象的指针。virtual POSITION GetFirstDocTemplatePosition() const
因为
CDocTemplate
对象列表由CDocManager
对象持有,所以需要有一种方法来迭代这个列表,所以这个函数和下一个函数允许你遍历应用程序中已注册的CDocTemplate
对象列表。virtual CDocTemplate* GetNextDocTemplate(POSITION& pos) const
允许你遍历
CDocTemplate
列表。// typical iteration code is: POSITION pos = m_pDocManager->GetFirstDocTemplatePosition(); while (pos) { CDocTemplate *pTemplate = m_pDocManager->GetNextDocTemplate(pos); ASSERT(pTemplate); ASSERT_VALID(pTemplate); // do something with the pDocTemplate object }
virtual void RegisterShellFileTypes(BOOL bCompat)
这是
CWinApp::RegisterShellFileTypes()
函数的实际实现。MSDN 对该函数的作用有很好的描述。void UnregisterShellFileTypes()
它将撤销上述函数的效果。MSDN 没有关于
CWinApp
中此函数的信息。virtual CDocument* OpenDocumentFile(LPCTSTR lpszFileName)
此函数将遍历应用程序中的
CDocTemplate
对象列表,并检查文件的扩展名是否与CDocTemplate
对象匹配。如果找到匹配项且文件尚未在你的应用程序中打开,则会创建一个正确类型的新文档并打开文件。用于匹配文档类型的文件扩展名定义在你的应用程序的 字符串表中。如果你按照以下方式注册了你的CDocTemplate
对象:pDocTemplate = new CMultiDocTemplate( IDR_CONTEXTTYPE, RUNTIME_CLASS(CContextHelpDoc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CContextHelpView)); AddDocTemplate(pDocTemplate);
那么 字符串表中 ID 为
IDR_CONTEXTTYPE
的条目将在其中一个字段中包含默认文件扩展名。你通常在 AppWizard 项目创建阶段定义第一个文档类型的文件扩展名……virtual BOOL SaveAllModified()
此函数再次是
CWinApp::SaveAllModified()
函数的实际实现。通常在用户想要终止当前应用程序会话时调用,它会遍历所有CDocTemplate
对象并对每个对象调用SaveAllModified()
函数。这些对象又会遍历它们各自打开的所有文档并调用CDocument::SaveModified()
函数。如果这些函数中的任何一个返回非零值(通常是因为用户通过消息框取消了操作),那么应用程序将不会关闭。virtual void CloseAllDocuments(BOOL bEndSession)
此函数与
SaveAllModified()
非常相似,用于关闭所有打开的文档。通常在应用程序关闭时由框架窗口使用,但仍可用于关闭所有打开的文档。virtual int GetOpenDocumentCount()
此函数实现了
CWinApp::GetOpenDocumentCount()
函数。它返回应用程序中所有CDocTemplate
对象的所有打开文档的总数。virtual BOOL DoPromptFileName(CString& fileName, UINT nIDSTitle, DWORD lFlags, BOOL bOpenFileDialog, CDocTemplate* pTemplate)
CWinApp::DoPromptFileName()
的默认实现,它会提示用户输入要首次保存的新文档的文件名。它使用为每个已注册的CDocTemplate
对象提供的标准过滤器,并添加默认的*.*
过滤器。如果你想更改应用程序的基本行为,例如,如果你正在编写一个图形应用程序,需要能够将图片保存为多种文件格式,那么覆盖这个函数是一个好方法,你可以在这里设置正确的文件过滤器列表。virtual BOOL OnDDECommand(LPTSTR lpszCommand)
此函数实现了通过 Windows Explorer 发送到主框架的 DDE 命令。这适用于打开/打印现有文件的命令。
virtual void OnFileNew()
CWinApp::OnFileNew()
的实际实现。此函数检查已向框架注册的CDocTemplate
对象数量。如果超过一个,则会显示一个对话框,其中包含一个列表框,列出文档类型的名称(来自 字符串表)。一旦用户选择了模板类型,框架就会在选定的模板上调用OpenDocumentFile(NULL)
来创建一个新的空文档。默认情况下,如果你只向系统注册了一个CDocTemplate
对象,它将自动创建该类型的文档。如果你需要创建特定类型的新空文档而不让框架显示选择对话框,你将不得不自己调用正确
CDocTemplate
对象上的CDocTemplate::OpenDocumentFile(NULL)
。virtual void OnFileOpen()
int GetDocumentCount()
这是一个
protected
成员函数,所以你无法直接调用它。它似乎是GetOpenDocumentCount()
函数的一个精确副本,所以你不需要使用它。
CDocTemplate
类
CDocTemplate
类处理打开的文档列表以及用于实现其功能的类。它还允许 MDI/SDI 接口知道当选定的文档类型处于活动状态时,主框架使用的默认菜单和加速键。首先,让我们看看 CDocTemplate
对象是如何在 InitInstance()
函数中注册的。
pDocTemplate = new CMultiDocTemplate( IDR_YOURDOCTYPE, RUNTIME_CLASS(CYourDoc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CYourView)); AddDocTemplate(pDocTemplate);
让我们看看这些参数
IDR_YOURDOCTYPE
此参数用于标识文档类型的菜单、加速器和文件扩展名。你会发现使用相同 ID 但类型不同的各种资源。
- 菜单:当你的文档处于活动状态时将使用的菜单。
- 加速器:菜单命令/快捷方式的加速键。
- 字符串表:此字符串表条目用于标识你的文档的文件扩展名。
字符串表条目
字符串表条目是你应用程序中非常重要的一部分。它包含按特定顺序排列的几个字符串,由 '\n'
字符分隔。这些条目的顺序由 enum
控制。
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 };
那么,这些示例分别做什么呢?
windowTitle
- 此条目仅在 MFC 注册/注销你的文档类型与 Shell 时使用。docName
- 创建此类新文档时给出的默认名称,后面会附加一个数字。fileNewName
- 如果你的应用程序注册了多个文档模板,并且用户选择了 新建…,则默认情况下 MFC 会显示一个 选择文档类型 对话框。这是对话框中列出的此文档类型的名称。filterName
- 出现在文件打开/保存对话框中的文件过滤器,例如 我的文档类型 (*.mdt)。filterExt
- 此文档类型的默认文件扩展名。默认情况下,你的文档只能处理单个文件扩展名,我稍后会告诉你如何 将多个文件扩展名与单个文档模板关联。RegFileTypeID
- Shell 用于你的文档类型的标识符。regFileTypeName
- Shell 用于向用户提供文档类型描述,例如在 Explorer 等应用程序中。RUNTIME_CLASS(CYourDoc)
当打开新文档或现有文档时,MFC 将创建的文档对象类型。
RUNTIME_CLASS(CChildFrame)
将在其中显示你的视图的子框架控制类。这是实际的 MDI 窗口。
RUNTIME_CLASS(CYourView)
将在 MDI 框架中显示的视图类型。
文档类和视图类(们)可以通过相应的访问函数相互引用。
对 CDocManager
和 CDocTemplate
的增强
以下是一些你可以用 CDocManager
和 CDocTemplate
类来增强和修改其功能,以更好地支持你的应用程序的功能列表。
当注册了多个 CDocTemplate
时,打开一个新的特定文档类型
如果你向框架注册了多个 CDocTemplate
对象,当用户单击 ID_FILE_NEW
时,默认情况下框架会显示一个“选择文档类型”对话框。
在这种情况下,你需要获取正确的 CDocTemplate
对象指针并调用 OpenDocumentFile(NULL>
。一个好的方法是定义一个 enum
,列出所有文档类型的注册顺序,并在 CDocManager
类中添加一个函数,通过继承你的类。
enum mdt_MyDocumentTypes { mdt_FirstDocumentType = 0, mdt_SecondDocumentType, mdt_ThirdDocumentType // ... add extras as required }; class CDocManagerEx : public CDocManager { DECLARE_DYNAMIC(CDocManagerEx) public: CDocManagerEx(); virtual ~CDocManagerEx(); CDocument* CreateNewDocument(int doc_index, CString filename = ""); }; CDocument* CDocManagerEx::CreateNewDocument(int doc_index, CString filename) { // Find the correct document template and create a document from it CDocument *pDoc = NULL; if (m_templateList.GetCount() >= doc_index) { POSITION pos = m_templateList.GetHeadPosition(); CDocTemplate *pTemplate = NULL; // iterate through the list looking for the required document type if (doc_index == 0) { pTemplate = (CDocTemplate*)m_templateList.GetNext(pos); } while (pos != NULL && doc_index > 0) { pTemplate = (CDocTemplate*)m_templateList.GetNext(pos); ASSERT_KINDOF(CDocTemplate, pTemplate); } if (pTemplate != NULL) { // create the document if (filename == "") { pDoc = pTemplate->OpenDocumentFile(NULL); } else { pDoc = pTemplate->OpenDocumentFile(filename); } } } // return the document pointer or NULL if failed return pDoc; }
你还需要在 CWinApp::InitInstance
中用自己的 CDocManager
对象替换 MFC 默认创建的对象。
m_pDocManager = new CDocManagerEx; // we replace the default doc manager CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_DPASTYPE, RUNTIME_CLASS(CSomeDoc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CSomeView)); AddDocTemplate(pDocTemplate);
要使用新函数,你可以这样调用它:
(static_cast<CDocManagerEx*>(AfxGetApp()->m_pDocManager))->CreateNewDocument(
mdt_FirstDocumentType);
如果想打开特定文档,可以添加一个可选的文件名作为第二个参数。
将多个文件扩展名与文档模板关联
如果你需要一个文档类型识别多个文件扩展名,你需要扩展 CMultiDocTemplate
类来识别这些额外的扩展名。有一个 virtual
函数需要修改。
class CMultiDocTemplateEx : public CMultiDocTemplate { DECLARE_DYNAMIC(CMultiDocTemplateEx) public: CMultiDocTemplateEx(UINT nIDResource, CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass ); virtual ~CMultiDocTemplateEx(); virtual Confidence MatchDocType(LPCTSTR lpszPathName, CDocument*& rpDocMatch); }; CMultiDocTemplateEx::CMultiDocTemplateEx( UINT nIDResource, CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass ) : CMultiDocTemplate(nIDResource, pDocClass, pFrameClass, pViewClass) { } CMultiDocTemplateEx::~CMultiDocTemplateEx() { } // one draw-back of using our own MatchDocType function is that // we never return the "yesAttemptForiegn" confidence // type anymore. Although I beleive this is a good thing as we // should not be trying to open file types we do not // recognise. CDocTemplate::Confidence CMultiDocTemplateEx::MatchDocType( LPCTSTR lpszPathName, CDocument*& rpDocMatch) { Confidence con = noAttempt; // make sure we have the correct resource string m_strDocStrings.LoadString(m_nIDResource) ; // code copied from the base class and now also checks // for multiple file extensions ASSERT(lpszPathName != NULL); rpDocMatch = NULL; // go through all documents POSITION pos = GetFirstDocPosition(); while (pos != NULL) { CDocument* pDoc = GetNextDoc(pos); if (AfxComparePath(pDoc->GetPathName(), lpszPathName)) { // already open rpDocMatch = pDoc; con = yesAlreadyOpen; break; } } if (con != yesAlreadyOpen) { // see if it matches our default suffix CString strFilterExt; if (GetDocString(strFilterExt, CDocTemplate::filterExt) && !strFilterExt.IsEmpty()) { CString extensions(strFilterExt); extensions.MakeLower(); // see if extension matches ASSERT(strFilterExt[0] == '.'); // break of the file extension. if its blank "", set it to ".!!!" // so that an empty file extension can be recognised. char ext[_MAX_EXT]; _splitpath(lpszPathName, NULL, NULL, NULL, ext); if (_tcslen(ext) == 0) { // no file extension, set to ".!!!" // which is an invalid file // extension that should never occur // in real life. We onyl use it here to recognise // files without extensions. // goto a special file extension _tcscpy(ext, ".!!!"); } CString copy(ext); copy.MakeLower(); if (extensions.Find(copy) >= 0) { // extension matches, looks like ours con = yesAttemptNative; } } } return con; }
一旦你在 InitInstance()
函数中使用了这种对象类型,你就可以更新字符串表资源中的 filterExt
字段以添加所需的额外文件扩展名。
\nMyDocType \nMyDocType \nMyDocType files (*.ex1;*.ex2;*.ex3;*.) \n.ex1.ex2.ex3.!!! \nMyDocType.Document \nMyDocType Document
注意这里使用的特殊文件扩展名 .!!!
,它用于识别空文件扩展名。
致谢
- Michael Dunn - 他的 HTML 布局我肆无忌惮地抄袭,因为我不知道自己怎么做(我讨厌 HTML)。
历史
- 2003年3月 - 初版。
尽情享用!