Removing and reapplying splitter windows on-the-fly

Although the MFC splitter window class CSplitterWnd is a neat feature of MFC and splitter windows in general are a great user interface gadget to show different aspects of a document, I really do not use them very often in the applications that I use in my everyday work. Instead I often end up using applications that show scrollbars just because they support dynamic splitter windows, although I never use those splitter windows at all. This is most irritating to me, either to see disabled scrollbars without a thumb to track or a scrollbar thumb without any use, like the vertical scrollbar thumb in an empty Word document. Word and other applications, like Visual Studio, always create enabled scrollbars with a thumb to track, even when the document or text file is empty and therefore there would be no need for scrollbars at all. As can be seen easily, very often there is never the need for one or both of the scrollbars at all, because the entire document fits into the client area of the frame window that shows the document. In such a case, it is just the splitter window support, that forces those frame windows to have scrollbars. I would appreciate, if applications let the user decide whether splitter windows are to be shown, in order to allow the user to get rid of those unnecessary scrollbars. I therefore wrote a CSplitterWnd derived class, named CCustSplitterWnd, that makes it possible to let the user decide if she wants to see splitter windows and the associated scrollbars or not. It is up to you to use this class "as is", as a base class for your own splitter window class or any other way you would like to use and modify it to suit your needs. At least, it could serve as a starting point for you to modify the standard splitter window behaviour that the CSplitterWnd class provides, to your particular application's needs. This article is made up of three parts: I will first explain the design goals for CCustSplitterWnd, then I will demonstrate how the Scribble sample has to be modified to benefit from this class, and finally, I will point out some implementation details of CCustSplitterWnd.


Design goals of the CCustSplitterWnd class

I decided that there have to be three major design goals for this class:

1. The user should be able to switch between the split mode and the unsplit mode of a CFrameWnd derived class on-the-fly. Unsplit mode means: the CFrameWnd derived class using this splitter window class should behave like there wasn't a splitter window at all.

2. The CFrameWnd derived class should be created initially with or without the splitter window support visible.

3. The class should be portable across the latest Win32 and Win16 MFC versions.


How the CCustSplitterWnd class is applied to the Scribble sample

To demonstrate how that CCustSplitterWnd class works, I decided to show how it is applied to the famous scribble sample. This part of the article is all about how scribble gets the new menu item "Split initially", that determines if a newly created scribble document will appear with splitter windows, and the new menu item "Split", that splits or unsplits a scribble document frame on-the-fly.
The sources for this article work with the latest 16 and 32 bit MFC versions (please beware that the CMDIChildWnd derived class of the Win16 MFC 2.5 scribble sample is named CScribFrame and resides in files scribfrm.cpp and scribfrm.h). In fact, it works with MFC versions 2.5x, 4.x, 5.0 and 6.0 (use scribvc4.mak for VC 4.x, scribble.dsp for VC5 and VC6). To see how it works, just recompile the sources I supply for this article or follow these instructions:

Copy the files of step 8 (step 7 if you are working with MFC 2.5 for Win16) of the scribble project and the two files custsplt.cpp and custsplit.h into a separate directory, add custsplt.cpp to the project (and do a rebuild all of Scribble if you like). Now replace the type of the Splitter Window class in file childfrm.h from the standard CSplitterWnd to the CSplitterWnd derived class CCustSplitterWnd. The next thing to do is to add an
#include "custsplt.h" 
statement into both scribble.cpp and childfrm.cpp right before the
#include "childfrm.h"
include statement . You might as well insert this include directive only once directly into childfrm.h, but I think it is generally good practice not to have include statements in header files, because this tends to unnecessary include statements that could be replaced by simple forward declarations that do not add to compilaton time. Just do it the way you prefer it.
Because we want to be able to remove and reapply the splitter window on the fly, we now have to add the menu item "Split" with Identifier ID_SPLIT to the "Window" submenu of menu IDR_SCRIBBTYPE. Also, add a command handler and an update command UI handler for ID_SPLIT to the CChildFrame class (CChildFrame::OnSplit and CChildFrame::OnUpdateSplit).
To determine if a new document created by scribble should have splitter windows initially, a new static member variable m_bInitSplitter of CChildFrame is used. Add this member variable to the protected section of the CChildFrame class declaration along with two public Get/Set methods (GetInitSplitter and SetInitSplitter). CChildFrame's class declaration in childfrm.h should now look something like this:

