Contents
IntroductionSplitter 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 WindowsThe header file atlsplit.h contains all of the WTL splitter window classes. There are three classes: Classes
Finally, Creating a splitterSince 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 methodsbool SetSplitterPos(int xyPos = -1, bool bUpdate = true) int GetSplitterPos() Call bool SetSinglePaneMode(int nPane = SPLIT_PANE_NONE) int GetSinglePaneMode() Call 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:
If none of those three styles are specified, the splitter defaults to being left- or top-aligned. If you pass There is one additional style that controls whether the user can move the splitter bar:
The default value of the extended styles is bool SetSplitterPane(int nPane, HWND hWnd, bool bUpdate = true) void SetSplitterPanes(HWND hWndLeftTop, HWND hWndRightBottom, bool bUpdate = true) HWND GetSplitterPane(int nPane) Call You can get the bool SetActivePane(int nPane) int GetActivePane()
bool ActivateNextPane(bool bNext = true) If the splitter is in single-pane mode, the focus is set to the visible pane. Otherwise, bool SetDefaultActivePane(int nPane) bool SetDefaultActivePane(HWND hWnd) int GetDefaultActivePane() Call void GetSystemSettings(bool bUpdate)
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 Data membersSome other splitter features are controlled by setting public members of
Starting the Sample ProjectNow 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 class CMainFrame : public ... { //...protected: CSplitterWindow m_wndVertSplit; }; Then in 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 ); return0; } Note that you need to set An alternative to calling 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 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 Since the pane windows will exist for the life of the app, we can use member variables in class CMainFrame : public ... { //...protected: CSplitterWindow m_wndVertSplit; CClipSpyListCtrl m_wndFormatList; CRichEditCtrl m_wndDataViewer; }; Creating windows in the panesNow 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 ); return0; } Notice that both The last step is to pass the 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 ); 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 Effects of WS_EX_CLIENTEDGEA little sidebar is in order about the effect that the
Message RoutingSince 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 BEGIN_MSG_MAP() MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground) MESSAGE_HANDLER(WM_SIZE, OnSize) CHAIN_MSG_MAP(baseClass) FORWARD_NOTIFICATIONS() END_MSG_MAP() The 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 ContainersWTL 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. ClassesThere are two classes in the implementation of pane containers, both in atlctrlx.h: Basic methodsHWND 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 DWORD SetPaneContainerExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)
DWORD GetPaneContainerExtendedStyle()
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 BOOL SetTitle(LPCTSTR lpstrTitle) BOOL GetTitle(LPTSTR lpstrTitle, int cchLength) int GetTitleLength() Call BOOL EnableCloseButton(BOOL bEnable) If the pane container has a Close button, you can use Using a pane container in a splitter windowTo 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 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
The Close button and message handlingWhen the user clicks the Close button, the pane container sends a The Advanced Splitter FeaturesIn this section, I'll describe how to do some common advanced UI tricks with WTL splitters. Nested splittersIf 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 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:
Using ActiveX controls in a paneHosting an ActiveX control in a splitter pane is similar to hosting a control in a dialog. You create the control at runtime using // 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 drawingIf 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 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) // Overrideablesvoid 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 neededif ( (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
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 HFONT GetTitleFont() This method returns an 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
void DrawPaneTitle(CDCHandle dc)
You can override this method to provide your own drawing for the header area. You can use 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 Bonus: Progress Bar in the Status BarAs 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:
You can check out the code in Up NextIn Part 8, I'll tackle the topic of property sheets and wizards. ReferencesWTL Splitters and Pane Containers by Ed Gadziemski Copyright and licenseThis 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
About Michael Dunn |