標準 SDI (Single Document Interface) 僅支援單一視圖。若需如 Outlook Express 般在同一文檔下切換多種視圖 (列表/預覽/詳細資訊),需自行管理視圖物件生命週期並切換顯示。本文提供兩種成熟方案:靜態預建所有視圖並切換可見度、動態銷毀舊視圖並建立新視圖,含完整程式碼與關鍵步驟解析。
適用於視圖數量固定、切換頻繁、追求零延遲的場景。
BOOL CMyApp::InitInstance() {
// ... 標準文檔模板建立 ...
CFrameWnd* pFrame = (CFrameWnd*)m_pMainWnd;
CDocument* pDoc = pFrame->GetActiveDocument();
CCreateContext ctx;
ctx.m_pCurrentDoc = pDoc;
ctx.m_pNewViewClass = NULL;
ctx.m_pNewDocTemplate = NULL;
ctx.m_pLastView = NULL;
ctx.m_pCurrentFrame = pFrame;
// 視圖 0 由文檔模板自動建立 (AFX_IDW_PANE_FIRST)
m_pViews[0] = pFrame->GetActiveView();
// 手動建立視圖 1, 2 ... 設為不可見
for (int i = 1; i < NUM_VIEWS; i++) {
m_pViews[i] = (CView*)new CView1(); // 或 CView2, CFormView 等
UINT nID = AFX_IDW_PANE_FIRST + i;
CRect rect(0,0,0,0);
m_pViews[i]->Create(NULL, NULL,
AFX_WS_DEFAULT_VIEW & ~WS_VISIBLE, // 初始隱藏
rect, pFrame, nID, &ctx);
// 手動發送 WM_INITIALUPDATE
m_pViews[i]->OnInitialUpdate();
}
return TRUE;
}
CView* CMyApp::SwitchView(int nIndex) {
ASSERT(nIndex >= 0 && nIndex < NUM_VIEWS);
CView* pNewView = m_pViews[nIndex];
CFrameWnd* pFrame = (CFrameWnd*)m_pMainWnd;
CView* pActiveView = pFrame->GetActiveView();
if (!pActiveView || pNewView == pActiveView) return pActiveView;
// 交換視窗 ID,讓 RecalcLayout 正確運作
UINT idOld = ::GetWindowLong(pActiveView->m_hWnd, GWL_ID);
UINT idNew = ::GetWindowLong(pNewView->m_hWnd, GWL_ID);
::SetWindowLong(pActiveView->m_hWnd, GWL_ID, idNew);
::SetWindowLong(pNewView->m_hWnd, GWL_ID, idOld);
pActiveView->ShowWindow(SW_HIDE);
pNewView->ShowWindow(SW_SHOW);
pFrame->SetActiveView(pNewView);
pFrame->RecalcLayout();
pNewView->Invalidate();
return pActiveView;
}
適用於視圖類型繁多、記憶體敏感、或需依據文檔內容動態決定視圖類型的場景。
不可使用 GetActiveView() (可能為 NULL),需列舉子視窗尋找 CView 衍生類:
// 回呼函式
BOOL CALLBACK EnumViewProc(HWND hWnd, LPARAM lParam) {
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
CView** ppView = (CView**)lParam;
if (pWnd && pWnd->IsKindOf(RUNTIME_CLASS(CView))) {
*ppView = static_cast<CView*>(pWnd);
return FALSE; // 停止列舉
}
return TRUE;
}
// 刪除邏輯
CView* pView = NULL;
::EnumChildWindows(m_hWnd, EnumViewProc, (LPARAM)&pView);
if (pView) {
CView* pTopView = pView;
// 找到父視窗為 CMainFrame 的最上層視圖
while (lstrcmp(pTopView->GetRuntimeClass()->m_lpszClassName, _T("CMainFrame")) != 0) {
pView = pTopView;
pTopView = (CView*)pTopView->GetParent();
}
CDocument* pDoc = pView->GetDocument();
pDoc->m_bAutoDelete = FALSE; // 防止文檔被連帶刪除
pView->DestroyWindow();
pDoc->m_bAutoDelete = TRUE;
}
CCreateContext ctx;
ctx.m_pNewViewClass = RUNTIME_CLASS(CNewView); // 指定新視圖類別
ctx.m_pCurrentDoc = pDoc;
CView* pNewView = STATIC_DOWNCAST(CView, pFrame->CreateView(&ctx));
if (!pNewView) return FALSE;
pNewView->OnInitialUpdate(); // CreateView 不會自動呼叫
pNewView->ShowWindow(SW_SHOW);
pFrame->SetActiveView(pNewView);
pFrame->RecalcLayout();
| 特性 | 靜態法 | 動態法 |
|---|---|---|
| 記憶體佔用 | 常駐所有視圖 | 僅保留當前視圖 |
| 切換速度 | 極快 (ShowWindow) | 較慢 (Destroy/Create) |
| 視圖狀態保持 | 自動保持 (滾動位置、選取) | 需自行序列化/還原 |
| 實作複雜度 | 較低 | 需處理列舉、ID 衝突、文檔關聯 |
| 適用場景 | 固定 2-4 視圖、頻繁切換 | 視圖類型多、條件式建立 |
GWL_ID,動態法 CreateView 自動分配唯一 ID。m_bAutoDelete = FALSE。Create、動態法 CreateView) 皆需自行呼叫。CFrameWnd 客戶區,CSplitterWnd 另有機制。