文檔/視圖結構是MFC中最有特色而又有難度的部分,在這當中涉及了應用、文檔模板、文檔、視圖、MDI框架窗口、MDI子窗口等不同的對象,如果不了解 這些部分之間如何關聯的話,就可能犯錯誤,也就很難編出有水平的文檔/視圖程序。比如我在初學VC編程的時候,為應用程序添加了兩個文檔模板,兩個模板公 用一個文檔類,只是視圖不一樣,期望當一個模板的文檔的視圖改變了文檔后,調用UpdateAllViews后也能更新另一個文檔模板的視圖,結果當然是 不行的,原因就是對MFC的文檔/視圖結構沒有深入的了解,了解的最好方法就是閱讀一下MFC的源代碼。下面就是我的筆記:
(一)應用程序對象與文檔模板之間的聯系:
首先,在應用程序對象中有一個CDocManager指針類型的共有數據成員m_pDocManager,在CDocManager中維護一個 CPtrList類型的鏈表:m_tempateList,它是一個保護成員。InitInstance函數中調用CWinApp:: AddDocTemplate函數,實際上是調用m_pDocManager的AddDocTemplate函數向鏈表m_templateList添加 模板指針。CWinApp提供了GetFirstDocTemplatePosition和GetNextDocTemplate函數實現對 m_templateList鏈表的訪問(實際上是調用了CDocManager的相關函數)。
在文件操作方面CWinApp提供的最常用的功能是文件的新建(OnFileNew)和打開(OnFileOpen),它也是調用CDocManager 類的同名函數。對于新建,一般的時候在只有一個文檔模板的時候,它新建一個空白的文件;如果有多個文檔模板的時候,它會出現一個對話框提示選擇文檔類型。 它的源代碼如下:
void CDocManager::OnFileNew()
{
if (m_templateList.IsEmpty())
{
.......
return;
}
//取第一個文檔模板的指針
CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead();
if (m_templateList.GetCount() > 1)
{
// 如果多于一個文檔模板,出現對話框提示用戶去選擇
CNewTypeDlg dlg(&m_templateList);
int nID = dlg.DoModal();
if (nID == IDOK)
pTemplate = dlg.m_pSelectedTemplate;
else
return; // none - cancel operation
}
......
//參數為NULL的時候OpenDocument File會新建一個文件
pTemplate->OpenDocumentFile(NULL);
}
打開文件:
void CDocManager::OnFileOpen()
{
// 出現打開文件對話框
CString newName;
if (!DoPromptFileName(newName, AFX_IDS_OPENFILE,
OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, TRUE, NULL))
return; // open cancelled
AfxGetApp()->OpenDocumentFile(newName); //實際也是調用文檔模板的同名函數
}
(二)文檔模板與文檔之間的聯系:
從上面看出應用程序對象對文件的新建和打開是依靠文檔模板的OpenDocumentFile函數實現的。MFC的模板類是用來聯系文檔類、視類和框架類的,在它的構造函數就需要這三者的信息:
CDocTemplate ( UINT nIDResource, CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass );
構造函數利用后三個參數為它的三個CruntimeClass*類型的保護成員賦值:
m_pDocClass = pDocClass;
m_pFrameClass = pFrameClass;
m_pViewClass = pViewClass;
文檔模板分為單文檔模板和多文檔模板兩種,這兩個模板的實現是不同的,除了上面的三個成員,內部有彼此不相同的但是很重要的成員變量。對于多文檔模板: CPtrList m_docList;,單文檔模板:CDocument* m_pOnlyDoc;。它們都有一個成員函數AddDocument,分別各自的成員進行賦值操作,而在它們的父類的CDocTemplate中則是為 它所添加的文檔的m_pDocTemplate變量賦值為模板自己的地址:
void CDocTemplate::AddDocument(CDocument* pDoc)
{
ASSERT_VALID(pDoc);
ASSERT(pDoc->m_pDocTemplate == NULL);
pDoc->m_pDocTemplate = this;
}
由于單文檔模板只能擁有一個文檔,所以它只是維護一個指向自己所擁有的模板的指針:m_pOnlyDoc,AddDocument函數就是要為這個成員賦值:
void CSingleDocTemplate::AddDocument(CDocument* pDoc)
{
......
CDocTemplate::AddDocument(pDoc);
m_pOnlyDoc = pDoc;
}
由于多文檔模板可以擁有多個文檔,所以它要維護的是包含它所打開的所有文檔的指針的鏈表,所以它的AddDocument的實現為:
void CMultiDocTemplate::AddDocument(CDocument* pDoc)
{
......
CDocTemplate::AddDocument(pDoc);
m_docList..AddTail(pDoc);
}
模板通過m_pOnlyDoc(單文檔)或記住了自己所擁有的所有的模板的指針,并通過GetFirstDocPosition和GetNextDoc函 數可以實現對它所擁有的文檔的訪問,同時使文檔記住了自己所屬文檔模板的指針,同時文檔提供了GetDocTemplate()函數可以取得它所屬的模 板。
對AddDocument函數的調用主要是發生在另一個成員函數CreateNewDocument里,它的作用是創建一個新的文檔:
CDocument* CDocTemplate::CreateNewDocument()
{
if (m_pDocClass == NULL)
{
……
}
CDocument* pDocument = (CDocument*)m_pDocClass->CreateObject();
……
AddDocument(pDocument);
return pDocument;
}
CreateNewDocument函數主要利用文檔類的運行時指針的函數CreateObject創建一個新文檔對象,并利用AddDocument將其指針賦給相關的成員,留做以后使用。
在應用程序的OnFileNew和OnFileOpen函數都使用了模板的OpenDocumentFile函數,而且在實際編程的時候也大都使用這個函 數。在MSDN的文檔說這個函數當參數不為NULL的時候打開文件,否則就用上面所說的CreateNewDocument函數創建一個新文檔,那么它是 如何實現的呢?
CDocument* CSingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,
BOOL bMakeVisible)
{
CDocument* pDocument = NULL;
CFrameWnd* pFrame = NULL;
BOOL bCreated = FALSE; // => doc and frame created
BOOL bWasModified = FALSE;
//如果已經有打開的文檔,就會詢問否保存文件
if (m_pOnlyDoc != NULL)
{
pDocument = m_pOnlyDoc;
if (!pDocument->SaveModified())
return NULL;
pFrame = (CFrameWnd*)AfxGetMainWnd();
......
}
//創建新文件
else
{
pDocument = CreateNewDocument();
ASSERT(pFrame == NULL);
bCreated = TRUE;
}
......
//如果第一次創建文檔則也要創建框架窗口。
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)
{
// 為新文檔設置默認標題
SetDefaultTitle(pDocument);
……
//一般的時候重載OnNewDocument初始化一些數據,如果返回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函數重新初始化文檔對象
if (!pDocument->OnOpenDocument(lpszPathName))
{
if (bCreated)
{
//新建文檔的情況
pFrame->DestroyWindow();
}
else if (!pDocument->IsModified())
{
// 文檔沒有被修改,恢復原來文檔的修改標志
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的實現
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)
//當是新建的時候
{
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
{
// 打開一個已經存在的文件
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函數里,利用CreateNewDocument對象使文檔對象與模板對象建立了聯系,利用了CreateNewFrame函數使框架窗口與文檔、視圖、模板發生了聯系: