MFC分割窗口(CSplitterWnd)與選項卡視圖(CTabView)的混合使用


本文提供了在主框架和選項卡視圖中建立分割窗口,在分割窗口中建立選項卡視圖並實現視圖切換,這樣分割窗口和選項卡視圖就能循環嵌套使用了,本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_CREATETVN_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,代碼如下,,包含相應頭文件(CIndexTreeViewCView1):

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();
}

然后在CIndexTreeViewOnTvnSelchanged函數中寫入代碼:

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節點,可以看到如下界面:

改進視圖切換的方式

可以看到,在CMainFrameSwitch函數中,我們是通過刪除分割窗格中原有的視圖然后重新建立(推倒重建)的方法來實現視圖的切換,但是當視圖中要顯示大量數據時,使用這種方法可能會導致卡頓的問題,所以我們可以使用另一種策略,通過顯示和隱藏達到視圖切換的目的,當然原來這種推倒重建的方法在數據量少的情況下是沒有問題的

首先我們會發現,CSplitterWnd中沒有綁定視圖的操作,我們只能通過調用它的CreateView來創建視圖,然而在調用時,我們只能通過RUNTIME_CLASS(class_name)告訴它要創建的視圖類型,它會去新建一個視圖,對於我們已有的視圖,是無法直接綁定上去的

其次,CSplitterWnd的每一個窗格中只支持一個視圖,如果將兩個視圖建在同一個窗格中程序就會報錯

於是我通過分析CSplitterWndGetPane函數的源碼明白了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,進而引發程序報錯

再來看看CSplitterWndIdFromRowCol的源碼:

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是純虛函數)

接着修改CMyTabViewOnCreate函數:

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消息時需要創建分割窗口,還要創建分割窗口中的視圖,然而在這一創建過程中,CView3GetDocument函數將返回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傳遞文檔指針,這里再次使用了CView1CView2,其實應該使用另外視圖的,為了減少大量的重復代碼,重復使用了這兩個視圖

然后為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)相互建立的方法,同時給出了兩種視圖切換的方式,這樣一來,我們可以不停地建立選項卡,分割視圖,再建立選項卡,循環往復(只要你願意這么做)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM