明輝手游網(wǎng)中心:是一個免費提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺!

文檔/視圖結(jié)構(gòu)中的各個局部是如何聯(lián)系到一起的

[摘要]文檔/視圖結(jié)構(gòu)是MFC中最有特色而又有難度的部分,在這當(dāng)中涉及了應(yīng)用、文檔模板、文檔、視圖、MDI框架窗口、MDI子窗口等不同的對象,如果不了解這些部分之間如何關(guān)聯(lián)的話,就可能犯錯誤,也就很難編出有水平的文檔/視圖程序。比如我在初學(xué)VC編程的時候,為應(yīng)用程序添加了兩個文檔模板,兩個模板公用一個文檔...
     文檔/視圖結(jié)構(gòu)是MFC中最有特色而又有難度的部分,在這當(dāng)中涉及了應(yīng)用、文檔模板、文檔、視圖、MDI框架窗口、MDI子窗口等不同的對象,如果不了解這些部分之間如何關(guān)聯(lián)的話,就可能犯錯誤,也就很難編出有水平的文檔/視圖程序。比如我在初學(xué)VC編程的時候,為應(yīng)用程序添加了兩個文檔模板,兩個模板公用一個文檔類,只是視圖不一樣,期望當(dāng)一個模板的文檔的視圖改變了文檔后,調(diào)用UpdateAllViews后也能更新另一個文檔模板的視圖,結(jié)果當(dāng)然是不行的,原因就是對MFC的文檔/視圖結(jié)構(gòu)沒有深入的了解,了解的最好方法就是閱讀一下MFC的源代碼。下面就是我的筆記:

(一)應(yīng)用程序?qū)ο笈c文檔模板之間的聯(lián)系:

        首先,在應(yīng)用程序?qū)ο笾杏幸粋CDocManager指針類型的共有數(shù)據(jù)成員m_pDocManager,在CDocManager中維護一個CPtrList類型的鏈表:m_tempateList,它是一個保護成員。InitInstance函數(shù)中調(diào)用CWinApp::AddDocTemplate函數(shù),實際上是調(diào)用m_pDocManager的AddDocTemplate函數(shù)向鏈表m_templateList添加模板指針。CWinApp提供了GetFirstDocTemplatePosition和GetNextDocTemplate函數(shù)實現(xiàn)對m_templateList鏈表的訪問(實際上是調(diào)用了CDocManager的相關(guān)函數(shù))。

         在文件操作方面CWinApp提供的最常用的功能是文件的新建(OnFileNew)和打開(OnFileOpen),它也是調(diào)用CDocManager類的同名函數(shù)。對于新建,一般的時候在只有一個文檔模板的時候,它新建一個空白的文件;如果有多個文檔模板的時候,它會出現(xiàn)一個對話框提示選擇文檔類型。它的源代碼如下:

void CDocManager::OnFileNew()

{

       if (m_templateList.IsEmpty())

       {

                                .......

              return;

       }

                //取第一個文檔模板的指針

       CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead();

       if (m_templateList.GetCount() > 1)

       {

              // 如果多于一個文檔模板,出現(xiàn)對話框提示用戶去選擇

              CNewTypeDlg dlg(&m_templateList);

              int nID = dlg.DoModal();

              if (nID == IDOK)

                     pTemplate = dlg.m_pSelectedTemplate;

              else

                     return;     // none - cancel operation

       }

                ......

                //參數(shù)為NULL的時候OpenDocument File會新建一個文件

       pTemplate->OpenDocumentFile(NULL);

}

打開文件:

void CDocManager::OnFileOpen()

{

       // 出現(xiàn)打開文件對話框

       CString newName;

       if (!DoPromptFileName(newName, AFX_IDS_OPENFILE,

         OFN_HIDEREADONLY OFN_FILEMUSTEXIST, TRUE, NULL))

              return; // open cancelled



       AfxGetApp()->OpenDocumentFile(newName);          //實際也是調(diào)用文檔模板的同名函數(shù)

}

(二)文檔模板與文檔之間的聯(lián)系:

        從上面看出應(yīng)用程序?qū)ο髮ξ募男陆ê痛蜷_是依靠文檔模板的OpenDocumentFile函數(shù)實現(xiàn)的。MFC的模板類是用來聯(lián)系文檔類、視類和框架類的,在它的構(gòu)造函數(shù)就需要這三者的信息:

CDocTemplate ( UINT nIDResource, CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass );

構(gòu)造函數(shù)利用后三個參數(shù)為它的三個CruntimeClass*類型的保護成員賦值:

       m_pDocClass = pDocClass;

       m_pFrameClass = pFrameClass;

       m_pViewClass = pViewClass;

    文檔模板分為單文檔模板和多文檔模板兩種,這兩個模板的實現(xiàn)是不同的,除了上面的三個成員,內(nèi)部有彼此不相同的但是很重要的成員變量。對于多文檔模板:CPtrList m_docList;,單文檔模板:CDocument* m_pOnlyDoc;。它們都有一個成員函數(shù)AddDocument,分別各自的成員進行賦值操作,而在它們的父類的CDocTemplate中則是為它所添加的文檔的m_pDocTemplate變量賦值為模板自己的地址:

void CDocTemplate::AddDocument(CDocument* pDoc)

{

       ASSERT_VALID(pDoc);

       ASSERT(pDoc->m_pDocTemplate == NULL);   

       pDoc->m_pDocTemplate = this;

}

由于單文檔模板只能擁有一個文檔,所以它只是維護一個指向自己所擁有的模板的指針:m_pOnlyDoc,AddDocument函數(shù)就是要為這個成員賦值:

void CSingleDocTemplate::AddDocument(CDocument* pDoc)

{

                ......

       CDocTemplate::AddDocument(pDoc);

       m_pOnlyDoc = pDoc;

}
由于多文檔模板可以擁有多個文檔,所以它要維護的是包含它所打開的所有文檔的指針的鏈表,所以它的AddDocument的實現(xiàn)為:

void CMultiDocTemplate::AddDocument(CDocument* pDoc)

{

                ......

       CDocTemplate::AddDocument(pDoc);

       m_docList..AddTail(pDoc);

}
    模板通過m_pOnlyDoc(單文檔)或記住了自己所擁有的所有的模板的指針,并通過GetFirstDocPosition和GetNextDoc函數(shù)可以實現(xiàn)對它所擁有的文檔的訪問,同時使文檔記住了自己所屬文檔模板的指針,同時文檔提供了GetDocTemplate()函數(shù)可以取得它所屬的模板。

對AddDocument函數(shù)的調(diào)用主要是發(fā)生在另一個成員函數(shù)CreateNewDocument里,它的作用是創(chuàng)建一個新的文檔:

CDocument* CDocTemplate::CreateNewDocument()

{

       if (m_pDocClass == NULL)

       {

         ……

       }

       CDocument* pDocument = (CDocument*)m_pDocClass->CreateObject();

    ……

       AddDocument(pDocument);

       return pDocument;

}

CreateNewDocument函數(shù)主要利用文檔類的運行時指針的函數(shù)CreateObject創(chuàng)建一個新文檔對象,并利用AddDocument將其指針賦給相關(guān)的成員,留做以后使用。

    在應(yīng)用程序的OnFileNew和OnFileOpen函數(shù)都使用了模板的OpenDocumentFile函數(shù),而且在實際編程的時候也大都使用這個函數(shù)。在MSDN的文檔說這個函數(shù)當(dāng)參數(shù)不為NULL的時候打開文件,否則就用上面所說的CreateNewDocument函數(shù)創(chuàng)建一個新文檔,那么它是如何實現(xiàn)的呢?

CDocument* CSingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,

       BOOL bMakeVisible)

{

       CDocument* pDocument = NULL;

       CFrameWnd* pFrame = NULL;

       BOOL bCreated = FALSE;      // => doc and frame created

       BOOL bWasModified = FALSE;

    //如果已經(jīng)有打開的文檔,就會詢問否保存文件

       if (m_pOnlyDoc != NULL)

       {

              pDocument = m_pOnlyDoc;

              if (!pDocument->SaveModified())

                     return NULL;      



              pFrame = (CFrameWnd*)AfxGetMainWnd();

                                ......

       }

    //創(chuàng)建新文件

       else

       {

              pDocument = CreateNewDocument();

              ASSERT(pFrame == NULL);     

              bCreated = TRUE;

       }

                ......

    //如果第一次創(chuàng)建文檔則也要創(chuàng)建框架窗口。

       if (pFrame == NULL)

       {

              ASSERT(bCreated);



              // create frame - set as main document frame

              BOOL bAutoDelete = pDocument->m_bAutoDelete;

              pDocument->m_bAutoDelete = FALSE;

              pFrame = CreateNewFrame(pDocument, NULL);

              pDocument->m_bAutoDelete = bAutoDelete;

                                ......

       }



       if (lpszPathName == NULL)

       {

              // 為新文檔設(shè)置默認標題

              SetDefaultTitle(pDocument);

        ……

      //一般的時候重載OnNewDocument初始化一些數(shù)據(jù),如果返回FALSE,表示初始化失//敗,銷毀窗口。

              if (!pDocument->OnNewDocument())

              {

                                                ......

                     if (bCreated)

                            pFrame->DestroyWindow();    // will destroy document

                     return NULL;

              }

       }

       else

       {

              CWaitCursor wait;



              // open an existing document

              bWasModified = pDocument->IsModified();

              pDocument->SetModifiedFlag(FALSE);

              //OnOpenDocument函數(shù)重新初始化文檔對象

              if (!pDocument->OnOpenDocument(lpszPathName))

              {

                     if (bCreated)

                     {

                //新建文檔的情況

                            pFrame->DestroyWindow();    

                     }

                     else if (!pDocument->IsModified())

                     {

                            // 文檔沒有被修改,恢復(fù)原來文檔的修改標志

                            pDocument->SetModifiedFlag(bWasModified);

                     }

                     else

                     {

                            // 修改了原始的文檔

                            SetDefaultTitle(pDocument);



                            if (!pDocument->OnNewDocument())

                            {

                                   TRACE0("Error: OnNewDocument failed after trying to open a document - trying to continue.\n");

                            }

                     }

                     return NULL;        // open failed

              }

              pDocument->SetPathName(lpszPathName);

       }



       CWinThread* pThread = AfxGetThread();

       if (bCreated && pThread->m_pMainWnd == NULL)

       {

              pThread->m_pMainWnd = pFrame;

       }

       InitialUpdateFrame(pFrame, pDocument, bMakeVisible);



       return pDocument;

}

以下是多文檔模板的OpenDocumentFile的實現(xiàn)

CDocument* CMultiDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,

       BOOL bMakeVisible)

{

       //新建一個文檔對象

       CDocument* pDocument = CreateNewDocument();

……



       BOOL bAutoDelete = pDocument->m_bAutoDelete;

       pDocument->m_bAutoDelete = FALSE;   

       CFrameWnd* pFrame = CreateNewFrame(pDocument, NULL);

       pDocument->m_bAutoDelete = bAutoDelete;

……



       if (lpszPathName == NULL)

    //當(dāng)是新建的時候

       {

              SetDefaultTitle(pDocument);



              // avoid creating temporary compound file when starting up invisible

              if (!bMakeVisible)

                     pDocument->m_bEmbedded = TRUE;



              if (!pDocument->OnNewDocument())

              {

                     pFrame->DestroyWindow();

                     return NULL;

              }



              m_nUntitledCount++;

       }

       else

       {

              // 打開一個已經(jīng)存在的文件

              CWaitCursor wait;

              if (!pDocument->OnOpenDocument(lpszPathName))

              {

                     // user has be alerted to what failed in OnOpenDocument

                     TRACE0("CDocument::OnOpenDocument returned FALSE.\n");

                     pFrame->DestroyWindow();

                     return NULL;

              }

              pDocument->SetPathName(lpszPathName);

       }



       InitialUpdateFrame(pFrame, pDocument, bMakeVisible);

       return pDocument;

}

    從上面看出模板類的OpenDocumentFile函數(shù)里,利用CreateNewDocument對象使文檔對象與模板對象建立了聯(lián)系,利用了CreateNewFrame函數(shù)使框架窗口與文檔、視圖、模板發(fā)生了聯(lián)系:



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

