第13章 多重文件与多重显示

优质
小牛编辑
124浏览
2023-12-01

你可能会以【Window/New Window】为同一份文件制造出另一个 View 窗口,也可能设计分裂窗口,以多个窗口呈现文件的不同角落 (如第 11 章所为)。但,这两种情况都是以相同的显示方式表达文件的内容。

如何突破一成不变的显示方法,达到丰富的表现效果?

这一章我将对于 Document/View 再作各种深入应用。重要放在显象技术以及多重文件的技术上。

MDI和SDI

首先再让我把MDI和SDI的观念厘清楚。

在传统的SDK 程序设计中,所谓MDI是指「一个大外框窗口,内部可容纳许多小子窗口」的这种程序风格。内部的小子窗口即是「Document 窗口」-- 虽然当时并未有如MFC 所谓的Document 观念。此外,「MDI 风格」还包括程序必须有一个Window 选单,提供对于小子窗口的管理,包括tile、cascade、icon arrange 等命令项:

至于SDI程序,就是一般的、没有上述风格的non-MDI程序。

在MFC的定义中,MDI表示可「同时」开启一份以上的Documents,这些 Documents可以是相同类型,也可以是不同类型。许多份Documents 同时存在,必然需要许多个子窗口容纳之,每个子窗口其实是Document的一个View。即使你在 MDI 程序中只开启一份 Document,但以【Window/New Window】的方式打开第二个view、第三个view...,亦需占用多个子窗口。因此这和 SDK 所定义的 MDI 有异曲同工的意义。

至于SDI 程序,同一时间只能开启一份Document。一份Document 只占用一个子窗口(亦即其 View 窗口),因此这也与 SDK所定义的SDI意义相同。当你要在SDI程序中开启第二份 Document,必须先把第一份Document关闭。MDI 程序未必一定得提供一个以上的 Document 类型。所谓不同的 Document 类型是指程序提供不同的CDocument 派生类,亦即有不同的Document Template。软件工业早期曾经流行一种「全效型」软件,既处理电子表格、又作文书处理、又能绘图作画,伟大得不得了,这种软件就需要数种文件类型:电子表格、文书、图形。

多重显像(Multiple Views)

只要是具备 MDI 性质的 MFC 程序(也就是你曾在 AppWizard 步骤一中选择【Multiple Documents】项目),天生就具备了「多重显像」能力。「天生」的意思是你不必动手,application framework 已经内含了这项功能:随便执行任何一版的 Scribble,你都可以在【Window】选单中找到【New Window】这个命令项,按下它,就可以获得「同源子窗口」如图 13-1。

我将以「多重显像」来称呼 Multiple Views。多重显像的意思是数据可以不同的类型显现出来。并以「同源子窗口」代表「显示同一份 Document 而又各自分离的View 窗口」。

图13-1 【 Window/New Window】可以为「目前作用中的

View 所对应的Document 再开一个View 窗口。

另外,第11章也介绍了一种变化,是利用分裂窗口的各个窗口,显示 Document 内容。这些窗口虽然集中在一个大窗口中,但它们的视野却可以各自独立,也就是说它们可以看到Document 中的不同局部,如图 13-2。

图13-2 分裂窗口的不同窗口可以观察同一Document数据的不同局部。

但是我们发现,不论是同源子窗口或分裂窗口的窗口,都是以相同的方式(也就是同一个 CMyView::OnDraw)表现Document内容。如果我们希望表达力丰富一些,如何是好?到现在为止我们并没有看到任何一个Scribble 版本具备了多种显像能力。

窗口的动态分裂

动态分裂窗口由CSplitterWnd 提供服务。这项技术已经在第11章的 Scribble Step4 示范过了。它并没有多重显像的能力,因为每一个窗口所使用的 View 类完全相同。当第一个窗口形成(也就是分裂窗口初产生的时候),它将使用Document Template中登记的View类,作为其View类。尔后当分裂发生,也就是当使用者拖拉滚动条之上名为分裂棒(splitter box)的横杆,导至新窗口诞生,程序就以「动态生成」的方式产生出新的View窗口。

因此,View 类一定必须支持动态生成,也就是必须使用 DECLARE_DYNCREATE 和IMPLEMENT_DYNCREATE 宏。请回顾第8章。

