MDI程序
MiniDraw只有一个About对话框,这回要把它变成一个MDI程序,借助于文档视图的威力,并不需要花很大的力气。
MDI由4个类组成:
主框架类,由CMDIFrameWnd派生而来,表示程序的MDI父窗口。
子框架类,由CMDIChildWnd派生而来,表示程序的MDI子窗口。
视图类,由CView派生而来,表示一个文档视图,内嵌于子窗口。
文档类,由CDocument派生而来,表示一份“文档”,一份“文档”可以由多个视图表现。
现在创建这些类,全部通过新建头文件和源文件生成,下面它们的代码:
MainFrm.h
#ifndef LINZHENQUN_MAINFMR_H_
#define LINZHENQUN_MAINFMR_H_
class CMainFrm: public CMDIFrameWnd
{
DECLARE_DYNAMIC(CMainFrm)
public:
CMainFrm();
};
#endif //LINZHENQUN_MAINFMR_H_
MainFrm.cpp
#include
#include "MainFrm.h"
IMPLEMENT_DYNAMIC(CMainFrm, CMDIFrameWnd)
CMainFrm::CMainFrm()
{
}
DrawChildFrm.h
#ifndef LINZHENQUN_DRAWCHILDFRM_H_
#define LINZHENQUN_DRAWCHILDFRM_H_
class CDrawChildFrm: public CMDIChildWnd
{
DECLARE_DYNCREATE(CDrawChildFrm)
public:
CDrawChildFrm();
};
#endif //LINZHENQUN_DRAWCHILDFRM_H_
DrawChildFrm.cpp
#include
#include "DrawChildFrm.h"
IMPLEMENT_DYNCREATE(CDrawChildFrm, CMDIChildWnd)
CDrawChildFrm::CDrawChildFrm()
{
}
DrawView.h
#ifndef LINZHENQUN_DRAWVIEW_H_
#define LINZHENQUN_DRAWVIEW_H_
class CDrawView: public CView
{
DECLARE_DYNCREATE(CDrawView)
public:
CDrawView();
protected:
virtual void OnDraw(CDC* pDC);
};
#endif //LINZHENQUN_DRAWVIEW_H_
DrawView.cpp
#include
#include "DrawView.h"
IMPLEMENT_DYNCREATE(CDrawView, CView)
CDrawView::CDrawView()
{
}
void CDrawView::OnDraw( CDC* pDC )
{
}
DrawDoc.h
#ifndef LINZHENQUN_DRAWDOC_H_
#define LINZHENQUN_DRAWDOC_H_
class CDrawDoc: public CDocument
{
DECLARE_DYNCREATE(CDrawDoc)
public:
CDrawDoc();
};
#endif //LINZHENQUN_DRAWDOC_H_
DrawDoc.cpp
#include
#include "DrawDoc.h"
IMPLEMENT_DYNCREATE(CDrawDoc, CDocument)
CDrawDoc::CDrawDoc()
{
}
4个类全部只是空架,具体的事情已经由基类处理了,其中只有CDrawView覆盖OnDraw,因为它是一个抽象成员函数,所以不得不覆盖一下。
除了MainFrm类,其他三个类都声明了DYNCREATE宏,因而具备动态创建的能力。
接下来要在CDrawApp::InitInstance创建这些类实例,不过之前还得创建一些资源,比如菜单,资源字符串。
先为主窗口创建一个菜单,这是AFX强制要求的,否则有很多断言等着你,在Resource.rc里插入一个菜单,ID为:IDR_MAINFRAME,然后加几个菜单项,效果如下:
接着为程序和主窗口创建一个图标,这回用导入的方式,毕竟有那么多现成的图标,何必自己来动手呢,导入:
我们将导入的图标ID也命名为IDR_MAINFRAME,等会儿会说明原因。可执行文件的图标由ID值为1的图标决定,我们第一次导入的这个图标ID值即为1,打开resource.h看看就知道了,因此也成为了执行文件的图标。
接下来再为子窗口设置一个图标,用同样的方式,将ID命名为IDR_MFCMDITYPE。
最后添加两个资源字符串,ID与上面相同,如下所示:
IDR_MAINFRAME用于指定主窗口的标题;IDR_MFCMDITYPE用于指定子窗口标题,打开对话框字符串等。
记住ID的命名只有两个IDR_MAINFRAME和IDR_MFCMDITYPE,分别对应于主窗口和子窗口。
加完资源,到CDrawApp::InitInstance写点代码,让主窗口显示出来:
BOOL CDrawApp::InitInstance()
{
//添加文档模板
CMultiDocTemplate *pTemplate = new CMultiDocTemplate(
IDR_MFCMDITYPE,
RUNTIME_CLASS(CDrawDoc),
RUNTIME_CLASS(CDrawChildFrm),
RUNTIME_CLASS(CDrawView)
);
AddDocTemplate(pTemplate);
//创建主窗口
CMainFrm* pMainFrm = new CMainFrm();
pMainFrm->LoadFrame(IDR_MAINFRAME);
m_pMainWnd = pMainFrm;
//默认新建一个文档子窗口
OnFileNew();
//显示主窗口
pMainFrm->ShowWindow(m_nCmdShow);
pMainFrm->UpdateWindow();
return TRUE;
}
第一件事情是创建一个文档模板,一个文档模板对应一种类型的“文档”,这些模板由一个管理器管理着,以后借助于RTTI来创建文档窗口。
第二件事情是创建主窗口,通过LoadFrame创建窗口,IDR_MAINFRAME在这里被用上了,LoadFrame不单会用这个ID来指定主窗口的菜单,还用这个ID来指定窗口图标和标题,这就是为什么在创建资源的时候要用一个相同的ID来命名几种资源。主窗口创建完后交由CWinApp的成员m_pMainWnd保管。
第三件事情是用OnFileNew创建一个子窗口,里面的代码仅仅调用m_pDocManager->OnFileNew(),而AFX会帮你把子窗口创建出来。
最后,显示并更新主窗口。
运行程序,效果如下:
我们纯手工打造了一个MDI程序,尽管上面已经说明了步骤,但仍然留给我们很多疑问,比如子窗口怎么创建出来的,文档与视图如何关联起来,这其中的奥妙就在文档视图的框架。
在分析文档视图的流程之前,可以从这里获得源代码。
文档模板
一个MDI程序有多个文档窗口,每一个文档窗口的内容可以不同,就像VS6一样,代码和资源分别为不同的文档窗口表示。
文档模板正是代表这样一种抽象,它由视图文档以及子框架类组件,RIIT在这里起了非常重要的作用,文档模板保存的是各个类的运行时结构,在必要的时候才利用运行时结构动态创建类实例。在我们的例子中,只有一种文档类型,所以只AddDocTemplate一次。
CWinApp有一个CDocManager类,专门用来管理文档模板,AddDocTemplate将一个文档模板加进CDocManager里面。
void CWinApp::AddDocTemplate(CDocTemplate* pTemplate)
{
if (m_pDocManager == NULL)
m_pDocManager = new CDocManager;
m_pDocManager->AddDocTemplate(pTemplate);
}
模板管理器采用延迟创建的方法,这样做是很有道理的,因为有些程序并不使用文档视图框架,可能只是一个对话框,如果一开始就创建模板管理器,就造成不必要的浪费了。
新建过程
新建文档调用CWinApp::OnFileNew:
void CWinApp::OnFileNew()
{
if (m_pDocManager != NULL)
m_pDocManager->OnFileNew();
}
m_pDocManager->OnFileNew取第一个模板类,如果存在多个模板类,则弹出一个对话框让用户选择。然后调用pTemplate->OpenDocumentFile(NULL)进行文档打开操作。
弹出对话框的行为个人觉得不是很好,并不是每一个程序都有这样的需求,或者说有些程序想要自己的选择方式。另外,这个选择对话框对于以后的本地化会成为一个问题。
真正的流程在CMultiDocTemplate::OpenDocumentFile,它创建了所有必须的类:
CDocument* CMultiDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,
BOOL bMakeVisible)
{
//创建文档类
CDocument* pDocument = CreateNewDocument();
//创建框架类
CFrameWnd* pFrame = CreateNewFrame(pDocument, NULL);
//文件名为空,表示是新建
if (lpszPathName == NULL)
{
// 设置一个默认的标题名
SetDefaultTitle(pDocument);
// 新建文档打开通过
pDocument->OnNewDocument();
// 文档计数,标题设置
m_nUntitledCount++;
}
else
{
//打开一个存在的文档
pDocument->OnOpenDocument(lpszPathName);
//文档的路径名
pDocument->SetPathName(lpszPathName);
}
//更新子框架
InitialUpdateFrame(pFrame, pDocument, bMakeVisible);
return pDocument;
}
它首先创建文档类和框架类,视图类会在框架类创建时连带被创建,等会儿看看视图类怎样与文档类关联起来。
接着分两种情况来处理,如果文件名为空,则进行新建操作,我们看到默认标题是这样表示出来的,Document后面跟随的1、2由m_nUntitledCount决定。如果文件名不空,则进行打开操作,OnOpenDocument将有序列化行为,这是以后的主题了。
最后更新框架。
我们要重点看看视图类与文档如何关联:
CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pDoc, CFrameWnd* pOther)
{
//指定创建视图所需要的信息,包括文档类
CCreateContext context;
border-right: medium none; padding-right: 0cm; border-top: medium none;