本文提供了在主框架和選項卡視圖中建立分割窗口,在分割窗口中建立選項卡視圖並實現視圖切換,這樣分割窗口和選項卡視圖就能循環嵌套使用了,本Demo項目的源碼在Github上可供下載:https://github.com/fenggwsx/SplitterWndTabViewCombined-Demo
新建解決方案
為了方便演示,我在創建MFC項目時,選擇的應用程序類型為單文檔,項目樣式為MFC standard:
創建完成后,首先在頭文件framework.h
中包含頭文件afxcview.h
,因為等下用到的CTreeView
在這個頭文件里,接着在pch.h
中包含Demo項目下的頭文件MainFrm.h
,然后編譯運行,界面如圖所示:
在主框架中創建分割窗口
先添加兩個類,分別為CIndexTreeView
(繼承自CTreeView
)和CView1
(繼承自CView
),CIndexTreeView
用來做索引的,為后續視圖切換做准備,CView1
是用來看顯示效果的,為了讓它能夠易於辨識,我們需要在該類中寫入一些繪圖代碼
先來寫一下CIndexTreeView
中的代碼,第一步是要讓該類具有動態創建的功能,所以在頭文件中添加如下代碼 :
protected:
CIndexTreeView() noexcept;
DECLARE_DYNCREATE(CIndexTreeView)
在源文件CIndexTreeView.cpp
中添加如下代碼:
IMPLEMENT_DYNCREATE(CIndexTreeView, CTreeView)
第二步,打開類向導,響應WM_CREATE
和TVN_SELCHANGED
消息,重寫虛函數PreCreateWindow
第三步,在OnCreate
函數中寫入如下代碼:
int CIndexTreeView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTreeView::OnCreate(lpCreateStruct) == -1)
return -1;
TVINSERTSTRUCT tvInsert;
HTREEITEM hRootItem;
tvInsert.hInsertAfter = NULL;
tvInsert.hParent = TVI_ROOT;
tvInsert.item.mask = LVFIF_TEXT;
tvInsert.item.pszText = _T("Root");
hRootItem = GetTreeCtrl().InsertItem(&tvInsert);
GetTreeCtrl().InsertItem(_T("Node1"), hRootItem);
GetTreeCtrl().InsertItem(_T("Node2"), hRootItem);
GetTreeCtrl().Expand(hRootItem, TVE_EXPAND);
return 0;
}
這樣,我們已經將CIndexTreeView
的節點都建立好了
第四步,在PreCreateWindow
函數中寫入如下代碼:
BOOL CIndexTreeView::PreCreateWindow(CREATESTRUCT& cs)
{
cs.style |= TVS_SHOWSELALWAYS | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS;
return CTreeView::PreCreateWindow(cs);
}
這些代碼是為了修改CIndexTreeView
的一些樣式,所以這一步不是必須的
接着寫CView1
中的代碼,第一步同樣是要讓它有動態創建的功能,代碼與CIndexTreeView
中的類似,只需要將其中的名稱改為相應的CView1
中的名稱
第二步是要重寫純虛函數OnDraw
,因為是純虛函數,所以必須重寫,在函數中寫入如下代碼:
void CView1::OnDraw(CDC* pDC)
{
CRect rect;
GetClientRect(&rect);
pDC->DrawText(CString(GetThisClass()->m_lpszClassName), &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
這些繪圖命令會在視圖的中央繪制出視圖類的類名稱
然后寫CMainFrame
中的代碼,第一步是在CMainFrame
類的頭文件MainFrm.h
中聲明成員變量:
protected:
CSplitterWnd m_wndSplitterWnd;
第二步,重寫虛函數OnCreateClient
,代碼如下,,包含相應頭文件(CIndexTreeView
和CView1
):
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
m_wndSplitterWnd.CreateStatic(this, 1, 2);
m_wndSplitterWnd.CreateView(0, 0, RUNTIME_CLASS(CIndexTreeView), CSize(200, 0), pContext);
m_wndSplitterWnd.CreateView(0, 1, RUNTIME_CLASS(CView1), CSize(0, 0), pContext);
return TRUE;
}
編譯運行,可以看到如下界面,界面被分成了左右兩塊區域,左邊是CIndexTreeView
,右邊是CView1
:
創建選項卡視圖
首先我們要新建視圖CView2
,與CView1
相同,可以將CView1
中的代碼復制過來,更改類名即可
接下來我們要創建選項卡視圖,添加類CMyTabView
繼承自CTabView
(因為只有一個選項卡視圖,所以不用下標)
第一步,同樣是要讓CMyTabView
支持動態創建,這里不再贅述
第二步,響應WM_CREATE
消息,在OnCreate
函數中寫入如下代碼,包含相應頭文件(View2.h
):
int CMyTabView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTabView::OnCreate(lpCreateStruct) == -1)
return -1;
GetTabControl().SetLocation(CMFCTabCtrl::LOCATION_TOP);
GetTabControl().ModifyTabStyle(CMFCTabCtrl::STYLE_FLAT);
AddView(RUNTIME_CLASS(CView2), CString(RUNTIME_CLASS(CView2)->m_lpszClassName));
return 0;
}
實現分割窗口的視圖切換
首先在CMainFrame
中添加函數Switch
:
public:
void Switch(int nIndex);
接着在Switch
函數中寫入如下代碼:
void CMainFrame::Switch(int nIndex)
{
switch (nIndex)
{
case 0:
m_wndSplitterWnd.DeleteView(0, 1);
m_wndSplitterWnd.CreateView(0, 1, RUNTIME_CLASS(CView1), CSize(0, 0), NULL);
break;
case 1:
m_wndSplitterWnd.DeleteView(0, 1);
m_wndSplitterWnd.CreateView(0, 1, RUNTIME_CLASS(CMyTabView), CSize(0, 0), NULL);
break;
}
m_wndSplitterWnd.RecalcLayout();
}
然后在CIndexTreeView
的OnTvnSelchanged
函數中寫入代碼:
void CIndexTreeView::OnTvnSelchanged(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
HTREEITEM hRootItem = GetTreeCtrl().GetRootItem();
HTREEITEM hCurItem = pNMTreeView->itemNew.hItem;
if (hCurItem != hRootItem)
{
int nIndex = 0;
HTREEITEM hItem = GetTreeCtrl().GetChildItem(hRootItem);
while (hItem)
{
if (hItem == hCurItem)
break;
hItem = GetTreeCtrl().GetNextSiblingItem(hItem);
nIndex++;
}
CMainFrame* pFrame = DYNAMIC_DOWNCAST(CMainFrame, AfxGetMainWnd());
if (pFrame != NULL)
{
pFrame->Switch(nIndex);
pFrame->SetActiveView(this);
}
}
*pResult = 0;
}
最后編譯運行,點擊左邊目錄樹上的Node2
節點,可以看到如下界面:
改進視圖切換的方式
可以看到,在CMainFrame
的Switch
函數中,我們是通過刪除分割窗格中原有的視圖然后重新建立(推倒重建)的方法來實現視圖的切換,但是當視圖中要顯示大量數據時,使用這種方法可能會導致卡頓的問題,所以我們可以使用另一種策略,通過顯示和隱藏達到視圖切換的目的,當然原來這種推倒重建的方法在數據量少的情況下是沒有問題的
首先我們會發現,CSplitterWnd
中沒有綁定視圖的操作,我們只能通過調用它的CreateView
來創建視圖,然而在調用時,我們只能通過RUNTIME_CLASS(class_name)
告訴它要創建的視圖類型,它會去新建一個視圖,對於我們已有的視圖,是無法直接綁定上去的
其次,CSplitterWnd
的每一個窗格中只支持一個視圖,如果將兩個視圖建在同一個窗格中程序就會報錯
於是我通過分析CSplitterWnd
中GetPane
函數的源碼明白了CSplitterWnd
運作機理,找到了解決方案,以下是GetPane
函數的源碼:
CWnd* CSplitterWnd::GetPane(int row, int col) const
{
ASSERT_VALID(this);
CWnd* pView = GetDlgItem(IdFromRowCol(row, col));
ASSERT(pView != NULL); // panes can be a CWnd, but are usually CViews
return pView;
}
可以看到,GetPane
函數僅僅是通過GetDlgItem
來獲取窗口指針的,所以窗口的ID號決定了窗口所在的位置,而同一個ID號有多個窗口會導致GetDlgItem
返回NULL
,進而引發程序報錯
再來看看CSplitterWnd
中IdFromRowCol
的源碼:
int CSplitterWnd::IdFromRowCol(int row, int col) const
{
ASSERT_VALID(this);
ASSERT(row >= 0);
ASSERT(row < m_nRows);
ASSERT(col >= 0);
ASSERT(col < m_nCols);
return AFX_IDW_PANE_FIRST + row * 16 + col;
}
#define AFX_IDW_PANE_FIRST 0xE900 // first pane (256 max)
#define AFX_IDW_PANE_LAST 0xE9ff
可以看到,CSplitterWnd
中窗口的ID號,是從0xE900到0xE9ff,共256個,這也是CSplitterWnd
的窗口分割最多支持16行16列的原因,了解了CSplitterWnd
的工作方式,我們就可以通過改變視圖的ID號和ShowWindow
函數來實現顯示和隱藏了
首先我們要找一個0xE900到0xE9ff之外的ID號,這里直接選擇0xFFFF
聲明兩個視圖類的指針作為CMainFrame
的成員變量(這樣我們就可以對視圖進行管理了):
protected:
CView1* m_pView1;
CMyTabView* m_pMyTabView;
修改CMainFrame
中的OnCreateClient
函數:
BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
{
m_wndSplitterWnd.CreateStatic(this, 1, 2);
m_wndSplitterWnd.CreateView(0, 0, RUNTIME_CLASS(CIndexTreeView), CSize(200, 0), pContext);
m_pView1 = DYNAMIC_DOWNCAST(CView1, RUNTIME_CLASS(CView1)->CreateObject());
m_pMyTabView = DYNAMIC_DOWNCAST(CMyTabView, RUNTIME_CLASS(CMyTabView)->CreateObject());
m_pView1->Create(NULL, NULL, WS_CHILD,
CRect(0, 0, 0, 0), &m_wndSplitterWnd, 0xFFFF, pContext);
m_pMyTabView->Create(NULL, NULL, WS_CHILD,
CRect(0, 0, 0, 0), &m_wndSplitterWnd, 0xFFFF, pContext);
Switch(0);
return TRUE;
}
注意Switch(0);
語句不能漏掉,不然沒有一個視圖的ID是m_wndSplitterWnd.IdFromRowCol(0,1)
會導致分割窗口找不到ID號所對應的窗口而出錯
修改Switch
函數:
void CMainFrame::Switch(int nIndex)
{
switch (nIndex)
{
case 0:
::SetWindowLong(m_pView1->m_hWnd, GWL_ID, m_wndSplitterWnd.IdFromRowCol(0,1));
m_pView1->ShowWindow(SW_SHOW);
::SetWindowLong(m_pMyTabView->m_hWnd, GWL_ID, 0xFFFF);
m_pMyTabView->ShowWindow(SW_HIDE);
break;
case 1:
::SetWindowLong(m_pView1->m_hWnd, GWL_ID, 0xFFFF);
m_pView1->ShowWindow(SW_HIDE);
::SetWindowLong(m_pMyTabView->m_hWnd, GWL_ID, m_wndSplitterWnd.IdFromRowCol(0, 1));
m_pMyTabView->ShowWindow(SW_SHOW);
break;
}
m_wndSplitterWnd.RecalcLayout();
}
重新編譯運行,可以看到實現了同樣的切換效果
在選項卡視圖中創建分割窗口
首先我們要新建視圖CView3
,這個視圖中代碼的結構也可以從CView1
中復制過來,但是要刪除OnDraw
中的代碼(不要刪除函數的聲明與定義,因為OnDraw
是純虛函數)
接着修改CMyTabView
的OnCreate
函數:
int CMyTabView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTabView::OnCreate(lpCreateStruct) == -1)
return -1;
GetTabControl().SetLocation(CMFCTabCtrl::LOCATION_TOP);
GetTabControl().ModifyTabStyle(CMFCTabCtrl::STYLE_FLAT);
CCreateContext context;
context.m_pCurrentDoc = GetDocument();
context.m_pCurrentFrame = NULL;
context.m_pLastView = NULL;
context.m_pNewDocTemplate = NULL;
context.m_pNewViewClass = NULL;
AddView(RUNTIME_CLASS(CView2), CString(RUNTIME_CLASS(CView2)->m_lpszClassName), -1, &context);
AddView(RUNTIME_CLASS(CView3), CString(RUNTIME_CLASS(CView3)->m_lpszClassName), -1, &context);
return 0;
}
這里注意到,我新建了一個CCreateContext
並在AddView
的第四個參數中使用,這是通過分析AddView
源碼得來的,以下是部分AddView
源碼:
CView* pView = DYNAMIC_DOWNCAST(CView, pViewClass->CreateObject());
ASSERT_VALID(pView);
if (!pView->Create(NULL, _T(""), WS_CHILD | WS_VISIBLE, CRect(0, 0, 0, 0), &m_wndTabs, (UINT) -1, pContext))
{
TRACE1("CTabView:Failed to create view '%s'\n", pViewClass->m_lpszClassName);
return -1;
}
CDocument* pDoc = GetDocument();
if (pDoc != NULL)
{
ASSERT_VALID(pDoc);
BOOL bFound = FALSE;
for (POSITION pos = pDoc->GetFirstViewPosition(); !bFound && pos != NULL;)
{
if (pDoc->GetNextView(pos) == pView)
{
bFound = TRUE;
}
}
if (!bFound)
{
pDoc->AddView(pView);
}
}
可以看到,AddView
函數先使用了CreateObject
創建對象,然后用Create
函數創建了視圖,最后去CDocument
里面尋找類是否綁定了文檔,如果沒有則進行綁定,這個過程的確符合構建的一般順序,然而我們在調用Create
函數的時候卻觸發了WM_CREATE
消息,導致被創建的視圖在調用Create
函數后先要響應WM_CREATE
消息,然后進行文檔綁定,但是在被創建的類CView3
中,在響應WM_CREATE
消息時需要創建分割窗口,還要創建分割窗口中的視圖,然而在這一創建過程中,CView3
的GetDocument
函數將返回NULL
,導致文檔類指針無法繼續向子窗口傳遞,所以我使用了CCreateContext
結構體,在調用Create
函數時直接將文檔指針傳入,從而使CView3
在創建子窗口時能繼續傳遞文檔指針
然后為CView3
響應WM_CREATE
消息,在OnCreate
函數中寫入如下代碼:
int CView3::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
CCreateContext context;
context.m_pCurrentDoc = GetDocument();
context.m_pCurrentFrame = NULL;
context.m_pLastView = NULL;
context.m_pNewDocTemplate = NULL;
context.m_pNewViewClass = NULL;
m_wndSplitterWnd.CreateStatic(this, 2, 1);
m_wndSplitterWnd.CreateView(0, 0, RUNTIME_CLASS(CView1), CSize(0, 0), &context);
m_wndSplitterWnd.CreateView(1, 0, RUNTIME_CLASS(CView2), CSize(0, 0), &context);
return 0;
}
保險起見,仍然使用CCreateContext
傳遞文檔指針,這里再次使用了CView1
和CView2
,其實應該使用另外視圖的,為了減少大量的重復代碼,重復使用了這兩個視圖
然后為CView3
響應WM_SIZE
消息,在OnSize
函數中寫入如下代碼:
void CView3::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
CRect rect;
GetClientRect(&rect);
if (m_wndSplitterWnd.GetSafeHwnd() != NULL)
{
m_wndSplitterWnd.MoveWindow(&rect);
m_wndSplitterWnd.SetRowInfo(0, cy / 2, 0);
m_wndSplitterWnd.RecalcLayout();
}
}
這樣實現了兩個子視圖平分分割窗口的功能
最后編譯運行,點擊左邊目錄樹上的Node2
節點,在點擊選項卡上的CView3
選項,可以看到如下界面:
總結
本文給出了分割窗口(CSplitterWnd)與選項卡視圖(CTabView)相互建立的方法,同時給出了兩種視圖切換的方式,這樣一來,我們可以不停地建立選項卡,分割視圖,再建立選項卡,循環往復(只要你願意這么做)