1. 程式人生 > >MFC中的Document-View結構

MFC中的Document-View結構

MFC之所以為Application Framework,最重要的一個特徵就是它能夠將管理資料的程式程式碼和負責顯示資料的程式程式碼分離開來,而這種能力有Document/View提供。

想要實現資料管理和顯示的分離,需要搞清楚一些幾個問題:

1.  程式的哪個部分持有資料

2.  程式的哪個部分負責更新資料

3.  如何以多種方式顯示資料

4.  如何讓資料的更改有一致性

5.  如何實現資料持久化

6.  如何管理使用者介面。不同的資料型別可能需要不同的使用者介面,而一個程式可能管理多種型別的資料。

其實Document/View結構與當下很流行的MVC架構有異曲同工之妙。其間的對應關係可以這樣認為:M-Document, V-View,C-Document Template。

Document

Document其實就是指資料。Document在MFC的CDocument中被例項化。CDocument本身並無實際用途,它只提供一個框架。當我們開發程式時,應該從CDocument派生出一個屬於自己的Document類,並且在類中宣告一些成員變數,用以容納資料。然後再(至少)改寫專門負責檔案讀寫操作的Serialize函式。當然,APPWizard會為我們搭好框架,我們只要往裡面填就行了。例如:

類宣告中:
virtual void Serialize(CArchive& ar);

類定義中:

void CMyDoc::Serialize(CArchive& ar)
{
	if (ar.IsStoring())
	{
		// TODO: add storing code here
	}
	else
	{
		// TODO: add loading code here
	}
}

View

View負責呈現Document中的資料。View在MFC的CView中被例項化。當我們開發自己的程式時,應該從CView派生出一個屬於自己的View類,並且在類中(至少)改寫專門負責顯示資料的OnDraw函式(針對螢幕)或Onprint函式(針對印表機)。同樣,APPWizard也會為我們搭好框架,如下:

類宣告中:
virtual void OnDraw(CDC* pDC);  // overridden to draw this view

類實現中:

void CMFCView::OnDraw(CDC* pDC)
{
	CMFCDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	// TODO: add draw code for native data here
}

由於CView派生子CWnd,所以它可以接收一般的Windows訊息(如WM_SIZE、WM_PAINT等),又派生自CCmdTarget,所以它可以接收自選單或工具欄的WM_COMMAND訊息。

在傳統的C/SDK程式中,當視窗函式收到WM_PAINT時,我們開始獲得DC並繪製畫面。而在MFC中,一旦WM_PAINT發生,Framework就會自動呼叫OnDraw函式。

View其實是一個沒有邊框的視窗。真正出現時,其外圍還有一個有邊框的視窗,我們稱之為Frame視窗。

Frame

    Frame負責視窗顯示時的UI管理。之所以不然View插手這件事而由Frame全權處理,涉及到框架設計層面的考慮。有時候功能之間需要一定的關聯度,但是這種關係又不能太強。把UI管理隔離出來,可以降低彼此之間的依賴程度,也可以是該功能重複適用於各種場合。

Document Template

MFC把Document/View/Frame視為三位一體,這個整體就由DocumentTemplate來掌管。

如果程式中能夠處理多種資料型別,就必須製造多個Document Template出來,並使用AddDocTemplate函式將它們一一加入系統之中。這和程式是不是MDI(Multiple Document Interface,就是所謂的多文件介面)無關。如果程式支援多種資料型別,但卻是個SDI(Single Document Interface,就是所謂的單文件介面),那隻不過表示我們每次只能開啟一份檔案罷了。

CDocTemplate管理CDocument/CView/CFrameWnd

由Document Template來管理Document/View/Frame,那麼誰又是Document Template的管理者呢?其實是CWinApp,可以看其InitInstance函式中的操作:

{
	CMultiDocTemplate* pDocTemplate;
	pDocTemplate = new CMultiDocTemplate(
		IDR_MFCTYPE,
		RUNTIME_CLASS(CMFCDoc),
		RUNTIME_CLASS(CChildFrame), // custom MDI child frame
		RUNTIME_CLASS(CMFCView));
	AddDocTemplate(pDocTemplate);
}

聯絡檔案的新建和開啟操作,如下圖(摘自:《深入淺出MFC》):

當使用者單擊【檔案/新建】命令項時,根據APPWizard為我們所做的Message Map( ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)  ),此命令由CWinApp::OnFileNew函式接手處理。後者呼叫CDocManager::OnFileNew,後者再呼叫CWinApp::OpenDocumentFile,後者再呼叫CDocManager:: OpenDocumentFile,後者再呼叫CMultiDocTemplate::OpenDocumentFile。最後呼叫的函式主要操作如下(定義於DOCMULTI.CPP):

CDocument*CMultiDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,

    BOOLbMakeVisible)