{

       if (pDoc != NULL)

              ASSERT_VALID(pDoc);



       ASSERT(m_nIDResource != 0); // 必須有資源ID

       CCreateContext context;

       context.m_pCurrentFrame = pOther;

       context.m_pCurrentDoc = pDoc;

       context.m_pNewViewClass = m_pViewClass;

       context.m_pNewDocTemplate = this;



       if (m_pFrameClass == NULL)

       {

           ……

       }

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

       if (pFrame == NULL)

       {

        ……

              return NULL;

       }

       ASSERT_KINDOF(CFrameWnd, pFrame);



       if (context.m_pNewViewClass == NULL)

              TRACE0("Warning: creating frame with no default view.\n");



       if (!pFrame->LoadFrame(m_nIDResource,

                     WS_OVERLAPPEDWINDOW FWS_ADDTOTITLE,   // default frame styles

                     NULL, &context))

       {

……

              return NULL;

       }

       return pFrame;

}

總結(jié):在模板里使用自己的數(shù)據(jù)結(jié)構(gòu)維護著自己擁有的文檔對象,并提供了GetFirstDocPosition和GetNextDoc函數(shù)實現(xiàn)對這些文檔的對象的訪問。所以,在一個擁有多個文檔模板的應(yīng)用程序中,即使每個模板使用了相同類型的文檔類,每個新建或打開的文檔在這些文檔模板之間也不是共享的。

(三)文檔與視圖之間的聯(lián)系

在視圖類有一個保護數(shù)據(jù)成員:CDocument* m_pDocument;,這個文檔指針指向視圖對象所屬的文檔,視圖里常用的函數(shù)GetDocument()就是返回的這個指針;在文檔類有一個保護數(shù)據(jù)成員:CDocument* m_viewList;,它保存的是所有正在顯示該文檔的視圖的指針,通過CDocument的成員函數(shù)GetFirstViewPosition和GetNextView函數(shù)可以實現(xiàn)對這些視圖的訪問。

在視圖被創(chuàng)建的時候,在OnCreate函數(shù)里視圖和文檔發(fā)生了關(guān)聯(lián):

int CView::OnCreate(LPCREATESTRUCT lpcs)

{

       if (CWnd::OnCreate(lpcs) == -1)

              return -1;

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



       if (pContext != NULL && pContext->m_pCurrentDoc != NULL)

       {

              pContext->m_pCurrentDoc->AddView(this);

              ASSERT(m_pDocument != NULL);

       }

       else

       {

              TRACE0("Warning: Creating a pane with no CDocument.\n");

       }



       return 0;   

}

這個關(guān)聯(lián)是通過文檔類的AddView函數(shù)實現(xiàn)的:

void CDocument::AddView(CView* pView)

{

    ……

       m_viewList.AddTail(pView);

       pView->m_pDocument = this;



       OnChangedViewList();   

}

在這個函數(shù)里,首先文檔對象先把所添加的視圖指針加到自己的視圖鏈表里,然后指向自己的指針賦給了所添加的視圖的m_pDocument成員。

眾所周知,文檔與視圖進行通信的方式先調(diào)用文檔的UpdateAllViews函數(shù),從而調(diào)用視圖的OnUpdate函數(shù):

