• 动态切换采用 CSplitterWnd 静态划分的视图布局(MFC)


    标题读起来有些拗口,具体是什么情况,我们来看:

    一、问题的提出

     一个采用MFC开发的软件,其窗体视图采用CSplitterWnd三分,效果如下图所示:


    图1 软件的默认视图布局

    该MFC开发的软件功能多,界面非常复杂,上图只是一个demo示意图,但并不妨碍我们理解本文描述的问题。

    即视图分为left、top、bottom三部分。该软件的各个视图实际界面比较复杂,bottom视图有时候要显示重要的预览图,但因top视图挤用空间,导致查看预览图的时候,要频繁拖动bottom视图中的滚动条。但是又不能直接将top视图大小拉到最小,因为查看过程中,还要与top视图互动。

    希望当bottom视图要显示内容时,可以采取一种方法(比如点击一个按钮切换布局)将top视图移动到left视图的下边,让bottom视图独立占满右边显示。

    即要求下图所示效果:


    图2 动态切换布局之后

    需求是很简的:加一个按钮,点击该按钮,能够使得软件界面视图的布局,在上述两个状态来回切换。

    初看该问题很好解决,它仅涉及整体层面的布局,不涉及软件的其它逻辑(创建工具栏、各视图的实现、文档、业务逻辑等); 而该软件的总体布局是非常简单的,我们用vs 2008+(我用的vs 2013)创建一个纯MFC单文档应用程序,通过一些步骤很容易模拟出软件的原始布局。

    二、场景重现

    重现改布局的步骤简述:

    1)VS里新建纯MFC SDI窗体应用程序;

    2)添加三个视图,为了方便后面区分,特意将三个视图基类分别设为 CFormView、CListView、CEditView; 本示例为了更进一步区分,响应了CListView的WM_PAINT事件,将背景颜色填充成灰色。

    3)创建三分视图。为此,在CMainFrame中添加两个CSplitterWnd对象,如下:

    1     CSplitterWnd m_wndSliter;
    2     CSplitterWnd m_wndSliterR;

     之后,在CMainFrame中重写OnCreateClient方法。其实现如下:

     1 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
     2 {
     3     // TODO: 在此添加专用代码和/或调用基类
     4 
     5     CRect rect;
     6     GetClientRect(&rect);
     7 
     8     m_wndSliter.CreateStatic(this, 1, 2);
     9     m_wndSliter.CreateView(0,0, RUNTIME_CLASS(CViewFrom), CSize(180, rect.Height()), pContext);
    10 
    11     m_wndSliterR.CreateStatic(&m_wndSliter, 2, 1, WS_CHILD|WS_VISIBLE, m_wndSliter.IdFromRowCol(0, 1));
    12     m_wndSliterR.CreateView(0,0, RUNTIME_CLASS(CViewEdit), CSize(rect.Width()-180, 100), pContext);
    13     m_wndSliterR.CreateView(1,0, RUNTIME_CLASS(CViewList), CSize(rect.Width()-180, rect.Height()-100), pContext);
    14 
    15     
    16     return TRUE;
    17     //return CFrameWnd::OnCreateClient(lpcs, pContext);
    18 }
    View Code

    其中,CViewFrom、CViewEdit和CViewList就是第2)步创建的三个视图。需要在MainFrame.h中包含这三个视图的头文件。 上述代码调试运行后,即可得类似图1的三分布局效果。

    分析一下上述三分视图的实现:

    首先,采用一个CSplitterWnd 将视图划分为左右两部分(1行两列),左边放置 viewleft,右边放置另一个 CSplitterWnd,该CSplitterWnd 又将右边划分为上下两部分(2行1列), 然后,上面部分放置viewtop,下面放置viewbottom。 该划分是在OnCreateClient的时候创建的,并且采用的是CreateStatic静态划分的形式。

    那么,这种情况下,能否动态的将viewtop切换到viewleft的下面呢? 软件运行中,各视图上有用户操作的各种状态,切换过程中,是否可以保持各视图的当前状态(即不要重新创建视图对象)呢?

    答案是肯定的。

    三、分析问题

    再来看一下图2所示的效果,其实就是采用一个 CSplitterWnd 将视图划分为左右两部分(1行2列),第0列又采用一个CSplitterWnd进一步划分为两行。然后将各个视图放到对应的位置。 我们不难知道 CSplitterWnd 类提供了GetPane方法可以获取指定行列处的视图,但是没有一个SetPane的方法,动态设置视图。 如何动态的给每个位置设置视图对象,疑惑就在这里了。

    Bing查到CodeProject上的一篇文章: http://www.codeproject.com/Articles/178/Switching-to-other-views-in-a-doc-view-application

    该文章中描述了采用CSplitterWnd时,如何动态的替换掉其中的一个视图。看到该文章,心里有些低了,原来CSplitterWnd各行列位置的视图,是可以动态替换掉的。

    刚才我们知道,有GetPane方法可以获取到各行列位置的视图对象,那么,问题不就变成了取出现有的视图对象放置到CSplitterWnd的指定行列位置吗。

    有了该思路,立刻动手,发现事情没有这么简单,中间过程就不说了。

    四、解决问题

    结果过程也没有多复杂。

    图2所示的情况,不就是左边变成两行划分吗,那么我们先增加一个CSplitterWnd,将左边划分为两行。

    即,初始状态下,应用程序视图是被划分为左上、左下、右上、右下四部分的。

    代码如下(红色部分为新增):

    1)在MainFrame.h中:

    1     CSplitterWnd m_wndSliter;
    2     CSplitterWnd m_wndSliterL; // 新增加一个拆分器
    3     CSplitterWnd m_wndSliterR;

    2)OnCreateClient实现:

     1 BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)
     2 {
     3     // TODO: 在此添加专用代码和/或调用基类
     4 
     5     CRect rect;
     6     GetClientRect(&rect);
     7 
     8     m_wndSliter.CreateStatic(this, 1, 2);
     9     m_wndSliter.CreateView(0,0, RUNTIME_CLASS(CViewFrom), CSize(180, rect.Height()), pContext);
    10 
    11     m_wndSliterR.CreateStatic(&m_wndSliter, 2, 1, WS_CHILD|WS_VISIBLE, m_wndSliter.IdFromRowCol(0, 1));
    12     m_wndSliterR.CreateView(0,0, RUNTIME_CLASS(CViewEdit), CSize(rect.Width()-180, 100), pContext);
    13     m_wndSliterR.CreateView(1,0, RUNTIME_CLASS(CViewList), CSize(rect.Width()-180, rect.Height()-100), pContext);
    14 
    15     m_wndSliterL.CreateStatic(&m_wndSliter, 2, 1, WS_CHILD|WS_VISIBLE, m_wndSliter.IdFromRowCol(0, 0));
    16     m_wndSliterL.ShowWindow(SW_HIDE);
    17 
    18     return TRUE;
    19     //return CFrameWnd::OnCreateClient(lpcs, pContext);
    20 }

    初始化新增加的拆分器,但是不给它设置视图,并且将其隐藏(本处分开两行写,实际可简化代码一步到位)。 隐藏是很重要的,拆分器划分布局后,如果不会为其创建视图,会导致应用程序运行失败。

    即在软件首次创建视图布局的时候,就采用 m_wndSliterL 将左边一列划分为两行,但是又不给它设置视图。而是将左边的视图直接设置在了 m_wndSliter 这个顶层划分中, 并且,非常重要的,要将 m_wndSliterL 隐藏

    上述两步完成后,编译运行,效果和没有修改前一样(如图1)。

    下面,我们来添加动态来回切换视图的代码:

    1)在CMainFrame.h中添加一个标记,以及一个方法申明:

    1 //...   
    2 bool _isCommomViewStyle;
    3 
    4 //...
    5 void SwitchViewStyle();


    2)在CMainFrame的构造函数函数中将_isCommomViewStyle标记初始化为true,即默认状态。

    3)添加SwitchViewStyle方法的实现,如下所示:

     1 // 切换视图
     2 void CMainFrame::SwitchViewStyle()
     3 {
     4     if (_isCommomViewStyle)
     5     {
     6         CWnd* viewLeft = m_wndSliter.GetPane(0,0);
     7         CWnd* viewTop = m_wndSliterR.GetPane(0,0);
     8         CWnd* viewBot = m_wndSliterR.GetPane(1,0);
     9 
    10         viewLeft->SetParent(&m_wndSliterL);
    11         viewTop->SetParent(&m_wndSliterL);
    12         viewBot->SetParent(&m_wndSliter);
    13 
    14         ::SetWindowLong(viewLeft->m_hWnd, GWL_ID, m_wndSliterL.IdFromRowCol(0, 0));
    15         ::SetWindowLong(viewTop->m_hWnd, GWL_ID, m_wndSliterL.IdFromRowCol(1, 0));
    16         ::SetWindowLong(viewBot->m_hWnd, GWL_ID, m_wndSliter.IdFromRowCol(0, 1));
    17         
    18         m_wndSliterL.SetRowInfo(1, 200, 0);
    19         m_wndSliterL.ShowWindow(SW_SHOW);
    20         m_wndSliterR.ShowWindow(SW_HIDE);
    21 
    22         m_wndSliterL.RecalcLayout();
    23         m_wndSliter.RecalcLayout();
    24         
    25         _isCommomViewStyle=false;
    26     }
    27     else
    28     {
    29         CWnd* viewLeft = m_wndSliterL.GetPane(0,0);
    30         CWnd* viewTop = m_wndSliterL.GetPane(1,0);
    31         CWnd* viewBot = m_wndSliter.GetPane(0,1);
    32 
    33         viewLeft->SetParent(&m_wndSliter);
    34         viewTop->SetParent(&m_wndSliterR);
    35         viewBot->SetParent(&m_wndSliterR);
    36 
    37         ::SetWindowLong(viewLeft->m_hWnd, GWL_ID, m_wndSliter.IdFromRowCol(0, 0));
    38         ::SetWindowLong(viewTop->m_hWnd, GWL_ID, m_wndSliterR.IdFromRowCol(0, 0));
    39         ::SetWindowLong(viewBot->m_hWnd, GWL_ID, m_wndSliterR.IdFromRowCol(1, 0));
    40 
    41         m_wndSliterL.ShowWindow(SW_HIDE);
    42         m_wndSliterR.ShowWindow(SW_SHOW);
    43         m_wndSliterR.RecalcLayout();
    44         m_wndSliter.RecalcLayout();
    45 
    46         _isCommomViewStyle=true;
    47     }
    48 }
    View Code

    即首先判断当前视图的状态,如果是普通状态(图1),则切换到图2所示状态;否则切换到图1所示状态。切换后同时更新状态标志。

    在切换状态的时候,主要步骤为:

    1)获取各位置的视图对象,采用CSplitterWnd::GetPane(int row, int col);

    2) 重新设置各视图的父容器,CWnd::SetParent(CWnd* pNewWnd);

    3) 修改视图窗体信息,::SetWindowLong,具体见代码

    4)切换SplitterWnd的显示

    5)更新SplitterWnd的布局

    其中,非常重要的一点是,SplitterWnd对象划分完成后,必须为每一部分设置视图对象(初次创建时采用CreateView方法),否则程序将不可运行。但是,如果该SpliiterWnd不可见则例外

    最后,我们可以在工具栏添加一个按钮,在该按钮的点击事件响应函数中,调用SwitchViewStyle即可完成布局的动态切换。

    下载示例代码

    注:本示例中m_wndSliter*变量的申明命名并不规范,请注意。

    全文完

    enjoy..


    本文最早由 杨志军(ic#,yang.email#qq.com) 发表于 个人备忘录软件,以及博客园。转发请保留本声明。

    2014/4/22

  • 相关阅读:
    大数相加和快速相乘
    (转)直线分割平面 与 平面分割区域 问题
    8.3水了一天
    8.2数论(1)
    7.31 基本算法1.2
    7.31 基本算法1.1
    《GSoC 2019小结》&《买车小记》By Ray Guo
    【笔试题】中国电信天翼智慧家庭2020春季校园招聘
    TinyMCE主题的文章目录没法点击页内跳转
    【毕设】答辩如何准备?本科毕业论文如何撰写文献综述?
  • 原文地址:https://www.cnblogs.com/yangzhj/p/3681206.html
Copyright © 2020-2023  润新知