AppWizard 支持动态分裂窗口。当你在AppWizard 步骤四的【Advanced】对话框的【Windows Styles】附页中选按【Use split window】选项:

你的程序比起一般未选【Use split window】选项者有如下差异(阴影部份):

✦ CSplitterWnd::Create 的详细规格请回顾第11章。

这些其实也就是我们在第 11 章为 Scribble Step4 亲手加上的代码。如果你一开始就打定主意要使用动态分裂窗口,如上便是了。

窗口(Panes)之间的同步更新,其机制着落在两个虚函数 CDocument::UpdateAllViews和 CView::OnUpdate 身上,与第 11 章的情况完全相同。

动态分裂的实现,非常简单。但它实在称不上「怎么样」!除了拥有「动态」增减窗口的长处之外,短处有二:第一,每一个窗口都使用相同的 View 类,因此显示出来的东西千篇一律;第二,窗口之间并非完全独立。同一水平列的窗口,使用同一个垂直卷轴;同一垂直行的窗口,使用同一个水平滚动条,如图 13-2。

窗口的静态分裂

动态分裂窗口的短处正是静态分裂窗口的长处, 动态分裂窗口的长处正是静态分裂窗口的短处。

静态分裂窗口的窗口个数一开始就固定了,窗口所使用的 view 必须在分裂窗口诞生之际就准备好。每一个窗口的活动完全独立自主,有完全属于自己的水平滚动条和垂直滚动条。

静态分裂窗口的窗口个数限制是 16 列 x 16 行,

动态分裂窗口的窗口个数限制是 2 列 x 2 行。

欲使用静态分裂窗口,最方便的办法就是先以 AppWizard 产生出动态分裂代码(如上一节所述),再修改其中部份程序。

不论动态分裂或静态分裂,分裂窗口都由CSplitterWnd 提供服务。动态分裂窗口的诞生是靠CSplitterWnd::Create,静态分裂窗口的诞生则是靠 CSplitterWnd::CreateStatic。为了静态分裂,我们应该把上一节由 AppWizard 产生的函数代码改变如下:

BOOL CChildFrame::OnCreateClient( LPCREATESTRUCT /*lpcs*/,
CCreateContext* pContext)
{
    // 产生静态分裂窗口,横列为 1,纵行为 2。
    m_wndSplitter.CreateStatic(this, 1, 2);
    // 产生第一个窗口(标号 0,0)的 view 窗口。
    m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CTextView),
    CSize(100, 0), pContext);
    // 产生第二个窗口(标号 0,1)的 view 窗口。
    m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CBarView),
    CSize(0, 0), pContext);
}

这会产生如下的分裂窗口:

CreateStatic 和 CreateView

静态分裂用到两个 CSplitterWnd 成员函数:

CreateStatic:

这个函数的规格如下:

BOOL CreateStatic( CWnd* pParentWnd, int nRows, in nCols,
DWORD dwStyle = WS_CHILD | WS_VISIBLE,
UINT nID = AFX_IDW_PANE_FIRST );

第一个参数代表此分裂窗口之父窗口。第二和第三参数代表横列和纵行的个数。第四个参数是窗口风格,预设为 WS_CHILD | WS_VISIBLE,第五个同时也是最后一个参数代表窗口(也是一个窗口)的ID起始值。

CreateView

这个函数的规格如下:

virtual BOOL CreateView( int row, int col, CRuntimeClass* pViewClass,
SIZE sizeInit, CCreateContext* pContext );

第一和第二参数代表窗口的标号(从 0 起算)。第三参数是 View 类的 CRuntimeClass指针,你可以利用RUNTIME_CLASS宏(第3章和第8章提过)取此指针,也可以利用OnCreateClient 的第二个参数CCreateContext* pContext 所储存的一个成员变量m_pNewViewClass。你大概已经忘了这个变量吧,但我早提过它了,请看第8章的 「CDocTemplate 管理 CDocument / CView / CFrameWnd」一节。所以,对于已在CMultiDocTemplate 中登记过的 View 类,此处可以这么写:

// 产生第一个窗口(标号 0,0)的 view 窗口。
m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CMyView),
CSize(100, 0), pContext);

