23.11 Splitter views

A splitter window holds two or more views of a document inside of it. There are two kinds of splitter windows, the dynamic splitter window and the static splitter window.

  • A dynamic splitter window is a window such as you have in Microsoft Word or in Visual Studio that the user can choose to split or not to split. Usually a dynamic splitter window has the property that if you resize it, the pane on the right-hand side will change size, but the pane on the left stays the same width.

  • A static splitter window is a window that is always split. With a static splitter window the programmer is better able to control the ratio of the size of the two panes. If you don't want to see the split, in a static splitter window, you can drag the split-divider over to one side, but if you go and click there, you'll find the divider bar is still waiting there.

The author designed the Pop Framework so you can build both. In the Adding a Static Splitter section below we show how to change from one build to the other. Our default build uses dynamic splitter, which you can activate with Window | Split.

In understanding the role of splitter windows we need to understand their place in the window tree. Ordinarily a CView is the immediate window child of its frame. In the case of MDI, which is what we're focusing on, the frame is a ChildFrame object (which is an instance of the CMDIChildWnd class). In the case of SDI, the frame would be a MainFrame object which is an instance of the CFrameWnd class. But in any case, the usual situation is that your view is right inside the frame, in terms of the windows tree.

When we add a splitter window, it gets in between the frame and view. Here's a picture showing the two kinds of window trees.


Let's describe how to add either kind of splitter. 'Dynamic' sounds more exciting and complicated than 'static,' but in some ways the static splitter windows are more powerful, and they're slightly harder to program. Let's explain the easier case first.

Adding a dynamic splitter

  1. Add a CSplitter _cSplitterWnd data field to ChildFrame in ChildFrm.h.

  2. Change the CChildFrame::OnCreateClient code in ChildFrm.cpp to have a single line as in the following.

BOOL CChildFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* 
        /* When adding a splitter, be sure to comment out the base 
            class code. */ 
        // CMDIChildWnd::OnCreateClient(lpcs, pContext); 
        /* We pass the CSplitterWnd::Create call the "this" pointer, 
            the max number of rows (1 or 2), the max number of 
            columns (1 or 2), and an (x,y) size at which you want a 
            pane to just disappear, the pContext variable, and a flags 
            variable. */ 
    return _cSplitterWnd.Create(this, 1, 2, CSize(20, 20), pContext, 
        WS_CHILD | WS_VISIBLE | SPLS_DYNAMIC_SPLIT); /* The default 
            includes | WS_HSCROLL | WS_VSCROLL in the sixth 
            argument, but I don't want scrollbars here. */ 

And that's it! If you rebuild now, you've got a working dynamic splitter window! If you use the Window | Split selection, you can split any of your views in two.

But there is one more consideration. It may be that you want the two panes of the split view to automatically be different. To do this, you could put some code like the following into your game's override of initializeView(CPopView* pview), so as to make the views in the two panes be different. When the window is not split, your default behavior will be that of the 'left pane.'

CSplitterWnd* csplitter = (CSplitterWnd*)(pview->GetParent()); 
if (pview == csplitter->GetPane(0,0) ) //we're the left, main view. 
    //Change the view settings the values you want in the left pane 
else //we're the right, subsidiary view. 
    /* Change the view settings to the values you want in the right 
        pane. */ 

You might also want to override the cGame child's initializeCritterViewer(cCritterViewer *pviewer) with a similar kind of switch on pviewer->_pownerview.

Adding a static splitter

If you use a static splitter window, you have the ability to make your window keep a fixed layout no matter what size it is. The idea is to allocate some fixed proportion, like 0.75 of the window for the left pane and give the rest to the right pane. Of course the user can still move the splitter bar back and forth, but whenever we resize the frame window we'll go back to our standard proportions.

Look for the following block of code at the top of the Childfrm.h file. Comment STATIC_SPLITTER in, set the Build | Set Active Configuration . . . to select the Release option and rebuild.

/* Comment STATIC_SPLITTER out to have a dynamic (user selectable) 
    splitter window rather than a static (automatically present) 
    splitter window. If we have STATIC_SPLITTER, then we use 
    LEFT_PANE_PERCENT to specify how much of the width of the child 
    frame window is devoted to the left pane. The PERCENT macro is 
    defined in stdafx.h to convert the 0 to 100 range to 0.0 to 1.0. */ 

Run the new *.exe and notice that when you resize the window, the left splitter pane always takes up 50 percent of the screen. Change the LEFT_PANE_PERCENT number to 75 and build again.

What makes this a little tricky is that a static splitter window is not fully initialized until you've both called CSplitterWnd::CreateStatic and repeatedly called CSplitterWnd::CreateView to put valid CView objects inside its panes. What with all the things the MDI framework does to initialize itself, you will hit the ChildFrame::OnSize method three or four times before you've managed to finish executing the ChildFrame::OnCreateClient code that initializes the _cSplitterWnd. So you need a BOOL _splittercreated field to keep you from trying to set the sizes of the _cSplitterWnd 's panes before they're fully ready.

If you look in the Pop ChildFrm code, you'll find the following two things.

  • First, there is a CSplitter _cSplitterWnd data field of ChildFrame in ChildFrm.h, and a BOOL _splittercreated field as well. The CChildFrame constructor initializes _splittercreated to FALSE.

  • Second, the CChildFrame::OnCreateClient method in ChildFrm.cpp is written to make a call to _cSplitterWnd.CreateStatic, three calls to _cSplitterWnd.CreateView, and a call to _cSplitterWnd.RecalcLayout. See the code in childfrm.cpp and its comments for details.

As in the case of the dynamic splitter, you may want to change your CPopView::OnCreate to initialize the views differently according to which pane of the splitter window they appear in.

Finally, if you want to change the behavior of the relative sizes of the panes when you resize the window, you can tweak the CChildFrame::OnSize method a bit further.

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference