Back to  main page


Chapter 3 Splitter Window

A splitter window resides within the frame window. It is divided into several panes, each pane can have a different size. Splitter window provides the user with several different views for monitoring data contained in the document at the same time. Normally, the size of each pane can be adjusted freely, this gives the user a better view of data. There are two types of splitter windows: Dynamic Splitter Window and Static Splitter Window. For a dynamic splitter window, all views within the splitter window are of the same type. The user can create new panes or remove old panes on the fly. For a static splitter window, the views could be of different types and the number of panes has to be fixed at the beginning. In this case, the user can not add or delete views after the program has started.

Both SDI and MDI applications can have splitter windows. In an SDI application, the splitter window is embedded in the mainframe window. In an MDI application, it is embedded in the child frame window.

3.1 Implementing Static Splitter Windows

In MFC, class CSplitterWnd is used to implement window splitting. To split a window into several panes, we must first declare a CSplitterWnd type variable in the frame window class. One CSplitterWnd can divide window into M(N sub-panes. The splitter window can be nested, which means we can further split a single pane into several sub panes by using another CSplitterWnd type variable. So if we want to create an unevenly divided splitter window, we need to declare more than one CSplitterWnd type variables.

For example, if we want to create a splitter window that has two rows, the first row has two columns and the second row has two columns, we need to first split the client area into a 1(2 splitter window, then split the first row into a 2(1 splitter window (Figure 3-1).

(Figure omitted)

To create static splitter window, first we need to declare CSplitterWnd type variable(s) in the frame window class, then in frame window's OnCreateClient(...) member function, call functions CSplitterWnd::CreateStatic(...) and CSplitterWnd::CreateView(...). Here, function CSplitterWnd:: CreateStatic(...) is used to split the window into several panes and CSplitterWnd::CreateView(...) is used to attach a view to each pane.

Sample application 3.1\Sdi\Spw demonstrates how to implement splitter window. It is generated by Application Wizard, with all settings set to default ones. Four main classes used to implement the application are CSpwApp, CMainFrame, CSpwDoc and CSpwView.

Each pane of the splitter window must be attached with a view in order to make it work. The view could be implemented by deriving class from any of the standard view classes: CView, CScrollView, CRichEditView, CListView, CTreeView etc. In the sample, besides the default view CSpwView created by the Application Wizard, two other classes are derived from CFormView and CEditView, which will be used to implement different panes of the splitter window.

Class CFormView is a standard MFC class that can be used to create view window from dialog template. A CFormView class must have a corresponding dialog template resource, which will be used to create the view. To implement a form view, we must first design dialog template, then derive a new class from CFormView.

In the sample, the dialog template used to implement the form view is IDD_DIALOG_VIEW. Its styles are set to "Child" and "No border", this is exactly the same with that of dialog bar (This is because both splitter window and dialog bar must be child windows). The dialog template contains a static text control and a multiple-line edit box. By double clicking on the dialog template resource, we will be prompted to add a new class for it. In this case the template resource ID will be automatically selected to be used by the new class. In the sample application, the new class is named CSpwFView. If the dialog template is not open when adding this new class, we must select the dialog ID by ourselves (Figure 3-2).

(Figure omitted)

The other pane of the splitter window is implemented using edit view. The new class for this window is derived from CEditView, and its name is CSpwEView.

To split a window, we need to call function CSplitterWnd::CreateStatic(...), which has five parameters:

(Cde omitted)

The first parameter pParentWnd is a CWnd type pointer that points to the parent window. Because a splitter window is always the child of frame window, this parameter can not be set to NULL. The second and third parameters specify the number of rows and columns the splitter window will have. The fourth parameter dwStyle specifies the styles of splitter window, whose default value is WS_CHILD | WS_VISIBLE. The fifth parameter, nID, identifies which splitter window is being created. This is necessary because within one frame window, we can create several nested splitter windows. For the root splitter window (The splitter window whose parent window is the frame window), this ID must be AFX_IDW_PANE_FIRST. For other nested splitter windows, this ID need to be obtained from the parent splitter windows by calling function CSplitterWnd::IdFromRowCol(...), and passing appropriate column and row coordinates to it. The following is the format of this function:

int CSplitterWnd::IdFromRowCol(int row, int col);

To attach a specific view to a pane, we need to call function CSplitterWnd::CreateView(...), which also has five parameters:

(Code omitted)

The first two parameters specify which pane is being created. The third parameter specifies what kind of view will be used to create this pane. Usually macro RUNTIME_CLASS must be used to obtain a CRuntimeClass type pointer. The fifth parameter is a creation context used to create the view. Within CMainFrame::OnCreateClient(...), the creation context is passed through the second parameter of this function.

In the sample application, we first use m_wndSpMain to call function CSplitterWnd:: CreateStatic(...) to split the client window into a 2(1 splitter window. Then, we use this variable to call CSplitterWnd::CreateView(...) and pass two 0s to the first two parameters of this function (This specifies (0, 0) coordinates). This will attach a new view to the left pane of the splitter window. Next we use m_wndSpSub to call CSplitterWnd::CreateStatic(...) to further split the right pane into a 1(2 splitter window, and call CSplitterWnd::CreateView(...) twice to create views for the two panes. At last, instead of calling function CMainFrame::OnCreateClient(...), a TRUE value is returned. This can prevent the default client window from being created.

The following steps show how the static splitter window is implemented in the sample:

1) Declare two CSplitterWnd type variables in class CMainFrame:

class CMainFrame : public CFrameWnd

{

......

protected:

CStatusBar m_wndStatusBar;

CToolBar m_wndToolBar;

CSplitterWnd m_wndSpMain;

CSplitterWnd m_wndSpSub;

......

}

Variable m_wndSpMain will be used to split the mainframe client window into a 2(1 splitter window, and m_wndSpSub will be used to further split the right column into a 1(2 splitter window.

2) In function CMainFrame::OnCreateClient(...), create splitter windows and attach views to each pane:

(Code omitted)

For an MDI application, everything is almost the same except that here CChildFrame replaces class CMainFrame. We can create an MDI application, declare m_wndSpMain and m_wndSpSub variables in class CChildFrame, and add code to CChildFrame::OnCreateClient(...) to create splitter windows. The code required here is exactly the same with implementing splitter window in an SDI application. Sample 3.1\MDI\Spw demonstrates this.

3.2 Dynamic Splitter Window

Once we understand how to create static splitter window, it is easier for us to create dynamic splitter window because it takes fewer steps. For a dynamic splitter window, all panes are created to be the same type of view, so there is no need to call function CSplitterWnd::CreateView(...) for each individual pane. Also, instead of calling CSplitterWnd::CreateStatic(...), we need to call function CSplitterWnd:: Create(...) to create dynamic splitter window within function CFrameWnd::OnCreateClient(...). The following is the format of function CSplitterWnd::Create(...):

BOOL CSplitterWnd::Create

(

CWnd* pParentWnd,

int nMaxRows, int nMaxCols,

SIZE sizeMin,

CCreateContext* pContext,

DWORD dwStyle=WS_CHILD | WS_VISIBLE |WS_HSCROLL | WS_VSCROLL | SPLS_DYNAMIC_SPLIT,

UINT nID=AFX_IDW_PANE_FIRST

);

The difference between function CSplitterWnd::CreateStatic(...) and CSplitterWnd::Create(...) is that when creating dynamic splitter window, we need to specify the maximum number of rows and columns. The maximum values of nMaxRows and nMaxCols parameters are both 2, which means that a window can be split to have at most 2x2 panes.

The Application Wizard has a built-in feature to add dynamic splitter window to the applications. In step 4 of the Application Wizard, if we press "Advanced..." button, an "Advanced Options" property sheet will pop up. By clicking "Window styles" tab then checking "Use split window" check box, code will be automatically added to the application for implementing dynamic split window (static splitter window can not be created this way).

It is also simple to implement splitter window manually. Like creating static split window, first we need to declare a CSplitterWnd type variable in class CMainFrame (In MDI applications, we need to do this in class CChildFrame). Then we can use Class Wizard to override function OnCreateClient(...). Within the overridden function, we can call CSplitterWnd::Create(...) to create splitter window.

Sample 3.2\Spw demonstrates how to create dynamic splitter window in an SDI application. The application is created from Application Wizard with all default settings. Then a new variable m_wndSp is declared in class CMainFrame, which will be used to implement the splitter window. In function CMainFrame::OnCreateClinet(...), the splitter window is created as follows:

(Code omitted)

In the above code, we did not pass any value to dwStyle and nID parameters of function CSplitterWnd::Create(...), so the default values are used.

3.3 Customizing the Behavior of Split Bar

The behavior of a dynamic splitter window is different from that of a static splitter window. For the dynamic splitter window, panes could be dynamically created by double clicking on the split boxes (Figure 3-3). After new panes are added, one or more split bars will appear. If the user double clicks any of the split bar, one f the two panes divided by that split bar will be deleted (Figure 3-4). We can examine the sample applications we've created to see the difference between static splitter window and dynamic splitter window.

(Figure omitted)

This behavior could be customized. For example, sometimes by double clicking on the split bar, we want to resize the two panes instead of deleting one of them. This feature gives the user much convenience for changing the size of each pane bit by bit.

Splitter Window Layout

We need to override the following two member functions of class CSplitterWnd in order to implement this feature: CSplitterWnd::DeleteRow(...) and CSplitterWnd::DeleteColumn(...). When the user double clicks on the split bar, one of the two functions will be called to delete a row or column dynamically. In order to customize this behavior, after the split bar is clicked, we can first change the size of each pane, then judge if the size of one pane is smaller than its minimum size. If so, we call the default implementation of the corresponding function to delete one row or column.

To change a pane's size, we need to call function CSplitterWnd::SetColumnInfo(...) and CSplitterWnd::SetRowInfo(...). The current size of a pane could be obtained by their counterpart functions CSplitterWnd::GetColumnInfo(...) and CSplitterWnd::GetRowInfo(...). The following shows the formats of the above four functions:

void CSplitterWnd::SetColumnInfo(int col, int cxIdeal, int cxMin);

void CSplitterWnd::SetRowInfo(int row, int cyIdeal, int cyMin);

void CSplitterWnd::GetColumnInfo(int col, int& cxCur, int& cxMin);

void CSplitterWnd::GetRowInfo(int row, int& cyCur, int& cyMin);

In the above functions, parameters row and col are used to identify a pane with specified row and column indices, cyIdeal and cxIdeal are the ideal size of a pane, cyMin and cxMin indicate minimum size of it.

When the splitter window is being displayed, each pane's dimension is decided from its ideal size. According to the current size of the frame window, some panes may be set to their ideal sizes, but some may not (This depends on how much space is left for that pane). In any case, a pane's actual size should not be smaller than its minimum size. This is why we need both ideal size and minimum size to set a row or column's dimension.

The number of rows and columns a splitter window currently has can be obtained by calling other two member functions of CSplitterWnd:

int CSplitterWnd::GetRowCount();

int CSplitterWnd::GetColumnCount();

After we call function CSplitterWnd::SetColumnInfo(...) or CSplitterWnd::SetRowInfo(...), the old layout will not change until we call function CSplitterWnd::RecalcLayout() to update the splitter window. The system will re-calculate the layout for each pane according to their new sizes (both ideal size and minimum size), and the split bar will be moved to a new position according to the new layout.

Overriding CSplitterWnd::DeleteRow(...) and CSplitterWnd:: DeleteColumn(...)

Sample 3.3\Spw is based on sample 3.2\Spw. In the new sample, the behavior of the split bar is modified: if the user double clicks on it, it will move a small step downward (for horizontal split bar) or rightward (for vertical split bar). A pane will be deleted after it reaches its minimum size.

In the sample application, first a new class MCSplitterWnd is derived from class CSplitterWnd:

class MCSplitterWnd : public CSplitterWnd

{

public:

void DeleteRow(int);

void DeleteColumn(int);

};

The class does nothing but overriding two functions. The implementation of function MCSplitterWnd ::DeleteRow(...) is listed as follows:

(Code omitted)

Since the maximum number of rows that can be implemented in a dynamic split window is 2, we will call the default implementation of this function (the corresponding function of the base class) if the number of rows is not 2. Otherwise, we first obtain the size of upper pane (pane 0), enlarge its vertical size, and set its current size. Then the current size of lower pane is reduced, if its ideal size is smaller than its minimum size after change, we simply call function CSplitterWnd::DeleteRow(...) to delete this row. If the panes are resized instead of being deleted, we call function CSplitterWnd::RecalcLayout() to update the new layout.

Function MCSplitterWnd::DeleteColumn(int colDelete) is implemented in the same way, except that here we call all the functions dealing with column instead of row.

Using the New Class

Using this new class is simple, we just need to include the header file containing class MCSplitterWnd in file "MainFrm.h", then use it to declare variable m_wndSpw in class CMainFrame as follows:

class CMainFrame : public CFrameWnd

{

protected:

CMainFrame();

DECLARE_DYNCREATE(CMainFrame)

MCSplitterWnd m_wndSp;

......

}

After these changes, by compiling and executing the application again, we will see that the split bar behaves differently.

3.4 Customizing the Default Appearance

Drawing Functions

Class CSplitterWnd has two member functions that can be overridden to customize the appearance of split bar, split box, split border, and split tracker. The functions are CSplitterWnd::OnDrawSplitter(...) and CSplitter::OnInvertTracker(...) respectively, which have the following formats:

void CSplitterWnd::OnDrawSplitter(CDC *pDC, ESplitType nType, const CRect &rect);

void CSplitterWnd::OnInvertTracker(const CRect &rect);

Function CSplitterWnd::OnDrawSplitter(...) is called when either the split bar, split box or split border needs to be painted. It has three parameters, the first of which is a pointer to the target device DC, which will be used to draw the objects. The second parameter is an enumerate type, which indicates what type of object is being drawn. This parameter could be either CSplitterWnd::splitBox, CSplitterWnd::splitBar, or CSplitterWnd::splitBorder, which indicates different splitter window objects. The third parameter specifies a rectangle region within which the object will be drawn.

Function CSplitterWnd::OnInvertTracker(...) is called when the user clicks the mouse on the split bar and drags it to resize the panes contained in the splitter window. In this case, a tracker will appear on the screen and move with the mouse. By default, the tracker is a grayed line. By overriding this function, we could let the tracker have a different appearance.

Sample

Sample 3.4\Spw demonstrates how to customize these styles. It is based on sample 3.3\Spw. First, two functions are declared in class MCSplitterWnd to override the default implementation:

class MCSplitterWnd : public CSplitterWnd

{

public:

virtual void DeleteRow(int);

virtual void DeleteColumn(int);

protected:

virtual void OnDrawSplitter(CDC*, CSplitterWnd::ESplitType, const CRect&);

virtual void OnInvertTracker(const CRect& rect);

};

Function MCSplitterWnd::OnDrawSplitter(...) is overridden as follows:

(Code omitted)

In the above function, first parameter pDC is checked. If it is not an available DC, we do nothing but calling the default implementation of the base class. Otherwise, the object type is checked. We will go on to implement the customization if the object is either a split bar or a split box.

The simplest way to fill a rectangle with certain pattern is to use brush. A brush can be different types: solid, hatched, etc. It could also be initialized with any color. To use a brush, we need to first create brush, then select it into the device context. If we draw a rectangle with this DC, the interior of the rectangle will be automatically filled with the currently selected brush, and its border will be drawn using the currently selected pen. After using the brush, we must select it out of the DC.

Brush selection can be implemented by calling function CDC::SelectObject(...). This function will return a pointer to the old brush. After using the brush, we can call this function again and pass the old brush to it. This will let the old brush be selected into the DC so the new brush is selected out.

When creating a brush, we need to use RGB macro to indicate the brush color. The three parameters of RGB macro indicate the intensity of red, green and blue colors.

A rectangle can be drawn by calling function CDC::Rectangle(...). We need to pass a CRect type variable to indicate the position and size of the rectangle.

In the sample, function MCSplitterWnd::OnInvertTracker(...)is implemented as follows:

(Code omitted)

There is no CDC type pointer passed to this function. However, for any window, its DC could always be obtained by calling function CWnd::GetDC(). This function will return a pointer to window's device context. After we use the DC, we must release it by calling function CWnd::ReleaseDC(...). In function MCSplitterWnd::OnDrawSplitter(...), first a solid brush with red color is created, then we select it into the DC, call function CDC::PatBlt(...) to fill the interior of the rectangle using the selected brush.

Function CDC::PatBlt(...) allows us to create a pattern on the device. We can choose different color output mode: we can copy the brush color to the destination, or we can combine brush color with the color on the target device using bit-wise operations. The first four parameters of function CDC::PatBlt(...) indicate the position and size of the rectangle within which we can output the pattern. The fifth parameter indicates the output mode. In the sample we use PATINVERT drawing mode, this will combine the destination color and brush color using bit-wise XOR operation. With this mode, the tracker can be easily erased if it is drawn twice.

Since we use PATINVERT mode to paint the tracker, its color will become the complement color of red when the user resizes panes using the mouse.

3.5 Splitter Window That Can't be Resized by Tracking

Sometimes we want each pane of the splitter window to have a fixed size and prevent the user from resizing the panes through using mouse or keyboard. Since dynamic resizing is a built-in feature of class CSplitterWnd, whenever we directly derive a class from it, we will automatically have a resizable split bar. It is not easy to disable this feature because by default, mouse clicking and dragging events will be processed automatically.

In class CSplitterWnd, four mouse messages are handled to change the state of the split bar: mouse left button down message WM_LBUTTONDOWN, mouse left button up message WM_LBUTTONUP, mouse move message WM_MOUSEMOVE, and left button double click message WM_LBUTTONDBLCLK. We need to disable only the first message handler if we want to disable tracking resize feature (Once the application cannot enter the tracking state, the rest messages will be processed normally instead of being treated as part of tracking instructions).

By default, when left button is clicked on a split bar, class CSplitterWnd will respond to this event by letting the user drag the split bar and place it to a new place. We can bypass this feature by overriding WM_LBUTTONDOWN message handler. Instead of calling the message handler implemented by CSplitterWnd, we can call the default function implemented by class CWnd, which is the base class of CSplitterWnd. For a dynamic splitter window, the WM_LBUTTONDBLCLK message handler should not be overridden because after we disable the tracking, double clicking becomes the only way that can be used by the user to dynamically add or delete panes. For WM_MOUSEMOVE and WM_LBUTTONUP messages, we don't need to modify their handlers because after message WM_LBUTTONDOWN is bypassed, the tracking will not happen anymore.

Sample 3.5\Spw demonstrates how to implement splitter window that cannot be resized through tracking the split bar. It is based on sample 3.4\Spw.

To let class MCSplitterWnd support both resizable and non-resizable split bars, a new Boolean type variable m_bResizable is declared in the class. Along with this variable a new member function MCSplitterWnd::SetResizable(...) is also declared, which can be called to set m_bResizable flag and indicate if tracking resize feature is currently supported. At the beginning variable MCSplitterWnd::m_bResizable is initialized to TRUE in the constructor. The following code fragment shows the modified class:

(Code omitted)

A WM_LBUTTONDOWN message handler is added to the application. This includes function declaration, adding ON_WM_LBUTTONDOWN message mapping macro, and the implementation of member function. Before adding message mapping, we need to make sure that DECLARE_MESSAGE_MAP macro is included in the class. This will enable massage mapping for the class. The following lists necessary steps for implementing the above message mapping:

1) Declare an afx_msg type member function OnLButtonDown(...) in the class. This function is originally declared in class CWnd, here we must declare it again in order to override it:

class MCSplitterWnd : public CSplitterWnd

{

......

protected:

......

afx_msg void OnLButtonDown(UINT, CPoint);

DECLARE_MESSAGE_MAP()

};

2) In the implementation file, add ON_WM_LBUTTONDOWN macro between BEGIN_MESSAGE_MAP and END_MESSAGE_MAP macros:

BEGIN_MESSAGE_MAP(MCSplitterWnd, CSplitterWnd)

//{{AFX_MSG_MAP(MCSplitterWnd)

//}}AFX_MSG_MAP

ON_WM_LBUTTONDOWN()

END_MESSAGE_MAP()

Macro ON_WM_LBUTTONDOWN maps message WM_LBUTTONDOWN to function OnLButtonDown(...).

3) Implement the message handler as follows:

void MCSplitterWnd::OnLButtonDown(UINT uFlags, CPoint point)

{

if(m_bResizable == TRUE)CSplitterWnd::OnLButtonDown(uFlags, point);

else CWnd::OnLButtonDown(uFlags, point);

}

The function implementation is simple. If the splitter window is trackable, we call CSplitterWnd::OnLButtonDown(...), which will implement tracking if mouse cursor is over the split bar. Otherwise we bypass this feature by calling function CWnd::OnLButtonDown(...).

If the splitter window is created by the Application Wizard, there will be a command View | Split implemented in the application. By default, this command gives an alternate way to resize panes by tracking the split bar. If we want to disable the tracking completely, we also need to disable or remove this menu command.

Summary

1) To implement static splitter window, we need to derive a class from CView (or other type of view classes) for each pane, then declare a CSplitterWnd type variable in class CMainFrame. In function CMainFrame::OnCreateClient(...), we need to call CSplitterWnd::CreateStatic(...) to create the splitter window and call CSplitterWnd::CreateView(...) to attach a view to each pane.

2) Static splitter window can be nested. This means instead of attaching a view, we can use CSplitterWnd to further create splitter window within a pane.

3) Creating dynamic splitter window is simple. In order to do this, we need to declare a CSplitterWnd type variable in class CMainFrame; then in CMainFrame::OnCreateClient(...), we need to call function CSplitterWnd::Create(...).

4) We can override functions CSplitterWnd::DeleteRow(...) and CSplitterWnd::DeleteColumn(...) to customize the behavior of split bars.

5) We can override functions CSplitterWnd::OnDrawSplitter(...) and CSplitterWnd:: OnInvertTracker(...) to customize the appearance of split bar, split box, split border and tracker.

6) To disable split bar tracking, we need to call CWnd::OnLButtonDown(...) instead of CSplitterWnd:: OnLbuttonDown(...) when handling message WM_LBUTTONDOWN.

 

BACK TO INDEX