{

    CDocument*pDocument = CreateNewDocument();

    if(pDocument == NULL)

    {

       TRACE0("CDocTemplate::CreateNewDocumentreturned NULL.\n");

       AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);

       returnNULL;

    }

    ASSERT_VALID(pDocument);

    BOOLbAutoDelete = pDocument->m_bAutoDelete;

    pDocument->m_bAutoDelete= FALSE;   // don't destroy if somethinggoes wrong

    CFrameWnd*pFrame = CreateNewFrame(pDocument, NULL);

    pDocument->m_bAutoDelete= bAutoDelete;

    if(pFrame == NULL)

    {

       AfxMessageBox(AFX_IDP_FAILED_TO_CREATE_DOC);

       deletepDocument;       // explicit delete onerror

       returnNULL;

    }

    ASSERT_VALID(pFrame);

    if(lpszPathName == NULL)

    {

       //create a new document - with default document name

           ……

    }

    else

    {

       //open an existing document

       ……

    }

    InitialUpdateFrame(pFrame,pDocument, bMakeVisible);

    returnpDocument;

}

其中CreateNewDocument函式(定義於DOCTEMPL.CPP)的操作如下:

CDocument* CDocTemplate::CreateNewDocument()
{
	// default implementation constructs one from CRuntimeClass
	if (m_pDocClass == NULL)
	{
		TRACE0("Error: you must override CDocTemplate::CreateNewDocument.\n");
		ASSERT(FALSE);
		return NULL;
	}
	CDocument* pDocument = (CDocument*)m_pDocClass-> CreateObject();//動態建立Document物件
	if (pDocument == NULL)
	{
		TRACE1("Warning: Dynamic create of document type %hs failed.\n",
			m_pDocClass->m_lpszClassName);
		return NULL;
	}
	ASSERT_KINDOF(CDocument, pDocument);
	AddDocument(pDocument);
	return pDocument;
}

正如所料,其中動態產生了Document物件。

CreateNewFrame函式的主要操作如下:

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

{

    if(pDoc != NULL)

       ASSERT_VALID(pDoc);

    //create a frame wired to the specified document

    ASSERT(m_nIDResource!= 0); // must have a resource ID to load from

    CCreateContextcontext;

    context.m_pCurrentFrame= pOther;

    context.m_pCurrentDoc= pDoc;

    context.m_pNewViewClass= m_pViewClass;

    context.m_pNewDocTemplate= this;

……

    CFrameWnd*pFrame = (CFrameWnd*)m_pFrameClass->CreateObject();//動態建立Document Frame物件

    ……

    //create new from resource

    if(!pFrame->LoadFrame(m_nIDResource,

           WS_OVERLAPPEDWINDOW| FWS_ADDTOTITLE,   // default framestyles

           NULL, &context))

    {

       TRACE0("Warning:CDocTemplate couldn't create a frame.\n");

       //frame will be deleted in PostNcDestroy cleanup

       returnNULL;

    }

    //it worked !

    returnpFrame;

}

顯然上面動態建立了所需的Frame,但是View卻是蹤跡全無。真實如此嗎?實則不然。上程式碼出現了一個context變數,該變數有一個欄位指向m_pViewClass。回想CFrameWnd::Create操作的最後一個引數,它正是context。當CFrameWnd收到WM_CREATE時會觸發CFrameWnd::OnCreate(由Message Map指定),其呼叫次序如下:

CFrameWnd::OnCreate-> CFrameWnd::OnCreateHelper -> CFrameWnd::OnCreateClient -> CFrameWnd::CreateView

其中最後呼叫的函式(定義於WINFRM.CPP)主要操作如下:

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

{

    ……

    CWnd*pView = (CWnd*)pContext->m_pNewViewClass->CreateObject();//動態生成View物件

    ……

    //views are always created with a border!

    if(!pView->Create(NULL, NULL,AFX_WS_DEFAULT_VIEW,

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

    {

       TRACE0("Warning:could not create view for frame.\n");

       returnNULL;        // can't continue without aview

    }

    if(afxData.bWin4 && (pView->GetExStyle() & WS_EX_CLIENTEDGE))

    {

       //remove the 3d style from the frame, since the view is

       //  providing it.

       //make sure to recalc the non-client area

       ModifyStyleEx(WS_EX_CLIENTEDGE,0, SWP_FRAMECHANGED);

    }

    returnpView;

}

由上可見,不僅View物件被動態生成,其對應的實際視窗也以Create函式產生出來。

CDocTemplate、CDocument、CView、CFrameWnd關聯關係

1.  CWinApp擁有一個物件指標:CDocManager * m_pDocManager

2.  CDocManager擁有一個指標連結串列CPtrList m_templateList,用來維護一系列的DocumentTemplate。應用程式在CMyWinApp::InitInstance中以AddDocTemplate將這些Document Templates加入到有CDocManager所維護的連結串列之中。

3.  CDocTemplate擁有三個成員變數,分別持有Document、View、Frame的CRuntimeClass指標,另有一個成員變數m_nIDResource,用來表示此Document顯示時應該採用的UI物件。這四份資料在CMyWinApp::InitInstance函式構造CDocTemplate時指定,稱為建構函式的引數。

4.  CDocument有一個成員變數CDocTemplate * m_pDocTemplate,回指其DocumentTemplate;另外有一個成員變數CPtrList m_viewList,表示它可以同時維護一組Views。

5.  CFrameWnd有一個成員變數CView * m_pViewActive,指向當前活動的View。

6.  CView有一個成員變數CDocument * m_pDocument,指向相關的Document。

關係圖示如下(該圖作者水平很高,無法超越,只有借花獻佛了):