Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WTL for MFC Programmers, Part VII - Splitter Windows

0.00/5 (No votes)
18 Jan 2006 10  
A tutorial on using WTL splitter windows and pane containers.

Contents

Introduction

Splitter windows have been a popular UI element since Explorer debuted in Windows 95, with its two-pane view of the file system. MFC has a complex and powerful splitter window class, however it is somewhat difficult to learn how to use, and coupled to the doc/view framework. In Part VII, I will discuss the WTL splitter window, which is much less complicated than MFC's. While WTL's splitter implementation does have fewer features than MFC's, it is far easier to use and extend.

The sample project for this part will be a rewrite of ClipSpy, using WTL of course instead of MFC. If you're not familiar with that program, please check out the article now, as I will be duplicating the functionality of ClipSpy here without providing in-depth explanations of how it works. This article's focus is the splitter window, not the clipboard.

WTL Splitter Windows

The header file atlsplit.h contains all of the WTL splitter window classes. There are three classes: CSplitterImpl, CSplitterWindowImpl, and CSplitterWindowT. The classes and their basic methods are explained below.

Classes

CSplitterImpl is a template class that takes two template parameters, a window interface class name and a boolean that indicates the splitter orientation: true for vertical, false for horizontal. CSplitterImpl has almost all of the implementation for a splitter, and many methods are overridable so you can provide custom drawing of the split bar or other effects. CSplitterWindowImpl derives from CWindowImpl and CSplitterImpl, but doesn't have much code. It has an empty WM_ERASEBKGND handler, and a WM_SIZE handler that resizes the splitter window.

Finally, CSplitterWindowT derives from CSplitterImpl and provides a window class name. If you don't need to do any customization, there are two convenient typedefs that you can use: CSplitterWindow for a vertical splitter, and CHorSplitterWindow for a horizontal splitter.

Creating a splitter

Since CSplitterWindow derives from CWindowImpl, you create a splitter just like any other child window. When a splitter will exist for the lifetime of the main frame, as it will in ClipSpy, you can add a CSplitterWindow member variable to CMainFrame. In CMainFrame::OnCreate(), you create the splitter as a child of the frame, then set the splitter as the main frame's client window:

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
// ...

  m_wndSplit.Create ( *this, rcDefault );
  m_hWndClient = m_wndSplit;
}

After creating the splitter, you can assign windows to its panes, and do any other necessary initialization.

Basic methods

bool SetSplitterPos(int xyPos = -1, bool bUpdate = true)
int GetSplitterPos()

Call SetSplitterPos() to set the position of the splitter bar. The position is expressed in pixels relative to the top edge (for horizontal splitters) or left edge (for vertical splitters) of the splitter window. You can use the default of -1 to position the splitter bar in the middle, making both panes the same size. You will usually pass true for bUpdate, to have the splitter immediately resize the panes accordingly. GetSplitterPos() returns the current position of the splitter bar, relative to the top or left edge of the splitter window. (If the splitter is in single-pane mode, GetSplitterPos() returns the position the bar will return to when both panes are shown.)

bool SetSinglePaneMode(int nPane = SPLIT_PANE_NONE)
int GetSinglePaneMode()

Call SetSinglePaneMode() to change the splitter between one-pane and two-pane mode. In one-pane mode, only one pane is visible and the splitter bar is hidden, similar to how MFC dynamic splitters work (although there is no little gripper handle to re-split the splitter). The allowable values for nPane are: SPLIT_PANE_LEFT, SPLIT_PANE_RIGHT, SPLIT_PANE_TOP, SPLIT_PANE_BOTTOM, and SPLIT_PANE_NONE. The first four indicate which pane to show (for example, passing SPLIT_PANE_LEFT shows the left-side pane and hides the right-side pane). Passing SPLIT_PANE_NONE shows both panes. GetSinglePaneMode() returns one of those five SPLIT_PANE_* values indicating the current mode.

DWORD SetSplitterExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
DWORD GetSplitterExtendedStyle()

Splitter windows have extended styles that control how the splitter bar moves when the entire splitter window is resized. The available styles are:

  • SPLIT_PROPORTIONAL: Both panes in the splitter resize together
  • SPLIT_RIGHTALIGNED: The right pane stays the same size when the entire splitter is resized, and the left pane resizes
  • SPLIT_BOTTOMALIGNED: The bottom pane stays the same size when the entire splitter is resized, and the top pane resizes

