文檔/視圖結(jié)構(gòu)中的各個局部是如何聯(lián)系到一起的
發(fā)表時間:2024-02-10 來源:明輝站整理相關(guān)軟件相關(guā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)的