也可以这么写:

m_wndSplitter.CreateView(0, 0, pContext->m_pNewViewClass,
CSize(100, 0), pContext);

让我再多提醒你一些,第8章的「CDocTemplate 管理CDocument / CView / CFrameWnd」一节主要是说明当使用者打开一份文件,MFC 内部有关于 Document / View / Frame「三位一体」的动态生成过程。其中View 的动态生成是在 CFrameWnd::OnCreate被唤起后,经历一连串动作,最后才在 CFrameWnd::CreateView 中完成的:

而我们现在,为了分裂窗口,正在改写其中第三个虚函数 CFrameWnd::OnCreateClient呢!

好了,回过头来,CreateView 的第四参数是窗口的初始大小,CSize(100, 0) 表示窗口宽度为100 个图素。高度倒是不为 0,对于横列为 1 的分裂窗口而言,窗口高度永远为窗口高度,Framework 并不理会你在CSize 中写了什么高度。至于第二个窗口的大小CSize(0, 0) 道理雷同,Framework 并不加理会其值,因为对于纵行为 2 的分裂窗口而言,右边窗口的宽度永远是窗口总宽度减去左边窗口的宽度。

程序进行中如果需要窗口的大小,只要在 OnDraw 函数(通常是这里需要)中这么写即可:

RECT rc; this->GetClientRect(&rc);

CreateView的第五参数是CCreateContext 指针。我们只要把 OnCreateClient 获得的第二个参数依样画葫卢地传下去就是了。

窗口的静态三叉分裂

分裂的方向可以无限延伸。我们可以把一个静态分裂窗口的窗口再做静态分裂,下面的程序代码展现了这种可能性:

// in header file
class CChildFrame : public CMDIChildWnd
{
    ...
protected:
    CSplitterWnd m_wndSplitter1;
    CSplitterWnd m_wndSplitter2;
public:
    // Overrides
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CChildFrame)
public:
    virtual BOOL OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext);
    virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
    //}}AFX_VIRTUAL
    ...
};
// in implementation file
BOOL CChildFrame::OnCreateClient( LPCREATESTRUCT /*lpcs*/,
CCreateContext* pContext)
{
    // 产生静态分裂窗口,横列为 1,纵行为 2。
    m_wndSplitter1.CreateStatic(this, 1, 2);
    // 产生分裂窗口的第一个窗口(标号 0,0)的 view 窗口。
    m_wndSplitter1.CreateView(0, 0, RUNTIME_CLASS(CTextView),
    CSize(300, 0), pContext);
    // 产生第二个分裂窗口,横列为 2,纵行为 1。位在第一个分裂窗口的(0,1)窗口
    m_wndSplitter2.CreateStatic(&m_wndSplitter1, 2, 1,
    WS_CHILD | WS_VISIBLE, m_wndSplitter1.IdFromRowCol(0, 1));
    // 产生第二个分裂窗口的第一个窗口(标号 0,0)的 view 窗口。
    m_wndSplitter2.CreateView(0, 0, RUNTIME_CLASS(CBarView),
    CSize(0, 150), pContext);
    // 产生第二个分裂窗口的第二个窗口(标号 1,0)的 view 窗口。
    m_wndSplitter2.CreateView(1, 0, RUNTIME_CLASS(CCurveView),
    CSize(0, 0), pContext);
    return TRUE;
}

这会产生如下的分裂窗口:

第二个分裂窗口的ID 起始值可由第一个分裂窗口的窗口之一获知(利用 IdFromRowCol 成员函数),一如上述程序代码中的动作。

剩下的问题,就是如何设计许多个 View 类了。

Graph 范例程序

Graph 是一个具备静态三叉分裂能力的程序。左侧窗口以文字方式显示 10 笔整数数据,右上侧窗口显示该 10 笔数据的长条图,右下侧窗口显示对应的曲线图。

进行至这一章,相信各位对于工具的基本操作技术都已经都熟练了,这里我只列出Graph 程序的制作大纲:

  • 进入AppWizard,制造一个Graph 项目。采用预设的选项,但在第四步骤的【Advanced】对话框的【Windows Styles】附页中,将【Use split window】致能 (enabled)起来。并填写【Documents Template Strings】附页如下:

    最后,AppWizard 给我们这样一份清单:

    我们获得的主要类整理如下:

    类              基类                  文件
    

  • 进入整合环境的Resource View窗口中,选择IDR_GRAPHTYPE选单,在【Window】之前加入一个【Graph Data】选单,并添加三个项目,分别是:

    选单项目名称               识别代码(ID)             提示字符串
    

    于是GRAPH.RC 的选单资源改变如下:

    IDR_GRAPHTYPE MENU PRELOAD DISCARDABLE
    BEGIN
    ...
    POPUP "&Graph Data"
    BEGIN
    MENUITEM "Data&1", ID_GRAPH_DATA1
    MENUITEM "Data&2", ID_GRAPH_DATA2
    MENUITEM "Data&3", ID_GRAPH_DATA3
    END
    ...
    END
    
  • 回到整合环境的Resource View 窗口,选择IDR_MAINFRAME 工具列,增加三个按钮,放在Help 按钮之后,并使用工具箱上的Draws Text 功能,为三个按钮分别涂上1, 2, 3 画面:

    这三个按钮的IDs 采用先前新增的三个选单项目的IDs。

    于是,GRAPH.RC 的工具列资源改变如下:

    IDR_MAINFRAME TOOLBAR DISCARDABLE  16, 15
    BEGIN
    ...
    BUTTON ID_FILE_PRINT
    BUTTON ID_APP_ABOUT
    BUTTON ID_GRAPH_DATA1
    BUTTON ID_GRAPH_DATA2
    BUTTON ID_GRAPH_DATA3
    END
    
  • 进入ClassWizard,为新增的这些UI对象制作Message Map。由于这些命令项会影响到我们的Document 内容(当使用者按下Data1,我们必须为他准备一份相关数据;按下Data2,我们必须再为他准备一份相关数据),所以在CGraphDoc 中处理这些命令消息甚为合适:

    UI 对象                 Messages                  消息处理例程
    

    原始代码改变如下:

    // in GRAPHDOC.H
    class CGraphDoc : public CDocument
    {
        ...
        // Generated message map functions
    protected:
        //{{AFX_MSG(CGraphDoc)
        afx_msg void OnGraphData1();
        afx_msg void OnGraphData2();
        afx_msg void OnGraphData3();
        afx_msg void OnUpdateGraphData1(CCmdUI* pCmdUI);
        afx_msg void OnUpdateGraphData2(CCmdUI* pCmdUI);
        afx_msg void OnUpdateGraphData3(CCmdUI* pCmdUI);
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()
    };
    // in GRAPHDOC.CPP
    BEGIN_MESSAGE_MAP(CGraphDoc, CDocument)
    //{{AFX_MSG_MAP(CGraphDoc)
    ON_COMMAND(ID_GRAPH_DATA1, OnGraphData1)
    ON_COMMAND(ID_GRAPH_DATA2, OnGraphData2)
    ON_COMMAND(ID_GRAPH_DATA3, OnGraphData3)
    ON_UPDATE_COMMAND_UI(ID_GRAPH_DATA1, OnUpdateGraphData1)
    ON_UPDATE_COMMAND_UI(ID_GRAPH_DATA2, OnUpdateGraphData2)
    ON_UPDATE_COMMAND_UI(ID_GRAPH_DATA3, OnUpdateGraphData3)
    //}}AFX_MSG_MAP
    END_MESSAGE_MAP()
    
  • 利用ClassWizard产生两个新类,做为三叉分裂窗口中的另两个窗口的View 类:

    类名称    基类                 文件
    
    CTextView   CView                   TEXTVIEW.CPP TEXTVIEW.H
    
    CBarView    CView                   BARVIEW.CPP BARVIEW.H
    
  • 改写CChildFrame::OnCreateClient 函数如下(这是本节的技术重点):

    #include "stdafx.h"
    #include "Graph.h"
    #include "ChildFrm.h"
    #include "TextView.h"
    #include "BarView.h"
    ...
    BOOL CChildFrame::OnCreateClient( LPCREATESTRUCT /*lpcs*/,
    CCreateContext* pContext)
    {
        // 产生静态分裂窗口,横列为 1,纵行为 2。
        m_wndSplitter1.CreateStatic(this, 1, 2);
        // 产生分裂窗口的第一个窗口(标号 0,0)的 view 窗口,采用 CTextView。
        m_wndSplitter1.CreateView(0, 0, RUNTIME_CLASS(CTextView),
        CSize(300, 0), pContext);
        // 产生第二个分裂窗口,横列为 2 纵行为 1。位在第一个分裂窗口的(0,1)窗口
        m_wndSplitter2.CreateStatic(&m_wndSplitter1, 2, 1,
        WS_CHILD|WS_VISIBLE, m_wndSplitter1.IdFromRowCol(0, 1));
        // 产生第二个分裂窗口的第一个窗口(标号 0,0)的 view 窗口,采用 CBarView。
        m_wndSplitter2.CreateView(0, 0, RUNTIME_CLASS(CBarView),
        CSize(0, 150), pContext);
        // 产生第二个分裂窗口的第二个窗口(标号 1,0)的 view 窗口,采用 CGraphView。
        m_wndSplitter2.CreateView(1, 0, pContext->m_pNewViewClass,
        CSize(0, 0), pContext);
        // 设定 active pane
        SetActiveView((CView*)m_wndSplitter1.GetPane(0,0));
        return TRUE;
    }
    

    为什么最后一次CreateView时我以pContext->m_pNewViewClass取代RUNTIME_CLASS(CGraphView)呢?后者当然也可以,但却因此必须含入CGraphView 的声明;而如果你因为这个原因而含入GraphView.h 檔,又会产生三个编译错误,挺麻烦!

    至此,Document 中虽然没有任何数据,但程序的 UI 已经完备,编译链接后可得以下执行画面:

    修改CGraphDoc,增加一个整数数组m_intArray,这是真正存放数据的地方,我采用MFC内建的CArray<int,int>,为此,必须在STDAFX.H 中加上一行:

    #include <afxtempl.h> // MFC templates
    

    为了设定数组内容,我又增加了一个SetValue 成员函数,并且在【Graph Data】选单命令被执行时,为 m_intArray 设定不同的初值:

    // in GRAPHDOC.H
    class CGraphDoc : public CDocument
    {
        ...
    public:
        CArray<int,int> m_intArray;
    public:
        void SetValue(int i0, int i1, int i2, int i3, int i4,
        int i5, int i6, int i7, int i8, int i9);
        ...
    };
    // in GRAPHDOC.CPP
    CGraphDoc::CGraphDoc()
    {
        SetValue(5, 10, 15, 20, 25, 78, 64, 38, 29, 9);
    }
    void CGraphDoc::SetValue(int i0, int i1, int i2, int i3, int i4,
    int i5, int i6, int i7, int i8, int i9);
    {
        m_intArray.SetSize(DATANUM, 0);
        m_intArray[0] = i0;
        m_intArray[1] = i1;
        m_intArray[2] = i2;
        m_intArray[3] = i3;
        m_intArray[4] = i4;
        m_intArray[5] = i5;
        m_intArray[6] = i6;
        m_intArray[7] = i7;
        m_intArray[8] = i8;
        m_intArray[9] = i9;
    }
    void CGraphDoc::OnGraphData1()
    {
        SetValue(5, 10, 15, 20, 25, 78, 64, 38, 29, 9);
        UpdateAllViews(NULL);
    }
    void CGraphDoc::OnGraphData2()
    {
        SetValue(50, 60, 70, 80, 90, 23, 68, 39, 73, 58);
        UpdateAllViews(NULL);
    }
    void CGraphDoc::OnGraphData3()
    {
        SetValue(12, 20, 8, 17, 28, 37, 93, 45, 78, 29);
        UpdateAllViews(NULL);
    }
    void CGraphDoc::OnUpdateGraphData1(CCmdUI* pCmdUI)
    {
        pCmdUI->SetCheck(m_intArray[0] == 5);
    }
    void CGraphDoc::OnUpdateGraphData2(CCmdUI* pCmdUI)
    {
        pCmdUI->SetCheck(m_intArray[0] == 50);
    }
    void CGraphDoc::OnUpdateGraphData3(CCmdUI* pCmdUI)
    {
        pCmdUI->SetCheck(m_intArray[0] == 12);
    }
    

    各位看到,为了方便,我把m_intArray 的数据封装属性设为public 而非 private,检查「m_intArray 内容究竟是哪一份数据」所用的方法也非常粗糙,呀,不要非难我,重点不在这里呀!

  • 在RESOURCE.H 文件中加上两个常数定义:

    #define DATANUM  10
    #define DATAMAX  100
    
  • 修改CGraphView,在OnDraw 成员函数中取得Document,再透过Document对象指针取得整数数组,然后将10 笔数据的曲线图绘出:

  • 修改CTextView 程序代码,在OnDraw成员函数中取得Document,再透过Document 对象指针取得整数数组,然后将10 笔数据以文字方式显示出来:

    #0001  #include "stdafx.h"
    #0002  #include "Graph.h"
    #0003  #include "GraphDoc.h"
    #0004  #include "TextView.h"
    #0005  ...
    #0006  void CTextView::OnDraw(CDC* pDC)
    #0007  {
    #0008       CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
    #0009
    #0010       TEXTMETRIC tm;
    #0011       int     x,y, cy, i;
    #0012       char    sz[20];
    #0013       pDC->GetTextMetrics(&tm);
    #0014       cy = tm.tmHeight;
    #0015       pDC->SetTextColor(RGB(255, 0, 0)); // red text
    #0016       for (x=5,y=5,i=0; i<DATANUM; i++,y+=cy)
    #0017      {
    #0018           wsprintf (sz, "%d", pDoc->m_intArray[i]);
    #0019           pDC->TextOut (x,y, sz, lstrlen(sz));
    #0020      }
    #0021  }
    
  • 修改CBarView程序代码,在OnDraw 成员函数中取得Document,再透过Document 对象指针取得整数数组,然后将10 笔数据以长条图绘出:

  • 如果你要令三个view 都有打印预视能力,必须在每一个view 类中改写以下三个虚函数:

    virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
    virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
    virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
    

    至于其函数内容,从 CGraphView 的同名函数中依样画葫芦拷贝一份过来即可。

  • 本例不示范文件读写动作,所以CGraphDoc 没有改写Serialize 虚函数。

