MFC-文档视图

MDI程序

       MiniDraw只有一个About对话框,这回要把它变成一个MDI程序,借助于文档视图的威力,并不需要花很大的力气。

       MDI4个类组成:

主框架类,由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<afxwin.h>

#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 <afxwin.h>

#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 <afxwin.h>

#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 <afxwin.h>

#include "DrawDoc.h"

 

IMPLEMENT_DYNCREATE(CDrawDoc, CDocument)

 

CDrawDoc::CDrawDoc()

{

   

}

4个类全部只是空架,具体的事情已经由基类处理了,其中只有CDrawView覆盖OnDraw,因为它是一个抽象成员函数,所以不得不覆盖一下。

除了MainFrm类,其他三个类都声明了DYNCREATE宏,因而具备动态创建的能力。

接下来要在CDrawApp::InitInstance创建这些类实例,不过之前还得创建一些资源,比如菜单,资源字符串。

先为主窗口创建一个菜单,这是AFX强制要求的,否则有很多断言等着你,在Resource.rc里插入一个菜单,ID为:IDR_MAINFRAME,然后加几个菜单项,效果如下:

MFC-文档视图_第1张图片

接着为程序和主窗口创建一个图标,这回用导入的方式,毕竟有那么多现成的图标,何必自己来动手呢,导入:

MFC-文档视图_第2张图片

我们将导入的图标ID也命名为IDR_MAINFRAME,等会儿会说明原因。可执行文件的图标由ID值为1的图标决定,我们第一次导入的这个图标ID值即为1,打开resource.h看看就知道了,因此也成为了执行文件的图标。

接下来再为子窗口设置一个图标,用同样的方式,将ID命名为IDR_MFCMDITYPE

最后添加两个资源字符串,ID与上面相同,如下所示:

IDR_MAINFRAME用于指定主窗口的标题;IDR_MFCMDITYPE用于指定子窗口标题,打开对话框字符串等。

记住ID的命名只有两个IDR_MAINFRAMEIDR_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会帮你把子窗口创建出来。

最后,显示并更新主窗口。

运行程序,效果如下:

MFC-文档视图_第3张图片      

       我们纯手工打造了一个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后面跟随的12m_nUntitledCount决定。如果文件名不空,则进行打开操作,OnOpenDocument将有序列化行为,这是以后的主题了。

       最后更新框架。

 

       我们要重点看看视图类与文档如何关联:

CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pDoc, CFrameWnd* pOther)

{

    //指定创建视图所需要的信息,包括文档类

    CCreateContext context;

    context.m_pCurrentFrame = pOther;

    context.m_pCurrentDoc = pDoc;

    context.m_pNewViewClass = m_pViewClass;

    context.m_pNewDocTemplate = this;

    //动态创建框架类

    CFrameWnd* pFrame = (CFrameWnd*)m_pFrameClass->CreateObject();

    //加载框架,注意&context))

    if (!pFrame->LoadFrame(m_nIDResource,

            WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE,

            NULL, & context))

    return pFrame;

}

       有两个地方需要注意,m_nIDResource正是在InitInstance新建模板时的IDR_MFCMDITYPE,这个资源在接下来的流程中会用于指定子框架的图标,标题栏等;另外,context包括了很多信息,这些信息是视图类所必需的。

       子框架创建到视图创建的路很长,我们将这之间的流程用下面的调用树表示:

CMDIChildWnd::LoadFrame

加载子框架

  CFrameWnd::GetIconWndClass

注册窗口类

    CDrawChildFrm::Create

创建窗口

      CMDIChildWnd::OnCreate

WM_CREATE引起的OnCreate处理函数

              CFrameWnd::OnCreateClient

创建客户区。

                   CFrameWnd::CreateView

创建视图

       OnCreateClient是一个虚函数,CFrameWnd的做法是调用CreateView创建视图,你可以覆盖OnCreateClient创新自定义的视图。

       CreateView利用pContext来动态创建:

CWnd* CFrameWnd::CreateView(CCreateContext* pContext, UINT nID)

{

    // 动态创建视图类

    CWnd* pView = (CWnd*)pContext->m_pNewViewClass->CreateObject();

    // 创建视图窗口

    pView->Create(NULL, NULL, AFX_WS_DEFAULT_VIEW,

        CRect(0,0,0,0), this, nID, pContext));

    return pView;

}

       至此,文档模板的三个类全部用上了。现在只剩下一件事情,视图与文档类的关联,在CView::OnCreate里面完成:

int CView::OnCreate(LPCREATESTRUCT lpcs)

{

    ...

    CCreateContext* pContext = (CCreateContext*)lpcs->lpCreateParams;

    //一个文档对应多个视图

    pContext->m_pCurrentDoc->AddView(this);

    return 0;

}

 

void CDocument::AddView(CView* pView)

{

    m_viewList.AddTail(pView);

    pView->m_pDocument = this;

    OnChangedViewList();}

       m_pDocument的值正是由此而来,在视图类内部,直接用m_pDocument可获得文档类,在视图类外部,用GetDocument()来获得。

       另外,从AddTail也可以证明,一个文档可以对应多个视图。

       新建的流程结束了,RTTI在这里居功至伟,如果没有RTTI,这一连带的创建动作几乎是没有办法完成的。

FQA

下面是编写MDI程序时思考的一些问题,我把它们当成一系列FQA放在这里,如果你有一些其他的实用经验,可以在这里分享给大家。

l         如何让MDI子窗口一创建就显示为最大化?

CDrawChildFrm类覆盖PreCreateWindow函数,指定创建样式:

BOOL CDrawChildFrm::PreCreateWindow( CREATESTRUCT& cs )

{

    cs.style |=WS_MAXIMIZE | WS_VISIBLE;

    return CMDIChildWnd::PreCreateWindow(cs);

}

 

l         视图类只是一个简单的窗口,可以指定其他的视图吗,比如一个Edit视图?

可以,将CDrawView的基类改为CEditView,其他类型的视图与此相似。

 

l         如上文指定菜单项,运行后文件菜单会多出子窗口列表菜单,如何将这些菜单放到“窗口”菜单项下。

MDI程序默认将子窗口列表放在倒数第二个子菜单下,所以在帮助菜单之前新建一个“窗口”子菜单,这是系统行为。

 

l         如何给菜单项指定加速键?

资源编辑器新建Accelerator类型的资源,命名同样为IDR_MAINFRAME;增加一项,项ID与某个菜单项的ID相同,比如为ID_FILE_NEW,然后指定加速键值。

       加了这一项后,新建菜单已有快捷键的功能了,因为pMainFrm->LoadFrame会默认调用LoadAccelTable

       新建菜单项的标题更改一下,变为新建(&N)/tCtrl+N,让使用者知道可以用快捷键来新建文档。

 

l         如何解决激活子窗口出现闪烁的问题?

很多程序都存在这个问题(包括VC6),解决的办法就是激活或新建的时候锁住客户区,结束后再解锁。

第一步是激活的情况,处理MDIClientWM_MDIACTIVATE消息,由于MDIClient没有子类化,所以只有自己来做,在MainFrm声明静态成员和函数:

static WNDPROC m_OldClientProc;

static LRESULT CALLBACK ClientWndProc( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam );

       MainFrm创建后子类化MDIClient

int CMainFrm::OnCreate( LPCREATESTRUCT lpCreateStruct )

{

    int nRet;

    nRet = CMDIFrameWnd::OnCreate(lpCreateStruct); 

    //子类化客户区窗口过程

    if (m_hWndMDIClient != 0)

        m_OldClientProc = (WNDPROC)::SetWindowLong(m_hWndMDIClient, GWL_WNDPROC, (LONG)ClientWndProc);

    return nRet;

}

       ClientWndProc里处理WM_MDIACTIVATE

WNDPROC CMainFrm::m_OldClientProc = NULL;

LRESULT CMainFrm::ClientWndProc( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam )

{

    LRESULT nRet;

    if (Msg == WM_MDIACTIVATE)

        ::SendMessage(hWnd, WM_SETREDRAW, FALSE, 0);

   

    if (m_OldClientProc != NULL)

        nRet = (*CMainFrm::m_OldClientProc)(hWnd, Msg, wParam, lParam);

   

    if (Msg == WM_MDIACTIVATE)

    {

        ::SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);

        ::RedrawWindow(hWnd,NULL,0,RDW_FRAME|RDW_INVALIDATE |RDW_ALLCHILDREN|RDW_NOINTERNALPAINT);

    }

    return nRet;

}

       第二步是新建的情况,CDrawChildFrm覆盖父类的Create函数:

BOOL CDrawChildFrm::Create( LPCTSTR lpszClassName, LPCTSTR lpszWindowName,

    DWORD dwStyle, const RECT& rect, CMDIFrameWnd* pParentWnd,

    CCreateContext* pContext )

{

    BOOL bRet;

    CMDIFrameWnd* pMainWnd = (CMDIFrameWnd*)(AfxGetThread()->m_pMainWnd);

    HWND hClient = pMainWnd->m_hWndMDIClient;

    if (hClient != 0)

        ::SendMessage(hClient, WM_SETREDRAW, FALSE, 0);

 

    bRet = CMDIChildWnd::Create(lpszClassName, lpszWindowName, dwStyle, rect, pParentWnd, pContext);

 

    if (hClient != 0)

    {

        ::SendMessage(hClient, WM_SETREDRAW, TRUE, 0);

        ::RedrawWindow(hClient,NULL,0, RDW_FRAME|RDW_INVALIDATE|RDW_ALLCHILDREN|RDW_NOINTERNALPAINT);

    }

    return bRet;

}

       处理完之后,你的MDI就再也不会闪了,效果请看上面的Demo程序。

 

你可能感兴趣的:(框架,null,Class,文档,mfc,callback)