本講深入 MFC 文檔/視圖架構的「一檔多視」機制,實作雙視圖切換:CView 文字視圖與 CHtmlView 網頁視圖。詳細對比兩種切換方案:動態創建視圖 + CCreateContext (方案一) 與 CDocument::AddView/RemoveView (方案二),分析文檔關聯、指針管理、效能、跨視圖訪問等關鍵差異,為複雜多視圖應用提供架構選型參考。
| 模式 | 架構 | 典型場景 |
|---|---|---|
| 多文檔多視圖 (MDI) | 同一文檔,多個框架窗口,各含一視圖 (Window → New Window) | Word 多窗口編輯同一文件 |
| 動態分割視窗 | 單一框架窗口,CSplitterWnd 動態分割,多視圖同步顯示同一文檔 | 瀏覽器原始碼/預覽雙欄 |
| 靜態分割視窗 | 單一框架窗口,靜態分割,各視圖**不同方式**顯示同一文檔 | 圖形編輯器:頂部 3D 模型,底部屬性表 |
本講聚焦 **單文檔 (SDI) 手動切換視圖**:同一時刻僅一視圖可見,透過選單命令在「文字模式」與「HTML 瀏覽模式」間切換。
CHtmlViewCWebView,基類 CHtmlView (封裝 WebBrowser ActiveX 控件)。WebView.h 頂部加入 #include 。CWebView 添加 OnInitialUpdate 重寫:
void CWebView::OnInitialUpdate() {
CHtmlView::OnInitialUpdate();
// 導航至當前文檔路徑 (需文檔提供路徑)
CString strPath = GetDocument()->GetPathName();
if (!strPath.IsEmpty())
Navigate2(strPath, NULL, NULL, NULL, NULL);
}
ViewerApp.cpp 中 InitInstance 的 CSingleDocTemplate 第 4 參數暫時改為 RUNTIME_CLASS(CWebView) 測試運行 — 應能正確顯示 HTML 檔案。CViewerView,準備實作切換邏輯。CCreateContext (SwitchToView1)
// MainFrm.h 前置聲明 (解決循環包含)
class CViewerView;
class CWebView;
class CMainFrame : public CFrameWnd {
// ...
void SwitchToView1(CRuntimeClass* pViewClass);
int m_nViewID; // 1=文字, 2=HTML
};
// MainFrm.cpp
void CMainFrame::SwitchToView1(CRuntimeClass* pViewClass) {
CCreateContext context;
context.m_pCurrentDoc = GetActiveDocument(); // 關鍵:傳遞當前文檔指針
context.m_pNewViewClass = pViewClass;
context.m_pNewDocTemplate = GetActiveDocument()->GetDocTemplate();
CView* pNewView = (CView*)pViewClass->CreateObject();
if (!pNewView) return;
// 創建視圖窗口 (WS_CHILD | WS_VISIBLE, 0 矩形由框架調整)
if (!pNewView->Create(NULL, NULL, WS_CHILD | WS_VISIBLE, CRect(0,0,0,0), this, AFX_IDW_PANE_FIRST, &context)) {
delete pNewView;
return;
}
// 切換活動視圖
SetActiveView(pNewView, FALSE);
RecalcLayout();
pNewView->OnInitialUpdate(); // 觸發初始化 (如 Navigate2)
// 更新狀態
m_nViewID = (pViewClass == RUNTIME_CLASS(CViewerView)) ? 1 : 2;
}
void CMainFrame::OnViewText() {
if (m_nViewID != 1) SwitchToView1(RUNTIME_CLASS(CViewerView));
}
void CMainFrame::OnUpdateViewText(CCmdUI* pCmdUI) {
pCmdUI->SetCheck(m_nViewID == 1);
pCmdUI->Enable(m_nViewID != 1);
}
void CMainFrame::OnViewHtml() {
if (m_nViewID != 2) SwitchToView1(RUNTIME_CLASS(CWebView));
}
void CMainFrame::OnUpdateViewHtml(CCmdUI* pCmdUI) {
pCmdUI->SetCheck(m_nViewID == 2);
pCmdUI->Enable(m_nViewID != 2);
}
CViewerView 和 CWebView 的構造函數預設為 protected (MFC 動態創建要求),動態 CreateObject 需為 public,需在頭文件手動修改:
public: CViewerView(); // 原為 protected
CDocument::AddView/RemoveView (SwitchToView2)
// MainFrm.h 成員指針 (需手動添加,ClassWizard 不支援指針類型直接添加)
CViewerView* m_pMainView;
CWebView* m_pWebView;
// MainFrm.cpp
void CMainFrame::SwitchToView2(CRuntimeClass* pViewClass) {
CDocument* pDoc = GetActiveDocument();
if (!pDoc) return;
CView* pOldView = GetActiveView();
if (pOldView) pDoc->RemoveView(pOldView); // 移除舊視圖關聯
CView* pNewView = NULL;
if (pViewClass == RUNTIME_CLASS(CViewerView)) {
if (!m_pMainView) {
m_pMainView = new CViewerView();
m_pMainView->Create(NULL, NULL, WS_CHILD | WS_VISIBLE, CRect(0,0,0,0), this, AFX_IDW_PANE_FIRST, NULL);
}
pNewView = m_pMainView;
} else if (pViewClass == RUNTIME_CLASS(CWebView)) {
if (!m_pWebView) {
m_pWebView = new CWebView();
m_pWebView->Create(NULL, NULL, WS_CHILD | WS_VISIBLE, CRect(0,0,0,0), this, AFX_IDW_PANE_FIRST, NULL);
// 需傳遞文檔路徑給 WebView (因無 CCreateContext)
m_pWebView->m_strFileName = pDoc->GetPathName();
}
pNewView = m_pWebView;
}
if (pNewView) {
pDoc->AddView(pNewView); // 建立文檔關聯
SetActiveView(pNewView, FALSE);
RecalcLayout();
pNewView->OnInitialUpdate();
}
}
CWebView 配合修改
// WebView.h
public:
CString m_strFileName; // 文檔路徑
// WebView.cpp
void CWebView::OnInitialUpdate() {
CHtmlView::OnInitialUpdate();
if (!m_strFileName.IsEmpty())
Navigate2(m_strFileName, NULL, NULL, NULL, NULL);
// 移除原本 GetDocument()->GetPathName() (因無文檔關聯時 GetDocument 返回 NULL)
}
| 維度 | 方案一:CCreateContext 動態創建 | 方案二:AddView/RemoveView 手動管理 |
|---|---|---|
| 文檔關聯 | **創建時自動建立** (context.m_pCurrentDoc),GetDocument() 直接可用 | **需手動 AddView/RemoveView**,創建時無關聯,GetDocument() 返回 NULL |
| 視圖實例 | 每次切換 **創建新實例** (舊視圖銷毀),CViewerView 會被創建多次 | **複用實例** (成員指針保存),CViewerView 僅創建一次 |
| 跨視圖訪問 | 視圖指針在函數內部,**無法直接互訪** | 指針為主框架成員,**視圖間可透過 AfxGetMainWnd() 互訪** |
| 效能 | 頻繁創建/銷毀視圖窗口、DC、GDI 物件,切換較慢 | 僅切換顯示/隱藏,極快 |
| 狀態保持 | 視圖銷毀導致滾動位置、選取狀態丟失 (需手動序列化) | 視圖持久化,狀態自動保持 |
| 代碼複雜度 | 較低 (MFC 內建機制) | 需手動管理指針、生命週期、文檔關聯 |
| 適用場景 | 視圖輕量、狀態無需保持、切換頻率低 | 視圖重量級 (如 CHtmlView 載入網頁)、需保持狀態、高頻切換 |
CSplitterWnd (簡述)CMainFrame::OnCreateClient 中創建靜態分割器,兩行一列,分別放入 CViewerView 和 CWebView。GetDocument() (方案一) 或主框架指針 (方案二) 訪問數據,修改後調用 UpdateAllViews(NULL) 通知所有視圖 (含隱藏視圖) 刷新。delete m_pMainView; delete m_pWebView; (若非 NULL)。CHtmlView 基於 IE 控件,受系統縮放影響;現代專案建議遷移至 CWebView2 (Edge WebView2) 獲得 Chromium 內核、高 DPI 原生支援、ES6+ 支援。MFC 文檔/視圖架構的靈活性在於「一檔多視」的多種實現方式。理解 CCreateContext、AddView/RemoveView、CSplitterWnd 三大機制的本質差異 — **文檔關聯建立時機、視圖實例生命週期、指針可達性** — 是設計複雜多視圖應用 (如 IDE、CAD、瀏覽器、報表系統) 的關鍵架構決策能力。