class CChildFrame : public CMDIChildWnd
{
	DECLARE_DYNCREATE(CChildFrame)
public:
	CChildFrame();

// Attributes
protected:
	CCustSplitterWnd    m_wndSplitter;
	static BOOL m_bInitSplitter;
public:
	static BOOL GetInitSplitter(void){return m_bInitSplitter;};
	static void SetInitSplitter(const BOOL &bValue){m_bInitSplitter = bValue;};
// Operations
public:
// Overrides
	// ClassWizard generated virtual function overrides
	//{{AFX_VIRTUAL(CChildFrame)
	public:
	virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
	protected:
	virtual BOOL OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext);
	//}}AFX_VIRTUAL

// Implementation
public:
	virtual ~CChildFrame();
#ifdef _DEBUG
	virtual void AssertValid() const;
	virtual void Dump(CDumpContext& dc) const;
#endif

// Generated message map functions
protected:
	//{{AFX_MSG(CChildFrame)
	afx_msg void OnSplit();
	afx_msg void OnUpdateSplit(CCmdUI* pCmdUI);
	//}}AFX_MSG
	DECLARE_MESSAGE_MAP()
};

Now we have to fill in some code into the ID_SPLIT handlers like this:
void CChildFrame::OnSplit() 
{
	m_wndSplitter.OnSplit(); 
}

