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

  • 相关阅读:
    摆动排序 II
    二进制中1的个数
    n的幂
    丢掉的数字
    phpcms直接取子栏目的内容、调用点击量的方法
    phpcms导航栏当前栏目选中方法
    PHP中的全局变量global和$GLOBALS的区别
    phpcms添加子栏目后的读取
    phpcms取内容发布管理中的来源
    cms替换主页
  • 原文地址:https://www.cnblogs.com/fenggwsx/p/14358104.html
Copyright © 2020-2023  润新知