65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (51投票s)

2003年10月16日

CPOL

11分钟阅读

viewsIcon

205411

downloadIcon

1

对 MSDN 文档的扩展,以及一份关于如何利用 MFC 文档/视图架构增强应用程序的技巧和窍门列表。

目录

本文档尚在进行中。我想先发布出来,这样可以促使我做更多的工作。编辑请注意 - 请勿编辑本文档。

引言

MFC 文档/视图类是创建应用程序的强大工具。我在论坛上看到很多关于这些类如何工作以及如何修改/扩展其标准行为的问题。在这里,我想分享我对这个架构的一些了解,以及修改和扩展它的方法,使其能够满足你的需求。

MSDN 对一些 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 框架中显示的视图类型。

    文档类和视图类(们)可以通过相应的访问函数相互引用。

CDocManagerCDocTemplate 的增强

以下是一些你可以用 CDocManagerCDocTemplate 类来增强和修改其功能,以更好地支持你的应用程序的功能列表。

当注册了多个 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月 - 初版。

尽情享用!

© . All rights reserved.