If none of those three styles are specified, the splitter defaults to being left- or top-aligned. If you pass SPLIT_PROPORTIONAL and SPLIT_RIGHTALIGNED/SPLIT_BOTTOMALIGNED together, SPLIT_PROPORTIONAL takes precedence.

There is one additional style that controls whether the user can move the splitter bar:

  • SPLIT_NONINTERACTIVE: The splitter bar cannot be moved and does not respond to the mouse

The default value of the extended styles is SPLIT_PROPORTIONAL.

bool SetSplitterPane(int nPane, HWND hWnd, bool bUpdate = true)
void SetSplitterPanes(HWND hWndLeftTop, HWND hWndRightBottom, bool bUpdate = true)
HWND GetSplitterPane(int nPane)

Call SetSplitterPane() to assign a child window to one pane of the splitter. nPane is one of the SPLIT_PANE_* values indicating which pane you are setting. hWnd is the window handle of the child window. You can assign child windows to both panes at once with SetSplitterPanes(). You will usually use the default value of bUpdate, which tells the splitter to immediately resize the child windows to fit in the panes. SetSplitterPane() returns a bool, however it will only return false if you pass an invalid value for nPane.

You can get the HWND of the window in a pane with GetSplitterPane(). If no window has been assigned to a pane, GetSplitterPane() returns NULL.

bool SetActivePane(int nPane)
int GetActivePane()

SetActivePane() sets the focus to one of the windows in the splitter. nPane is one of the SPLIT_PANE_* values indicating which pane you are setting as the active one. It also sets the default active pane (explained below). GetActivePane() checks the window with the focus, and if that window is a pane window or a child of a pane window, returns a SPLIT_PANE_* value indicating which pane. If the window with the focus is not a child of a pane, GetActivePane() returns SPLIT_PANE_NONE.

bool ActivateNextPane(bool bNext = true)

If the splitter is in single-pane mode, the focus is set to the visible pane. Otherwise, ActivateNextPane() checks the window with the focus using GetActivePane(). If a pane (or child of a pane) has the focus, the splitter sets the focus to the other pane. Otherwise, ActivateNextPane() activates the left/top pane if bNext is true, or the right/bottom pane if bNext is false.

bool SetDefaultActivePane(int nPane)
bool SetDefaultActivePane(HWND hWnd)
int GetDefaultActivePane()

Call SetDefaultActivePane() with either a SPLIT_PANE_* value or window handle to set that pane as the default active pane. If the splitter window itself gets the focus, via a SetFocus() call, it in turn sets the focus to the default active pane. GetDefaultActivePane() returns a SPLIT_PANE_* value indicating the current default active pane.

void GetSystemSettings(bool bUpdate)

GetSystemSettings() reads various system settings and sets data members accordingly. Pass true for bUpdate to have the splitter immediately redraw itself using the new settings.

The splitter calls this method when it is created, so you don't have to call it yourself. However, your main frame should handle the WM_SETTINGCHANGE message and pass it along to the splitter; CSplitterWindow calls GetSystemSettings() in its WM_SETTINGCHANGE handler.

Data members

Some other splitter features are controlled by setting public members of CSplitterWindow. These are all reset when GetSystemSettings() is called.

m_cxySplitBar
For vertical splitters: Controls the width of the splitter bar. The default is the value returned by GetSystemMetrics(SM_CXSIZEFRAME).
For horizontal splitters: Controls the height of the splitter bar. The default is the value returned by GetSystemMetrics(SM_CYSIZEFRAME).
m_cxyMin
For vertical splitters: Controls the minimum width of each pane. The splitter will not allow you to drag the bar if it would make either pane smaller than this number of pixels. The default is 0 if the splitter window has the WS_EX_CLIENTEDGE extended window style. Otherwise, the default is 2*GetSystemMetrics(SM_CXEDGE).
For horizontal splitters: Controls the minimum height of each pane. The default is 0 if the splitter window has the WS_EX_CLIENTEDGE extended window style. Otherwise, the default is 2*GetSystemMetrics(SM_CYEDGE).
m_cxyBarEdge
For vertical splitters: Controls the width of the 3D edge drawn on the sides of the splitter bar. The default value is 2*GetSystemMetrics(SM_CXEDGE) if the splitter window has the WS_EX_CLIENTEDGE extended window style, otherwise the default is 0.
For horizontal splitters: Controls the height of the 3D edge drawn on the sides of the splitter bar. The default value is 2*GetSystemMetrics(SM_CYEDGE) if the splitter window has the WS_EX_CLIENTEDGE extended window style, otherwise the default is 0.
m_bFullDrag
If this member is set to true, the panes resize as the splitter bar is dragged. If it is false, only a ghost image of the splitter bar is drawn, and the panes don't resize until the user releases the splitter bar. The default is the value returned by SystemParametersInfo(SPI_GETDRAGFULLWINDOWS).

Starting the Sample Project

Now that we have the basics out of the way, let's see how to set up a frame window that contains a splitter. Start a new project with the WTL AppWizard. On the first page, leave SDI Application selected and click Next. On the second page, uncheck Toolbar, then uncheck Use a view window as shown here:

 [AppWizard pg 2 - 22K]

 [VC7 AppWizard - 23K]

We don't need a view window because the splitter and its panes will become the "view." In CMainFrame, add a CSplitterWindow member:

class CMainFrame : public ...
{
//...

protected:
  CSplitterWindow  m_wndVertSplit;
};

Then in OnCreate(), create the splitter and set it as the view window:

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...

  // Create the splitter window

  m_wndVertSplit.Create ( *this, rcDefault, NULL,
                          0, WS_EX_CLIENTEDGE );
 
  // Set the splitter as the client area window, and resize

  // the splitter to match the frame size.

  m_hWndClient = m_wndVertSplit;
  UpdateLayout();
 
  // Position the splitter bar.

  m_wndVertSplit.SetSplitterPos ( 200 );
 
  return 0;
}

Note that you need to set m_hWndClient and call CFrameWindowImpl::UpdateLayout() before setting the splitter position. UpdateLayout() resizes the splitter window to its initial size. If you skip that step, the splitter's size isn't under your control and it might be smaller than 200 pixels wide. The end result would be that SetSplitterPos() wouldn't have the effect you wanted.

An alternative to calling UpdateLayout() is to get the client RECT of the frame window, and use that RECT when creating the splitter, instead of rcDefault. This way, you create the splitter in its initial position, and all subsequent methods dealing with position (like SetSplitterPos()) will work correctly.

If you run the app now, you'll see the splitter in action. Even without creating anything for the panes, the basic behavior is there. You can drag the bar, and double-clicking it moves the bar to the center.

 [Empty splitter - 4K]

To demonstrate different ways of managing the pane windows, I'll use one CListViewCtrl-derived class, and a plain CRichEditCtrl. Here's a snippet from the CClipSpyListCtrl class, which we'll use in the left pane:

typedef CWinTraitsOR<LVS_REPORT | LVS_SINGLESEL | LVS_NOSORTHEADER>
          CListTraits;
 
class CClipSpyListCtrl :
  public CWindowImpl<CClipSpyListCtrl, CListViewCtrl, CListTraits>,
  public CCustomDraw<CClipSpyListCtrl>
{
public:
  DECLARE_WND_SUPERCLASS(NULL, WC_LISTVIEW)
 
  BEGIN_MSG_MAP(CClipSpyListCtrl)
    MSG_WM_CHANGECBCHAIN(OnChangeCBChain)
    MSG_WM_DRAWCLIPBOARD(OnDrawClipboard)
    MSG_WM_DESTROY(OnDestroy)
    CHAIN_MSG_MAP_ALT(CCustomDraw<CClipSpyListCtrl>, 1)
    DEFAULT_REFLECTION_HANDLER()
  END_MSG_MAP()
//...

};

If you've been following the previous articles, you should have no trouble reading this class. It handles WM_CHANGECBCHAIN to know when other clipboard viewers come and go, and WM_DRAWCLIPBOARD to know when the contents of the clipboard change.

Since the pane windows will exist for the life of the app, we can use member variables in CMainFrame for them as well:

class CMainFrame : public ...
{
//...

protected:
  CSplitterWindow  m_wndVertSplit;
  CClipSpyListCtrl m_wndFormatList;
  CRichEditCtrl    m_wndDataViewer;
};

Creating windows in the panes

Now that we have member variables for the splitter and the panes, filling in the splitter is a simple matter. After creating the splitter window, we create both child windows, using the splitter as their parent:

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...

  // Create the splitter window

  m_wndVertSplit.Create ( *this, rcDefault, NULL,
                          0, WS_EX_CLIENTEDGE );
 
  // Create the left pane (list of clip formats)

  m_wndFormatList.Create ( m_wndVertSplit, rcDefault );
 
  // Create the right pane (rich edit ctrl)

  DWORD dwRichEditStyle = 
        WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
        ES_READONLY | ES_AUTOHSCROLL | 
        ES_AUTOVSCROLL | ES_MULTILINE;
 
  m_wndDataViewer.Create ( m_wndVertSplit, rcDefault, 
                           NULL, dwRichEditStyle );
  m_wndDataViewer.SetFont ( AtlGetStockFont(ANSI_FIXED_FONT) );
 
  // Set the splitter as the client area window, and resize

  // the splitter to match the frame size.

  m_hWndClient = m_wndVertSplit;
  UpdateLayout();
 
  m_wndVertSplit.SetSplitterPos ( 200 );
 
  return 0;
}

Notice that both Create() calls use m_wndVertSplit as the parent window. The RECT parameter is not important, since the splitter will resize both pane windows as necessary, so we can use CWindow::rcDefault.

The last step is to pass the HWNDs of the panes to the splitter. This also has to come before UpdateLayout() so all the windows end up the correct size.

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...

  m_wndDataViewer.SetFont ( AtlGetStockFont(ANSI_FIXED_FONT) );
 
  // Set up the splitter panes

  m_wndVertSplit.SetSplitterPanes ( m_wndFormatList, m_wndDataViewer );
 
  // Set the splitter as the client area window, and resize

  // the splitter to match the frame size.

  m_hWndClient = m_wndVertSplit;
  UpdateLayout();
 
  m_wndVertSplit.SetSplitterPos ( 200 );
 
  return 0;
}

And here's what the result looks like, after the list control has had some columns added:

 [Splitter w/panes - 4K]

Note that the splitter puts no restrictions on what windows can go in the panes, unlike MFC where you are supposed to use CViews. The pane windows should have at least the WS_CHILD style, but beyond that you're pretty much free to use anything.

Effects of WS_EX_CLIENTEDGE

A little sidebar is in order about the effect that the WS_EX_CLIENTEDGE style has on the splitter and the windows in the panes. There are three places where we can apply this style: the main frame, the splitter window, or the window in a splitter pane. WS_EX_CLIENTEDGE creates a different look in each case, so I will illustrate them here.

WS_EX_CLIENTEDGE on the frame window:
This is the least appealing choice, since the border of the splitter has an edge, but the bar has no edge.

 [Client edge style on main frame - 4K]  [Client edge style on main frame (XP) - 7K]

WS_EX_CLIENTEDGE on the splitter window:
When a CSplitterWindow has the WS_EX_CLIENTEDGE style, the drawing code takes the extra step of drawing a border along each side of the bar, so that there is an edge around each pane as well as around the entire splitter window.

 [Client edge style on splitter - 4K]  [Client edge style on splitter (XP) - 7K]

WS_EX_CLIENTEDGE on the pane windows:
Each pane window has a border, and the splitter bar merges into the frame window's menu and border without any breaks. This is more noticeable on pre-XP Windows (or XP with themes turned off). On XP with themes turned on, it's hard tell that there's a splitter bar there unless you go hunting with the mouse.

 [Client edge style on pane - 4K]  [Client edge style on pane (XP) - 7K]

Message Routing

Since we now have another window sitting between the main frame and the pane windows, you might have wondered how notification messages work. Specifically, how can the main frame receive NM_CUSTOMDRAW notifications so it can reflect them to the list? The answer can be found in the CSplitterWindowImpl message map:

  BEGIN_MSG_MAP()
    MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
    MESSAGE_HANDLER(WM_SIZE, OnSize)
    CHAIN_MSG_MAP(baseClass)
    FORWARD_NOTIFICATIONS()
  END_MSG_MAP()

The FORWARD_NOTIFICATIONS() macro at the end is the important one. Recall from Part IV that there are several notification messages which are always sent to the parent of a child window. What FORWARD_NOTIFICATIONS() does is re-send the message to the splitter's parent window. So when the list sends a WM_NOTIFY message to the splitter (the list's parent), the splitter in turn sends the WM_NOTIFY to the main frame (the splitter's parent). When the main frame reflects the message, it is sent back to the window that generated the WM_NOTIFY in the first place, so the splitter doesn't get involved in reflection.

The result of all this is that notification messages sent between the main frame and the list don't get affected by the presence of the splitter window. This makes it rather easy to add or remove splitters, because the child window classes won't have to be changed at all for their message processing to continue working.

Pane Containers

WTL also supports a widget like the one in the left pane of Explorer, called a pane container. This control provides a header area with text, and optionally a Close button:

 [Explorer pane container - 3K]

The pane container manages a child window, just as the splitter manages two pane windows. When the container is resized, the child is automatically resized to match the space inside the container.

Classes

There are two classes in the implementation of pane containers, both in atlctrlx.h: CPaneContainerImpl and CPaneContainer. CPaneContainerImpl is a CWindowImpl-derived class that contains the complete implementation; CPaneContainer provides just a window class name. Unless you want to override any methods to change how the container is drawn, you will always use CPaneContainer.

Basic methods

HWND Create(
    HWND hWndParent, LPCTSTR lpstrTitle = NULL,
    DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
    DWORD dwExStyle = 0, UINT nID = 0, LPVOID lpCreateParam = NULL)
HWND Create(
    HWND hWndParent, UINT uTitleID,
    DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
    DWORD dwExStyle = 0, UINT nID = 0, LPVOID lpCreateParam = NULL)

Creating a CPaneContainer is similar to creating other child windows. There are two Create() methods that differ in just the second parameter. In the first version, you pass a string that will be used for the title text drawn in the header. In the second method, you pass the ID of a string table entry. The defaults for the remaining parameters are usually sufficient.

DWORD SetPaneContainerExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
DWORD GetPaneContainerExtendedStyle()

CPaneContainer has additional extended styles that control the close button and the layout of the container:

  • PANECNT_NOCLOSEBUTTON: Set this style to remove the Close button from the header.
  • PANECNT_VERTICAL: Set this style to make the header area vertical, along the left side of the container window.

The default value of the extended styles is 0, which results in a horizontal container with a close button.

HWND SetClient(HWND hWndClient)
HWND GetClient()

Call SetClient() to assign a child window to the pane container. This works similarly to the SetSplitterPane() method in CSplitterWindow. SetClient() returns the HWND of the old client window. Call GetClient() to get the HWND of the current client window.

BOOL SetTitle(LPCTSTR lpstrTitle)
BOOL GetTitle(LPTSTR lpstrTitle, int cchLength)
int GetTitleLength()

Call SetTitle() to change the text shown in the header area of the container. Call GetTitle() to retrieve the current header text, and call GetTitleLength() to get the length in characters of the current header text (not including the null terminator).

BOOL EnableCloseButton(BOOL bEnable)

If the pane container has a Close button, you can use EnableCloseButton() to enable and disable it.

Using a pane container in a splitter window

To demonstrate how to add a pane container to an existing splitter, we'll add a container to the left pane of the ClipSpy splitter. Instead of assigning the list control to the left pane, we assign the pane container. The list is then assigned to the pane container. Here are the lines in CMainFrame::OnCreate() to change to set up the pane container.

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
//...

  m_wndVertSplit.Create ( *this, rcDefault );
 
  // Create the pane container.

  m_wndPaneContainer.Create ( m_wndVertSplit, IDS_PANE_CONTAINER_TEXT );
 
  // Create the left pane (list of clip formats)

  m_wndFormatList.Create ( m_wndPaneContainer, rcDefault );
//...

  // Set up the splitter panes

  m_wndPaneContainer.SetClient ( m_wndFormatList );
  m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );

Notice that the parent of the list control is m_wndPaneContainer. Also, m_wndPaneContainer is set as the left pane of the splitter. Here's what the modified left pane looks like.

 [Pane container - 5K]

The Close button and message handling