void CChildFrame::OnUpdateSplit(CCmdUI* pCmdUI) 
{
	m_wndSplitter.OnUpdateSplit(pCmdUI); 	
}
Before Scribble can be recompiled to test the behaviour of the ID_SPLIT menu item, the m_bInitSplitter member variable of CChildFrame must be implemented and initialized in childfrm.cpp like this, right after the include statements (otherwise we would get a linker error, because it is a static member variable):
BOOL CChildFrame::m_bInitSplitter=TRUE;
No recompile scribble and try the ID_SPLIT menu item. If a scribble document frame was previously split, it will lose its splitter window panes and the associated scrollbars (the splitter window just won't be visible anymore), whereas a document frame without splitter windows will show its splitter window and the associated scrollbars again (and possibly splitter window panes, depending on the split location after tracking).

The next thing we are going to implement is a menu item that is used to set the value of the static member variable CChildFrame::m_bInitSplitter in the "View" submenu of the IDR_MAINFRAME and IDR_SCRIBBTYPE menus of Scribble. Name it "Split initially" and give it the Identifier ID_INITSPLIT. Add a command handler and an update command UI handler for ID_INITSPLIT to the CMainFrame class (CMainFrame::OnInitsplit and CMainFrame::OnUpdateInitsplit) and implement it like this:
void CMainFrame::OnInitsplit() 
{
	CChildFrame::SetInitSplitter(!CChildFrame::GetInitSplitter());
}

void CMainFrame::OnUpdateInitsplit(CCmdUI* pCmdUI) 
{
	pCmdUI->SetCheck(CChildFrame::GetInitSplitter());
}
Because the CMainFrame class does not yet know anything about the CChildFrame class, these two include directives have to be added to mainfrm.cpp:
#include "custsplt.h"
#include "childfrm.h"
The last thing to do before everything works as expected, is to add this line to CChildFrame's constructor:
	m_wndSplitter.SplitWindow() = m_bInitSplitter;
Now recompile Scribble and notice how the ID_INITSPLIT menu item works: If the check mark next to this item is set, newly created scribble documents will appear with splitter windows, otherwise without them.



Some implementation details of the CCustSplitterWnd class

A CCustSplitterWnd instance holds the information about the visibility of splitter scrollbars and the splitter window itself in a private member variable m_bSplit of type BOOL. This member variable can be modified using the member function BOOL &SplitWindow(void) that should be called after the constructor and before the Create member function (it is perfectly legal, but not recommended, to set m_bSplit at any time, even after the call to Create, but this may cause the CCustSplitterWnd instance to be somewhat puzzled. The perfect solution would be, to make m_bSplit read-only by not returning a reference to it with BOOL &SplitWindow(void) and to make the initial value of m_bSplit set by an additional parameter to the constructor or the Create function, but I did not want to change the interfaces inherited from the standard CSplitterWnd class. Change that if you like!). The Create member function simply removes the WS_HSCROLL and WS_VSCROLL style bits, if the splitter window is to be created without scrollbars initially visible. This was the easy part of proper initialization of the CCustSplitterWnd class. As you can imagine, it was much harder to implement the code for the change of the splitter behaviour "on-the-fly". For this to work, two different situations must be handled: The window is split ( that means, it already shows scrollbars for the splitter window and probably one or more panes) and the window is unsplit (that means, it looks like it didn't have splitting support at all):

What happens if a previously unsplit splitter window is to be split?

The OnSplit member function is the function that toggles the behaviour of the splitter window class. If invoked for a previously unsplit splitter window, the m_bSplit member variable has a value of FALSE and basically invokes the CSplitterWnd member function DoKeyboardSplit and finally sets m_bSplit to TRUE. Before DoKeyboardSplit is invoked, the scrolling settings for the current active view are obtained and stored in the member variables m_nMapMode, m_sizeTotal, m_sizePage, m_sizeLine via a call to CScrollView::GetDeviceScrollSizes before the scrolling of this view is temporarily disabled by a call to CScrollView::SetScrollSizes with the second parameter set to CSize(0,0). This is necessary to solve two different problems, depending on the version of MFC that is used. If we would omit the disabling of scrolling we would see a weird user interface (scrollbars inside the top left splitter pane) on Win32 after tracking, like this:



whereas on Win16 we would see the scrollbars additionally during tracking, like this:



When DoKeyboardSplit is called, the splitter window is in the "tracking state": You can move the splitters with the mouse or the cursor keys and if you press Enter or Escape or the left mouse button, this state is left and the window is split. Consequently, the WM_LBUTTONDOWN-handler OnLButtonDown and the WM_KEYDOWN-handler OnKeyDown of CCustSplitterWnd restore the active view's scrolling settings from the member variables m_nMapMode, m_sizeTotal, m_sizePage, m_sizeLine. OnKeyDown additionally calls RecalcLayout to set the newly shown scrollbars correctly. Without the call to RecalcLayout in this situation, the newly created scrollbars would not be visible until the window is resized. A call to RecalcLayout can also be located in the WM_LBUTTONUP-handler OnLButtonUp, because this also happens when splitting is done with the mouse. In OnLButtonUp, the RecalcLayout is done after the call to the base classes version of the handler function, because the base class version must first get the opportunity to remove the splitters by painting over them.

What happens if a previously split splitter window is to be unsplit?

In this case OnSplit is invoked with the m_bSplit member variable having a value of TRUE. Basically, in this case all panes except for the top-left pane are destroyed by calls to DeleteRow(1) and DeleteColumn(1) and also the now unnecessary scrollbars of the CCustSplitterWnd object. If the current active view is a CScrollView, it probably has to reflect some changes in its scrollbars which is, why ResizeParentToFit is called for it, before m_bSplit is set to FALSE. Here some subtle differences between the Win16 and Win32 implementations of CSplitterWnd and CCustSplitterWnd become apparent, as well as with the SetScrollStyle member function, that is not available for the Win16 versions of MFC and which I therefore had to rewrite for CCustSplitterWnd. But in general, the splitter window implementation is pretty much the same for the Win16 and the Win32 versions of MFC.

Download demo project - 161 KB (unzip with directory preservation)

Download source - 4 KB


If you find bugs or if you have any comments or questions regarding this article and the accompanied source code, don't hesitate to let me know.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: November 6, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Are you wanting to target two or more platforms such as iOS, Android, and/or Windows? You are not alone. 90% of enterprises today are targeting two or more platforms. Attend this eSeminar to discover how mobile app developers can rely on one IDE to create applications across platforms and approaches (web, native, and/or hybrid), saving time, money, and effort and introducing apps to market faster. You'll learn the trade-offs for gaining long …

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds