加载中

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)相互建立的方法,同时给出了两种视图切换的方式,这样一来,我们可以不停地建立选项卡,分割视图,再建立选项卡,循环往复(只要你愿意这么做)

posted @ 2021-02-01 18:14  fenggwsx  阅读(1932)  评论(0编辑  收藏  举报