When the user clicks the Close button, the pane container sends a WM_COMMAND message to its parent, with a command ID of ID_PANE_CLOSE (a constant defined in atlres.h). When you use the pane container in a splitter, the usual course of action is to call SetSinglePaneMode() to hide the splitter pane that has the pane container. (But remember to provide a way for the user to show the pane again!)

The CPaneContainer message map also has the FORWARD_NOTIFICATIONS() macro, just like CSplitterWindow, so the container passes notification messages from its client window to its parent. In the case of ClipSpy, there are two windows between the list control and the main frame (the pane container and the splitter), but the FORWARD_NOTIFICATIONS() macros ensure that all notifications from the list arrive at the main frame.

Advanced Splitter Features

In this section, I'll describe how to do some common advanced UI tricks with WTL splitters.

Nested splitters

If you plan on writing an app such as an email client or RSS reader, you'll probably end up using nested splitters - one horizontal and one vertical. This is easy to do with WTL splitters - you create one splitter as the child of the other.

To show this in action, we'll add a horizontal splitter to ClipSpy. The horizontal splitter will be the topmost one, and the vertical splitter will be nested in it. After adding a CHorSplitterWindow member called m_wndHorzSplitter, we create that splitter the same way as we create m_wndVertSplitter. To make m_wndHorzSplitter the topmost splitter. m_wndVertSplitter is now created as a child of m_wndHorzSplitter. Finally, m_hWndClient is set to m_wndHorzSplitter, since that's the window that now occupies the main frame's client area.

LRESULT CMainFrame::OnCreate()
{
//...

  // Create the splitter windows.

  m_wndHorzSplit.Create ( *this, rcDefault );
  m_wndVertSplit.Create ( m_wndHorzSplit, rcDefault );
//...

  // Set the horizontal splitter as the client area window.

  m_hWndClient = m_wndHorzSplit;
 
  // Set up the splitter panes

  m_wndPaneContainer.SetClient ( m_wndFormatList );
  m_wndHorzSplit.SetSplitterPane ( SPLIT_PANE_TOP, m_wndVertSplit );
  m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );
//...

}

And here's what the result looks like:

 [Horz splitter w/empty pane - 5K]

Using ActiveX controls in a pane

Hosting an ActiveX control in a splitter pane is similar to hosting a control in a dialog. You create the control at runtime using CAxWindow methods, then assign the CAxWindow to a pane in the splitter. Here's how you would add a browser control to the bottom pane of the horizontal splitter:

    // Create the bottom pane (browser)

  CAxWindow wndIE;
  DWORD dwIEStyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN |
                    WS_HSCROLL | WS_VSCROLL;
 
  wndIE.Create ( m_wndHorzSplit, rcDefault, 
                 _T("http://www.codeproject.com"), dwIEStyle );
 
  // Set the horizontal splitter as the client area window.

  m_hWndClient = m_wndHorzSplit;
 
  // Set up the splitter panes

  m_wndPaneContainer.SetClient ( m_wndFormatList );
  m_wndHorzSplit.SetSplitterPanes ( m_wndVertSplit, wndIE );
  m_wndVertSplit.SetSplitterPanes ( m_wndPaneContainer, m_wndDataViewer );

Special drawing

If you want to provide a different appearance for the splitter bar, for example to draw a texture on it, you can derive a class from CSplitterWindowImpl and override DrawSplitterBar(). If you just want to tweak the appearance, you can copy the existing function in CSplitterWindowImpl and make any little changes you want. Here's an example that paints a diagonal hatch pattern in the bar.

template <bool t_bVertical = true>
class CMySplitterWindowT : 
    public CSplitterWindowImpl<CMySplitterWindowT<t_bVertical>, t_bVertical>
{
public:
  DECLARE_WND_CLASS_EX(_T("My_SplitterWindow"), 
                       CS_DBLCLKS, COLOR_WINDOW)
 
  // Overrideables

  void DrawSplitterBar(CDCHandle dc)
  {
  RECT rect;
 
    if ( m_br.IsNull() )
      m_br.CreateHatchBrush ( HS_DIAGCROSS, 
                              t_bVertical ? RGB(255,0,0) 
                                          : RGB(0,0,255) );
 
    if ( GetSplitterBarRect ( &rect ) )
      {
      dc.FillRect ( &rect, m_br );
 
      // draw 3D edge if needed

      if ( (GetExStyle() & WS_EX_CLIENTEDGE) != 0)
        {
        dc.DrawEdge(&rect, EDGE_RAISED, 
                    t_bVertical ? (BF_LEFT | BF_RIGHT) 
                                : (BF_TOP | BF_BOTTOM));
        }
      }
  }
 
protected:
  CBrush m_br;
};
 
typedef CMySplitterWindowT<true>  CMySplitterWindow;
typedef CMySplitterWindowT<false> CMyHorSplitterWindow;

Here's the result (with the bars made wider so the effect is easier to see):

 [custom drawn bars - 14K]

Special Drawing in Pane Containers

CPaneContainer has a few methods that you can override to change the appearance of a pane container. You can derive a new class from CPaneContainerImpl and override the methods you want, for example:

class CMyPaneContainer :
  public CPaneContainerImpl<CMyPaneContainer>
{
public:
  DECLARE_WND_CLASS_EX(_T("My_PaneContainer"), 0, -1)
//... overrides here ...

};

Some of the more interesting methods are:

void CalcSize()

The purpose of CalcSize() is simply to set m_cxyHeader, which controls the width or height of the container's header area. However, there is a bug in SetPaneContainerExtendedStyle() that results in a derived class's CalcSize() not being called when the pane is switched between horizontal and vertical modes. You can fix this by changing line 2215 in atlctrlx.h to call pT->CalcSize() instead of CalcSize().

HFONT GetTitleFont()

This method returns an HFONT, which will be used to draw the header text. The default is the value returned by GetStockObject(DEFAULT_GUI_FONT), which is MS Sans Serif. If you want to use the more modern-looking Tahoma, you can override GetTitleFont() and return a handle to a Tahoma font that you create.

BOOL GetToolTipText(LPNMHDR lpnmh)

Override this method to provide tooltip text when the cursor hovers over the Close button. This method is actually a handler for TTN_GETDISPINFO, so you cast lpnmh to a NMTTDISPINFO* and set the members of that struct accordingly. Keep in mind that you have to check the notification code - it may be TTN_GETDISPINFO or TTN_GETDISPINFOW - and access the struct accordingly.

void DrawPaneTitle(CDCHandle dc)

You can override this method to provide your own drawing for the header area. You can use GetClientRect() and m_cxyHeader to calculate the RECT of the header area. Here is sample code to draw a gradient fill in the header area of a horizontal container:

void CMyPaneContainer::DrawPaneTitle ( CDCHandle dc )
{
RECT rect;
 
  GetClientRect(&rect);
 
TRIVERTEX tv[] =
  { 
    { rect.left, rect.top, 0xff00 },
    { rect.right, rect.top + m_cxyHeader, 0, 0xff00 } 
  };
GRADIENT_RECT gr = { 0, 1 };
 
  dc.GradientFill ( tv, 2, &gr, 1, GRADIENT_FILL_RECT_H );
}

The sample project demonstrates overriding some of these methods, and the result is shown here:

 [Custom drawing in a pane cont. - 6K]

The demo project has a Splitters menu, shown above, that lets you toggle various special drawing features of the splitters and pane containers, so you can see the differences. You can also lock the splitters, which is done by toggling on the SPLIT_NONINTERACTIVE extended style.

Bonus: Progress Bar in the Status Bar

As I promised a couple of articles ago, this new ClipSpy demonstrates how to create a progress bar in the status bar. It works just like the MFC version - the steps involved are:

  1. Get the RECT of the first status bar pane
  2. Create a progress bar control as a child of the status bar, with is RECT set to the RECT of the pane
  3. Update the progress bar position as the edit control is being filled

You can check out the code in CMainFrame::CreateProgressCtrlInStatusBar().

Up Next

In Part 8, I'll tackle the topic of property sheets and wizards.

References

WTL Splitters and Pane Containers by Ed Gadziemski

Copyright and license

This article is copyrighted material, (c)2003-2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.

The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefiting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

  • July 9, 2003: Article first published.
  • January 12, 2006: Mostly editing to fix unclear or badly-worded parts of the article. Updated some screen shots. Added section on WS_EX_CLIENTEDGE.

Series Navigation: � Part VI (Hosting ActiveX Controls) | � Part VIII (Property Sheets and Wizards)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here