void CDocument::UpdateAllViews(CView* pSender, LPARAM lHint, CObject* pHint)

       // walk through all views

{

    //視圖鏈表不能為空且發(fā)送者不能為空

       ASSERT(pSender == NULL !m_viewList.IsEmpty());

       POSITION pos = GetFirstViewPosition();

       while (pos != NULL)

       {

              CView* pView = GetNextView(pos);

              ASSERT_VALID(pView);

      //不調(diào)用發(fā)送者的OnUpdate函數(shù)

              if (pView != pSender)

                     pView->OnUpdate(pSender, lHint, pHint);

       }

}

在視圖的OnUpdate函數(shù)里默認的實現(xiàn)僅是通知視圖進行重畫:

Invalidate(TRUE);

我們一般重載這個更新視圖的某些數(shù)據(jù)或進行其他操作,比如更新視圖滾動條的滾動范圍。

(四)框架窗口與文檔、視圖之間的聯(lián)系

在框架窗口被創(chuàng)建的時候,創(chuàng)建了視圖,相關(guān)的函數(shù)如下:

int CFrameWnd::OnCreate(LPCREATESTRUCT lpcs)

{

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

       return OnCreateHelper(lpcs, pContext);

}

int CFrameWnd::OnCreateHelper(LPCREATESTRUCT lpcs, CCreateContext* pContext)

{

       if (CWnd::OnCreate(lpcs) == -1)

              return -1;



       // create special children first

       if (!OnCreateClient(lpcs, pContext))

       {

              TRACE0("Failed to create client pane/view for frame.\n");

              return -1;

       }



       // post message for initial message string

       PostMessage(WM_SETMESSAGESTRING, AFX_IDS_IDLEMESSAGE);



       // make sure the child windows have been properly sized

       RecalcLayout();



       return 0;   // create ok

}

BOOL CFrameWnd::OnCreateClient(LPCREATESTRUCT, CCreateContext* pContext)

{

       // default create client will create a view if asked for it

       if (pContext != NULL && pContext->m_pNewViewClass != NULL)

       {

              if (CreateView(pContext, AFX_IDW_PANE_FIRST) == NULL)

                     return FALSE;

       }

       return TRUE;

}

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

{



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

       if (pView == NULL)

       {

              return NULL;

       }

       ASSERT_KINDOF(CWnd, pView);



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

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

       {

              return NULL;        // can't continue without a view

       }



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

       {

              ModifyStyleEx(WS_EX_CLIENTEDGE, 0, SWP_FRAMECHANGED);

       }

       return pView;

}

在文檔模板的OpenDocumentFile函數(shù)發(fā)生了如下的調(diào)用:

InitialUpdateFrame(pFrame, pDocument, bMakeVisible);

文檔模板的這個函數(shù)的實現(xiàn)為:

pFrame->InitialUpdateFrame(pDoc, bMakeVisible);

實際是調(diào)用了框架窗口的同名函數(shù):

void CFrameWnd::InitialUpdateFrame(CDocument* pDoc, BOOL bMakeVisible)

{

       CView* pView = NULL;

       if (GetActiveView() == NULL)

       {

              //取主視圖

              CWnd* pWnd = GetDescendantWindow(AFX_IDW_PANE_FIRST, TRUE);

              if (pWnd != NULL && pWnd->IsKindOf(RUNTIME_CLASS(CView)))

              {

        //主視圖存在且合法,把當(dāng)前的主視圖設(shè)置為活動視圖

                     pView = (CView*)pWnd;

                     SetActiveView(pView, FALSE);

              }

       }



       if (bMakeVisible)

       {

              SendMessageToDescendants(WM_INITIALUPDATE, 0, 0, TRUE, TRUE);



              if (pView != NULL)

                     pView->OnActivateFrame(WA_INACTIVE, this);

        ……

              ActivateFrame(nCmdShow);

              if (pView != NULL)

                     pView->OnActivateView(TRUE, pView, pView);

       }



       // update frame counts and frame title (may already have been visible)

       if (pDoc != NULL)

              pDoc->UpdateFrameCounts();

       OnUpdateFrameTitle(TRUE);

}

上面的函數(shù)中對視圖的操作主要是用SetActiveView設(shè)置了活動視圖,并且調(diào)用了視圖的OnActivateFrame函數(shù)。在CFrameWnd類中維護著一個保護成員:CView* m_pViewActive;,SetAcitveView函數(shù)主要就是對它進行操作:

void CFrameWnd::SetActiveView(CView* pViewNew, BOOL bNotify)

{

       CView* pViewOld = m_pViewActive;

       if (pViewNew == pViewOld)

              return;     // do not re-activate if SetActiveView called more than once



       m_pViewActive = NULL;   // no active for the following processing



       // deactivate the old one

       if (pViewOld != NULL)

              pViewOld->OnActivateView(FALSE, pViewNew, pViewOld);



       if (m_pViewActive != NULL)

              return;     // already set

       m_pViewActive = pViewNew;



       // activate

       if (pViewNew != NULL && bNotify)

              pViewNew->OnActivateView(TRUE, pViewNew, pViewOld);

}

CFrameWnd還有另一個函數(shù)返回這個成員:

CView* CFrameWnd::GetActiveView() const

{

       ASSERT(m_pViewActive == NULL

              m_pViewActive->IsKindOf(RUNTIME_CLASS(CView)));

       return m_pViewActive;

}

CframeWnd還有一個函數(shù)能取得當(dāng)前活動的文檔,它是通過活動視圖間接得到的:

CDocument* CFrameWnd::GetActiveDocument()

{

       ASSERT_VALID(this);

       CView* pView = GetActiveView();

       if (pView != NULL)

              return pView->GetDocument();

       return NULL;

}

(五)MDI主窗口和子窗口之間的關(guān)聯(lián):

在MDI子窗口創(chuàng)建的時候,指定了它與MDI之間的關(guān)系:

BOOL CMDIChildWnd::Create(LPCTSTR lpszClassName,

       LPCTSTR lpszWindowName, DWORD dwStyle,

       const RECT& rect, CMDIFrameWnd* pParentWnd,

       CCreateContext* pContext)

{

       if (pParentWnd == NULL)

       {

              CWnd* pMainWnd = AfxGetThread()->m_pMainWnd;

              ASSERT(pMainWnd != NULL);

              ASSERT_KINDOF(CMDIFrameWnd, pMainWnd);

              pParentWnd = (CMDIFrameWnd*)pMainWnd;

       }

    ……

       pParentWnd->RecalcLayout();



       CREATESTRUCT cs;

……

//指定了所屬的MDI子窗口

       cs.hwndParent = pParentWnd->m_hWnd;

    ……

       cs.lpCreateParams = (LPVOID)pContext;



       if (!PreCreateWindow(cs))

       {

              PostNcDestroy();

              return FALSE;

       }

       MDICREATESTRUCT mcs;

    ……

       mcs.style = cs.style & ~(WS_MAXIMIZE WS_VISIBLE);

       mcs.lParam = (LONG)cs.lpCreateParams;



       AfxHookWindowCreate(this);

    //發(fā)送WM_MDICREATE消息,創(chuàng)建了MDI子窗口

       HWND hWnd = (HWND)::SendMessage(pParentWnd->m_hWndMDIClient,

              WM_MDICREATE, 0, (LPARAM)&mcs);

       if (!AfxUnhookWindowCreate())

              PostNcDestroy();        // cleanup if MDICREATE fails too soon

    ……

       return TRUE;

}

當(dāng)MDI子窗口創(chuàng)建了以后,MDI主窗口就可以用自己的函數(shù)實現(xiàn)對子窗口的管理,例如取得當(dāng)前的活動子窗口是通過發(fā)送WM_MDIGETACTIVE消息實現(xiàn)的