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:
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 )
{
m_wndVertSplit.Create ( *this, rcDefault, NULL,
0, WS_EX_CLIENTEDGE );
m_hWndClient = m_wndVertSplit;
UpdateLayout();
m_wndVertSplit.SetSplitterPos ( 200 );
return0;
}
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.
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 )
{
m_wndVertSplit.Create ( *this, rcDefault, NULL,
0, WS_EX_CLIENTEDGE );
m_wndFormatList.Create ( m_wndVertSplit, rcDefault );
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) );
m_hWndClient = m_wndVertSplit;
UpdateLayout();
m_wndVertSplit.SetSplitterPos ( 200 );
return0;
}
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 HWND
s 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) );
m_wndVertSplit.SetSplitterPanes ( m_wndFormatList, m_wndDataViewer );
m_hWndClient = m_wndVertSplit;
UpdateLayout();
m_wndVertSplit.SetSplitterPos ( 200 );
return0;
}
And here's what the result looks like, after the list control has had some columns added:
Note that the splitter puts no restrictions on what windows can go in the panes, unlike MFC where you are supposed to use CView
s. 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.
-
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.
-
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.
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:
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 );
m_wndPaneContainer.Create ( m_wndVertSplit, IDS_PANE_CONTAINER_TEXT );
m_wndFormatList.Create ( m_wndPaneContainer, rcDefault );
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.
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()
{
m_wndHorzSplit.Create ( *this, rcDefault );
m_wndVertSplit.Create ( m_wndHorzSplit, rcDefault );
m_hWndClient = m_wndHorzSplit;
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:
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:
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 );
m_hWndClient = m_wndHorzSplit;
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)
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 );
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):
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)
};
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:
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:
- Get the
RECT
of the first status bar pane
- Create a progress bar control as a child of the status bar, with is
RECT
set to the RECT
of the pane
- 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
.