图13-3是Graph 程序的执行画面。

图13-3 Graph 执行画面。每当选按【 File/New】(或工具列上的对应按钮)打开一份新文件,其内就有10笔整数数据。你可选按【 Graph Data】(或工具列上的对应按钮)改变数据内容。

静态分裂窗口之观念整理

我想你已经从前述的OnCreateClient函数中认识了静态分裂窗口的相关函数。我可以用图 13-4 解释各个类的关系与运用。

基本上图 13-4 三个窗口可以视为三个完全独立的 view 窗口,有各自的类,以各自的方式显示数据。不过,数据倒是来自同一份 Document。试试看预视效果,你会发现,哪一个窗口为「作用中」,哪一个窗口的绘图动作就主宰预窗口口。你可以利用SetActivePane设定作用中的窗口,也可以调用 GetActivePane 获得作用中的窗口。但是,你会发现,从外观上很难看出哪一个窗口是「作用中的」窗口。

图13-4 静态分裂窗口的类运用 (以Graph为例 )

同源子窗口

虽然我说静态分裂窗口的窗口可视为完全独立的 view 窗口,但毕竟它们不是!它们还框在一个大窗口中。如果你不喜欢分裂窗口(谁知道呢,当初我也不太喜欢),我们来试点新鲜的。

点子是从【Window/New Window】开始。这个选单项目令 Framework 为我们做出目前作用中的view窗口的另一份拷贝。如果我们能够知道 Framework 是如何动作,是不是可以引导它使用另一个view类,以不同的方式表现同一份数据?

这就又有偷窥原始代码的需要了。MFC 并没有提供正常的管道让我们这么做,我们需要MFC 原始代码。

CMDIFrameWnd::OnWindowNew

如果你想在程序中设计中断点,一步一步找出【Window/New Window】的动作,就像我在第12章对付OnFilePrint和OnFilePrintPreview 一样,那么你会发现没有着力点,因为AppWizard 并不会做出像这样的消息映射表格:

BEGIN_MESSAGE_MAP(CScribbleView, CScrollView)
...
ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
END_MESSAGE_MAP()

你根本不知道【Window/New Window】这个命令流到哪里去了。第7章的「标准选单 File/ Edit / View / Window / Help」一节,也曾说过这个命令项是属于「与 Framework 预有关联型」的。

那么我如何察其流程?1/3 用猜的,1/3 靠字符串搜寻工具 GREP(第8章介绍过),1/3 靠勤读书。然后我发现,【New Window】命令流到 CMDIFrameWnd::OnWindowNew 去了。

图 13-5 是其原始代码(MFC 4.0 的版本)。

图13-5 CMDIFrameWnd::OnWindowNew原始代码(in WINMDI.CPP)

我们的焦点放在CMDIFrameWnd::OnWindowNew 函数的第14 行,该处取得我们在InitInstance 函数中做好的 Document Template,而你知道,Document Template 中记录有View 类。好,如果我们能够另准备一个崭新的View类,有着不同的OnDraw 显示方式,并再准备好另一份Document Template,记录该新的View 类,然后改变图13-5 的第14行,让它使用这新的Document Template,大功成矣。

当然,我们绝不是要去改MFC原始代码,而是要改写虚函数OnWindowNew,使为我们所用。这很简单,我只要把【Window / New Window】命令项改变名称,例如改为【Window / New Hex Window】,然后为它撰写命令处理函数,函数内容完全仿照图 13-5,但把第14 行改设定为新的Document Template 即可。

Text 范例程序

Text 程序提供【Window / New Text Window】和【Window / New Hex Window】两个新的选单命令项目,都可以产生出 view 窗口,一个以ASCII型式显示数据,一个以Hex型式显示数据,数据来自同一份Document。

以下Text程序的是制作过程:

  • 进入AppWizard,制造一个Text 项目,采用各种预设的选项。获得的主要类如下:

    类              基类                文件
    

  • 进入整合环境中的Resource View 窗口,选择IDR_TEXTTYPE选单,在【Window】选单中加入两个新命令项:

    命令项目名称    识别代码(ID)    提示字符串
    New Text Window ID_WINDOW_TEXT  New a Text Window with Active Document
    New Hex Window  ID_WINDOW_HEX   New a Hex Window with Active Document
    
  • 再在Resource View 窗口中选择IDR_MAINFRAME 工具列,增加两个按钮,安排在Help 按钮之后:

    这两个按钮分别对应于新添加的两个选单命令项目。

  • 进入ClassWizard,为两个UI对象制作Message Map。这两个命令消息并不会影响Document内容(不像上一节的GRAPH 例那样),我们在CMainFrame中处理这两个命令消息颇为恰当。

    UI 对象           消息          消息处理例程
    ID_WINDOW_TEXT  COMMAND         OnWindowText
    ID_WINDOW_HEX   COMMAND         OnWindowHex
    
  • 利用ClassWizard 产生一个新类,准备做为同源子窗口的第二个View类:

    类名称             基类           文件
    CHexView             CView              HEXVIEW.CPP HEXVIEW.H
    
  • 修改程序代码,分别为两个view类都做出对应的Docment Template:

  • 修改CTextDoc 程序代码,添加成员变量。Document 的数据是10 笔字符串:

  • 修改CTextView::OnDraw 函数代码,在其中取得Document对象指针,然后把文字显现出来:

  • 修改CHexView程序代码,在OnDraw函数中取得Document对象指针,把ASCII 转换为Hex代码,再以文字显示出来:

  • 定义CMainFrame的两个命令处理例程:OnWindowText和OnWindowHex,使选单命令项目和工具列按钮得以发挥效用。函数内容直接拷贝自图 13-5,只要修改其中第14 行即可。这两个函数是本节的技术重点。

如果你要两个view 都有打印预视的能力,必须在CHexView 中改写下面三个虚函数,至于它们的内容,可以依样画葫芦地从CTextView 的同名函数中拷贝一份过来:

本例并未示范Serialization 动作。

非制式作法的缺点

既然是走后门,就难保哪一天出问题。如果MFC的版本变动,CMDIFrameWnd::OnWindowNew 内容改了,你就得注意本节这个方法还能适用否。

下面是Text 程序的执行画面。我先开启一个Text 窗口,再选按【Window/New Hex Window】或工具列上的对应按钮,开启另一个Hex 窗口。两个View 窗口以不同的方式显示同一份文件数据。

当你选按【File/Preview】命令项,哪一个窗口为 active 窗口,那个窗口的内容就出现在预视画面中。以下是Text窗口的打印预视画面:

以下是Hex窗口的打印预视画面:

多重文件

截至目前,我所谈的都是如何以不同的方式在不同的窗口中显示同一份文件数据。如果我想写那种「多功能」软件,必须支持许多种文件类型,该怎么办?

就以前一节的 Graph 程序为基础,继续我们的探索。Graph 的文件类型原本是一个整数数组,数量有10笔。我想在上面再多支持一种功能:文字编辑能力。

新的Document类

首先,我应该利用ClassWizard 新添一个Document 类,并以CDocument 为基础。启动 ClassWizard,选择【Member Variables】附页,按下【Add Class...】钮,出现对话框,填写如下:

下面是ClassWizard为我们做出来的代码:

注:阴影中的这两行代码(#0070 和 #0071)不是 ClassWizard 产生的,是我自己加的,提前与你见面。稍后我会解释为什么加这两行。

新的 Document Template

然后,我应该为此新的文件类型产生一个 Document Template,并把它加到系统所维护的DocTemplate 串列中。注意,为了享受现成的文字编辑能力,我选择CEditView 做为与此Document 搭配之View 类。还有,由于 CChildFrame 已经因为第一个文件类型Graph 的三叉静态分裂而被我们改写了 OnCreateClient 函数,已不再适用于这第二个文件类型(NewDoc),所以我决定直接采用 CMDIChildWnd 做为 NewDoc 文件类型的 MDIChild Frame 窗口:

CMultiDocTemplate 的第一个参数(resource ID)也不能再延用Graph 文件类型所使用的IDR_GRAPHTYPE了。要知道,这个ID值关系非常重大。我们得自行设计一套适用于NewDoc文件类型的UI系统出来(包括选单、工具列、文件存取对话框的内容、文件图标、窗口标题...)。

怎么做?第7章的深入讨论将在此开花结果!请务必回头复习复习「Document Template的意义」一节,我将直接动作,不再多做说明。

新的UI系统

下面就是为了这新的NewDoc文件类型所对应的UI系统,新添的文件内容(没有什么好工具可以帮忙,一般文字编辑器的copy/paste 最快):

新文件的文件读写动作

你大概还没有忘记,第7章最后曾经介绍过,当我们在 AppWizard 中选择 CEditView(而不是CView)作为我们的View类基础时,AppWizard 会为我们在 CMyDoc::Serialize函数中放入这样的代码:

void CMyDoc::Serialize(CArchive& ar)
{
    // CEditView contains an edit control which handles all serialization
    ((CEditView*)m_viewList.GetHead())->SerializeRaw(ar);
}

当你使用CEditView,编辑器窗口所承载的文字是放在 Edit 控制组件自己的一个内存区块中,而不是切割到Document 中。所以,文件的文件读写动作只要调用CEditView 的SerializeRaw 函数即可。

为了这NewDoc文件类型能够读写文件,我们也依样画葫芦地把上一段代码阴影部份加到Graph程序新的Document类去:

void CNewDoc::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        // TODO: add storing code here
    }
    else
    {
        // TODO: add loading code here
    }
    // CEditView contains an edit control which handles all serialization
    ((CEditView*)m_viewList.GetHead())->SerializeRaw(ar);
}

现在一切完备,重新编辑链接并执行。一开始,由于InitInstance 函数会自动为我们New一个新文件,而Graph 程序不知道该New 哪一种文件类型才好,所以会给我们这样的对话框:

往后每一次选按【File/New】,都会出现上述对话框。

以下是我们打开Graph 文件和NewDoc 文件各一份的画面。注意,当 active 窗口是NewDoc 文件,工具列上属于 Graph 文件所用的最后三个按钮是不起作用的:

以下是【Open】对话框(用来开文件)。注意,文件有 .fig 和 .txt 和 . 三种选择:

这个新的 Graph 版本放在书附光盘片的 \GRAPH2.13 目录中。