/**************************************************************************** * * * MultiSplit.h: Scrolling multi-panel splitter for use with WTL * * * * Written by Ted Szoczei (ted.szoczei@nimajin.com), * * Nimajin Software Consulting * * for Microtronix Systems Ltd. * * Copyright (c) 2003-2004 Ted Szoczei. * * * * Portions adapted from Windows Template Library - WTL version 7.0, * * atlsplit.h * * Copyright (C) 1997-2002 Microsoft Corporation, All rights reserved. * * * * Permission to copy, use, sell and distribute this file is granted * * provided this copyright notice appears in all copies. * * Permission to modify the code herein and to distribute modified code is * * granted provided this copyright notice appears in all copies, and a * * notice that the code was modified is included with the copyright notice. * * * * This software and information is provided "as is" without express or im- * * plied warranty, and with no claim as to its suitability for any purpose. * * * * Find the latest version at http://www.nimajin.com * * * **************************************************************************** * REVISIONS * * --------- * * DATE VER CHANGES /BY * * ---------- ----- ----------- * * 08-04-25: Added PanesCount property and Contains method. * * Revised Insert so index >= PanesCount will Add, not assert./tsz* * 04-12-13: Added mouse wheel scrolling support. * * Added SetFocus to MouseActivate so mouse wheel works whenever * * window active. /tsz * * 03-05-16: Initial release. /tsz * * * **************************************************************************** * NOTES * * ----- * * Limited mode behaviour: * * For one or two panes, multi-splitter acts the same as a regular two-pane* * splitter. * * Panes are limited in size to fill visible area. * * The last pane resizes to fit the area left by other panes. * * Resizing a pane affects only the pane on the other side of the splitter * * bar. * * Specifying -1 for a pane size resizes all panes to fit evenly within the* * window. * * * * Scroll mode behaviour: * * Panes always start with the size specified, the virtual display area * * grows to fit them. * * Panes can be any size. * * Resizing a pane moves all following panes. * * There is always an empty square pane at the end for the last pane to * * resize into. * * Specifying -1 for a pane size gives it the average size of all existing * * panes, or evenly divides them into visible space if all are -1. * * * * All extended styles can be switched at any time by calling * * SetExtendedStyle. * * Panes can be added to the end or inserted between existing panes. * * Panes can be removed or replaced. Panes are indexed starting with 0. * * * * No right/bottom alignment support. * * * ****************************************************************************/ #ifndef __ATLMULTISPLIT_H__ #define __ATLMULTISPLIT_H__ #pragma once #ifndef __cplusplus #error ATL requires C++ compilation (use a .cpp suffix) #endif #ifndef __ATLAPP_H__ #error multisplit.h requires atlapp.h to be included first #endif #ifndef __ATLWIN_H__ #error multisplit.h requires atlwin.h to be included first #endif #include // CRect #include // CAtlArray #include // CAtlArray ///////////////////////////////////////////////////////////////////////////// // Classes in this file // // CMultiSplitImpl // CMultiSplitWindowImpl // CMultiSplitWindow namespace WTL { ///////////////////////////////////////////////////////////////////////////// // CMultiSplitImpl - Provides multi-pane splitter support to any window // MultiSplit extended styles #define MULTISPLIT_VERTICAL 0x00000001 // split horizontally, splitter bars are vertical #define MULTISPLIT_PROPORTIONAL 0x00000002 // resize keeps same pane proportions #define MULTISPLIT_SCROLL 0x00000004 // panes can be larger than display area #define MULTISPLIT_NONINTERACTIVE 0x00000008 // no mouse response struct CMultiSplitPane // info about a child window { HWND window; // HWND of child int size; // original or changed size int proporSize; // proportionate size int position; // actual positions in view list, subject to splitters moving CMultiSplitPane (void) : window (0), size (0), proporSize (0), position (0) { } CMultiSplitPane (HWND initWindow) : window (initWindow), size (0), proporSize (0), position (0) { } CMultiSplitPane (HWND initWindow, int initSize) : window (initWindow), size (initSize), proporSize (0), position (0) { } }; template class CMultiSplitImpl { public : enum { proportionalMax = 10000 }; // scale range for proportional position DWORD extendedStyle; // splitter specific extended styles CAtlArray panes; // contained child windows CRect visibleRect; // visible area defined by parent int visibleSpace; // width or height of visbleRect depending on style CRect fillRect; // resizing space in scroll mode int barWidth; // splitter bar width/height (system setting) int minPaneSize; // minimum pane size (0 or edgeWidth if window has no edges) int edgeWidth; // splitter bar and client edge width (2 edges) (system setting) int contentSize; // combined width or height of all children & splitter bars int defaultFocusPane; // pane to focus when splitter gets focus static HCURSOR vertCursor; // cursor to display on vertical splitter bar static HCURSOR horzCursor; // cursor to display on horizontal splitter bar int resizingPane; // pane being resized, -1 if none bool drawContentWhileResizing; // resize content while moving splitter bar (system setting) int scrollBar; // none == -1, IsVertical ()? SB_HORZ : SB_VERT int scrollMax; // range for scroll bar int scrollPosition; // position of scroll bar thumb int mouseWheelLines; // number of lines to scroll on mouse wheel // Constructor CMultiSplitImpl (DWORD initStyle = MULTISPLIT_VERTICAL + MULTISPLIT_PROPORTIONAL) : extendedStyle (initStyle), visibleSpace (0), barWidth (0), minPaneSize (0), edgeWidth (0), contentSize (0), defaultFocusPane (0), resizingPane (-1), drawContentWhileResizing (true), scrollMax (0), scrollPosition (0), mouseWheelLines (3) { visibleRect.SetRectEmpty (); fillRect.SetRectEmpty (); if (vertCursor == 0) { ::EnterCriticalSection (&_Module.m_csStaticDataInit); if (vertCursor == 0) vertCursor = ::LoadCursor (0, IDC_SIZEWE); ::LeaveCriticalSection (&_Module.m_csStaticDataInit); } if (horzCursor == 0) { ::EnterCriticalSection (&_Module.m_csStaticDataInit); if (horzCursor == 0) horzCursor = ::LoadCursor (0, IDC_SIZENS); ::LeaveCriticalSection (&_Module.m_csStaticDataInit); } scrollBar = IsLimited ()? -1 : (IsVertical ()? SB_HORZ : SB_VERT); } // Attributes void RectSet (LPRECT newRect = 0, bool update = true) { // define visible area within parent T * pT = static_cast (this); bool Init = visibleRect.IsRectEmpty () == TRUE; if (newRect == 0) // use all of parent's client area pT->GetClientRect (&visibleRect);// includes edges else visibleRect = *newRect; visibleSpace = IsVertical ()? visibleRect.Width () : visibleRect.Height (); // ATLTRACE(_T("CMultiSplitImpl::RectSet: %d %d %d %d (%d %d) %d\n"), // visibleRect.left, visibleRect.top, visibleRect.right, visibleRect.bottom, // visibleRect.Width (), visibleRect.Height (), visibleSpace); if (visibleSpace == 0) return; if (Init) { // confirm pre-set sizes & init proportional values size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) PaneSizeSet (PaneIndex, panes [PaneIndex].size, false); } else if (IsProportional ()) // move splitters proportionately ProportionalUpdate (); // otherwise splitters don't move if (update) { UpdateLayout (); UpdateScrollbar (); } } void RectGet (LPRECT destRect) const { ATLASSERT (!(destRect == 0)); *destRect = visibleRect; } // Changing scrolling or vertical styles updates view. // Changing from scrolling to limited shrinks panes proportionately to fit visible space. // Limited pane sizes are kept when changing to scrolling. // Pane proportions are also kept when changing between vertical & horizontal. DWORD ExtendedStyleSet (DWORD newExtendedStyle, DWORD mask = 0) { DWORD PrevStyle = extendedStyle; int PrevScrollBar = scrollBar; if (mask == 0) // replace all styles extendedStyle = newExtendedStyle; else // mask may reset existing bits & limit new bits extendedStyle = (extendedStyle & ~mask) | (newExtendedStyle & mask); scrollBar = IsLimited ()? -1 : (IsVertical ()? SB_HORZ : SB_VERT); T * pT = static_cast (this); if (!pT->IsWindow ()) // changed before Create return PrevStyle; if ((extendedStyle & MULTISPLIT_VERTICAL) != (PrevStyle & MULTISPLIT_VERTICAL)) { if (PrevScrollBar >= 0) // -1 is no scrollbar { // now either none or different, old must go pT->ShowScrollBar (PrevScrollBar, FALSE); if (scrollBar >= 0) pT->ShowScrollBar (scrollBar, TRUE); else // ShowScrollBar causes resize & RectSet scrollPosition = 0; } else if (scrollBar >= 0) // none before pT->ShowScrollBar (scrollBar, TRUE); else { // no scroll bar change, just direction visibleSpace = IsVertical ()? visibleRect.Width () : visibleRect.Height (); if (IsProportional ()) // move splitbars proportionately ProportionalUpdate (); UpdateLayout (); pT->UpdateWindow (); } } else if ((extendedStyle & MULTISPLIT_SCROLL) != (PrevStyle & MULTISPLIT_SCROLL)) { if (PrevScrollBar >= 0) // was scrolling, now not { LimitedUpdate (); // resize panes to fit in visibleRect scrollPosition = 0; pT->ShowScrollBar (PrevScrollBar, FALSE); } else // no bar before pT->ShowScrollBar (scrollBar, TRUE); } return PrevStyle; } DWORD ExtendedStyleGet (void) const { return extendedStyle; } bool IsVertical (void) const { return (extendedStyle & MULTISPLIT_VERTICAL) != 0; } bool IsProportional (void) const { return (extendedStyle & MULTISPLIT_PROPORTIONAL) != 0; } bool IsScrolling (void) const { return (extendedStyle & MULTISPLIT_SCROLL) != 0; } bool IsLimited (void) const { return (extendedStyle & MULTISPLIT_SCROLL) == 0; } bool IsInteractive (void) const { return (extendedStyle & MULTISPLIT_NONINTERACTIVE) == 0; } // MultiSplit operations // Return the number of windows in the splitter size_t PanesCount (void) const { return panes.GetCount (); } // Return the index of the pane in the splitter occupied by the specified window int PaneIndex (HWND hWnd) const { size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) if (panes [PaneIndex].window == hWnd) return PaneIndex; return -1; } // Return true if the specified window is in the splitter bool Contains (HWND hWnd) const { return PaneIndex (hWnd) >= 0; } // Return the window handle of the child window in pane [index] HWND PaneWindow (unsigned paneIndex) const { ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return false; return panes [paneIndex].window; } // Append a window to the end // Returns pane index int Add (HWND hWnd, int size = -1, bool update = true) { unsigned PaneIndex = PanesCount (); if (visibleSpace) { if (IsLimited ()) PaneInsertLimited (PaneIndex, hWnd, size, update); else { panes.Add (CMultiSplitPane (hWnd)); PaneSizeSet (PaneIndex, size, update); } } else { panes.Add (CMultiSplitPane (hWnd, size)); PanePositionSet (PaneIndex); } return PaneIndex; } // Insert a new pane // Panes are indexed starting with 0. // New pane ends up at the index specified. // If index is too high to insert between existing panes, then window is appended as new pane // Returns pane index // The indexes of existing panes with higher indexes than the new pane are incremented. // Visually, the new pane takes the place of an existing pane with the same index // and the same and higher indexed panes move down or to the right. // In non-scroll mode the last pane will shrink. int Insert (unsigned paneIndex, HWND hWnd, int size = -1, bool update = true) { if (paneIndex >= PanesCount ()) return Add (hWnd, size, update); if (visibleSpace) { if (IsLimited ()) PaneInsertLimited (paneIndex, hWnd, size, update); else { panes.InsertAt (paneIndex, CMultiSplitPane (hWnd)); PaneSizeSet (paneIndex, size, update); } } else { panes.InsertAt (paneIndex, CMultiSplitPane (hWnd, size)); PanePositionsSet (paneIndex); // bump followers } return paneIndex; } // Replacing existing pane with new window. // Existing window is hidden and not destroyed. // size of -1 makes new window the same size as the one it replaces bool Replace (unsigned paneIndex, HWND hWnd, int size = -1, bool update = true) { ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return false; ::ShowWindow (panes [paneIndex].window, SW_HIDE); panes [paneIndex].window = hWnd; if (visibleSpace) { if (size != -1) PaneSizeSet (paneIndex, size, update); else if (update) { T * pT = static_cast (this); if (pT->IsWindow ()) UpdateLayout (); } } return true; } // Remove specified pane // Window in pane is hidden and not destroyed. // The indexes of existing panes with higher indexes than the removed pane are decremented. // Visually, the removed pane disappears and existing panes with higher indexes // move up or to the left. In non-scroll mode the last pane will grow. HWND Remove (unsigned paneIndex, bool update = true) { ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return 0; HWND Window = panes [paneIndex].window; ::ShowWindow (Window, SW_HIDE); panes.RemoveAt (paneIndex); PanePositionsSet (paneIndex); // bump followers T * pT = static_cast (this); if (update && pT->IsWindow ()) { UpdateLayout (); UpdateScrollbar (); pT->UpdateWindow (); } return Window; } // Hide all child windows, delete all panes. void Clear (bool update = true) { size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) ::ShowWindow (panes [PaneIndex].window, SW_HIDE); contentSize = defaultFocusPane = scrollMax = scrollPosition = 0; panes.RemoveAll (); T * pT = static_cast (this); if (update && pT->IsWindow ()) { UpdateLayout (); UpdateScrollbar (); pT->UpdateWindow (); } } // set keyboard focus to paneIndex bool FocusPaneSet (unsigned paneIndex) { ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return false; defaultFocusPane = paneIndex; ::SetFocus (panes [defaultFocusPane].window); return true; } // get the keyboard focus pane int FocusPaneGet (void) const { HWND FocusWindow = ::GetFocus (); if (FocusWindow != 0) { size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) { if (FocusWindow == panes [PaneIndex].window || ::IsChild (panes [PaneIndex].window, FocusWindow)) return PaneIndex; } } return 0; // focus is not in splitter } // set keyboard focus to pane following current focus // if splitter doesn't have focus use first or last bool NextPaneActivate (bool next = true) { int PaneIndex = FocusPaneGet (); if (next) { PaneIndex++; if (PaneIndex >= PanesCount ()) PaneIndex = 0; // wrap } else { // previous if (PaneIndex == 0) { // attempt wrap if (PanesCount () > 0) PaneIndex = PanesCount () - 1; } else PaneIndex--; } return FocusPaneSet (pane); } bool DefaultFocusPaneSet (unsigned paneIndex) { ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return false; defaultFocusPane = paneIndex; return true; } int DefaultFocusPaneGet (void) const { return defaultFocusPane; } int PaneSizeGet (unsigned paneIndex) const { ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return 0; return panes [paneIndex].size; } int PanePortionalSizeGet (unsigned paneIndex) const { ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return 0; return panes [paneIndex].proporSize; } // paneIndex = -1 changes ALL panes to the specified size // newSize = -1 changes ALL panes to visibleSpace/#panes (all same size) // newSize < -1 is proportional size in scale of 10,000 // newSize is size of interior of pane, does not include associated splitBar // update causes redraw - set true if panes are visisble, useless before RectSet // limited mode panels may only resize into the space of the next panel // in limited mode, the last pane always fills to visibleSpace, but its size is retained in case another pane added // in scroll mode the last pane is the size specified // in limited mode pane size growth limited to area of adjacent pane // no pane size limit in scroll mode // used only when visibleSpace is known bool PaneSizeSet (int paneIndex = -1, int newSize = -1, bool update = true) { bool Changed; if (newSize < -1) // convert proportional to real units newSize = ::MulDiv (-newSize, visibleSpace, proportionalMax); if (paneIndex < 0 || newSize < 0) // all must change { if (PanesCount () == 0) return false; newSize = IsLimited ()? EvenPaneSizeLimited (newSize) : EvenPaneSizeScroll (newSize); if (newSize < minPaneSize + edgeWidth) newSize = minPaneSize + edgeWidth; AllPaneSizesSet (newSize); Changed = true; } else { ATLASSERT(static_cast(paneIndex) < PanesCount ()); if (static_cast(paneIndex) >= PanesCount ()) return false; // no panes or invalid index if (newSize < minPaneSize + edgeWidth) newSize = minPaneSize + edgeWidth; Changed = IsLimited ()? PaneSizeSetLimited (static_cast(paneIndex), newSize) : PaneSizeSetScroll (static_cast(paneIndex), newSize); } T * pT = static_cast (this); if (update && Changed && pT->IsWindow ()) { UpdateLayout (); UpdateScrollbar (); } return Changed; } // divide space of pane [paneIndex] and the next pane evenly bool EvenPanes (unsigned paneIndex) { ATLASSERT(paneIndex + 1 < PanesCount ()); if (paneIndex + 1 >= PanesCount ()) return false; int NewSize = (panes [paneIndex].size + panes [paneIndex + 1].size) / 2; bool Changed = PaneSizeStore (paneIndex, NewSize); Changed = PaneSizeStore (paneIndex + 1, NewSize) || Changed; PanePositionSet (paneIndex + 1); T * pT = static_cast (this); if (Changed && pT->IsWindow ()) UpdateLayout (); return Changed; } // draw all splitter bars & empty panes, occupied panes draw themselves void Draw (CDCHandle dc) { ATLASSERT(!(dc.m_hDC == 0)); T * pT = static_cast (this); size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) { if (panes [PaneIndex].window == 0) pT->EmptyPaneDraw (dc, PaneIndex); pT->BarDraw (dc, PaneIndex); // bottom or left } pT->EmptyRectDraw (dc, fillRect); } // Overrideable by derived classes void EmptyPaneDraw (CDCHandle dc, unsigned paneIndex) { // called only if paneIndex is empty RECT Rect; if (PaneRectGet (paneIndex, &Rect)) EmptyRectDraw (dc, Rect); } void EmptyRectDraw (CDCHandle dc, RECT rect) { // called only if paneIndex is empty T * pT = static_cast (this); // draw 3D edge if needed & adjust Rect for fill if ( (pT->GetExStyle () & WS_EX_CLIENTEDGE) == 0) dc.DrawEdge (&rect, EDGE_SUNKEN, BF_RECT | BF_ADJUST); dc.FillRect (&rect, COLOR_APPWORKSPACE); } void BarDraw (CDCHandle dc, unsigned paneIndex) { // draw bar to top/left of pane RECT Rect; if (SplitBarRectGet (paneIndex, &Rect)) // fails on last pane { dc.FillRect (&Rect, COLOR_3DFACE); T * pT = static_cast (this);// draw 3D edge if needed if ((pT->GetExStyle () & WS_EX_CLIENTEDGE) != 0) dc.DrawEdge (&Rect, EDGE_RAISED, IsVertical ()? (BF_LEFT | BF_RIGHT) : (BF_TOP | BF_BOTTOM)); } } void GhostBarDraw (void) { CRect BarRect; if (SplitBarRectGet (static_cast(resizingPane), &BarRect)) { // invert the brush pattern (looks just like frame window sizing) T * pT = static_cast (this); CWindowDC dc (pT->m_hWnd); CBrush GhostBrush = CDCHandle::GetHalftoneBrush (); if (!GhostBrush.IsNull ()) { CBrushHandle OldBrush = dc.SelectBrush (GhostBrush); dc.PatBlt (BarRect.left, BarRect.top, BarRect.Width (), BarRect.Height (), PATINVERT); dc.SelectBrush (OldBrush); } } } // Message map and handlers typedef CMultiSplitImpl< T> thisClass; BEGIN_MSG_MAP (thisClass) MESSAGE_HANDLER (WM_CREATE, OnCreate) MESSAGE_HANDLER (WM_PAINT, OnPaint) MESSAGE_HANDLER (WM_PRINTCLIENT, OnPaint) if (IsInteractive ()) { MESSAGE_HANDLER (WM_SETCURSOR, OnSetCursor) // cursor in client area MESSAGE_HANDLER (WM_MOUSEMOVE, OnMouseMove) MESSAGE_HANDLER (WM_LBUTTONDOWN, OnLButtonDown) MESSAGE_HANDLER (WM_LBUTTONUP, OnLButtonUp) MESSAGE_HANDLER (WM_VSCROLL, OnScroll) // scroll bar messages MESSAGE_HANDLER (WM_HSCROLL, OnScroll) MESSAGE_HANDLER (WM_MOUSEWHEEL, OnMouseWheel) MESSAGE_HANDLER (WM_LBUTTONDBLCLK, OnLButtonDoubleClick) } MESSAGE_HANDLER (WM_SETFOCUS, OnSetFocus) MESSAGE_HANDLER (WM_MOUSEACTIVATE, OnMouseActivate) MESSAGE_HANDLER (WM_SETTINGCHANGE, OnSettingChange) END_MSG_MAP () LRESULT OnCreate (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL & bHandled) { SystemSettingsGet (false); CRect InitRect (0, 0, ((CREATESTRUCT *) lParam)->cx, ((CREATESTRUCT *) lParam)->cy); RectSet (&InitRect, false); if (IsScrolling ()) { UpdateScrollbar (); T * pT = static_cast (this); pT->ShowScrollBar (scrollBar); } bHandled = FALSE; return 1; } LRESULT OnPaint (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL & /*bHandled*/) { T * pT = static_cast (this); CPaintDC dc (pT->m_hWnd); pT->Draw (dc.m_hDC); return 0; } LRESULT OnSetCursor (UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { T * pT = static_cast (this); if (reinterpret_cast(wParam) == pT->m_hWnd && LOWORD (lParam) == HTCLIENT) { DWORD Position = ::GetMessagePos (); POINT Point = { GET_X_LPARAM (Position), GET_Y_LPARAM (Position) }; pT->ScreenToClient (&Point); if (IsOverSplitBar (Point) != -1) return 1; } bHandled = FALSE; return 0; } // Dragging splitbar resizes panes to either side of bar // Pane size is checked and changed on every receipt of this message. LRESULT OnMouseMove (UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { T * pT = static_cast (this); POINT Point = { GET_X_LPARAM (lParam), GET_Y_LPARAM (lParam) }; if ((wParam & MK_LBUTTON) && ::GetCapture () == pT->m_hWnd) { // resizing int NewSize = (IsVertical ()? Point.x : Point.y) + scrollPosition - panes [resizingPane].position; if (NewSize <= 0) // avoid -1, that means equal NewSize = 1; if (panes [resizingPane].size != NewSize) { if (drawContentWhileResizing) { if (PaneSizeSet (resizingPane, NewSize, true)) pT->UpdateWindow (); } else { GhostBarDraw (); PaneSizeSet (resizingPane, NewSize, false); GhostBarDraw (); } } } else { // not dragging, just set cursor if (IsOverSplitBar (Point) != -1) ::SetCursor (IsVertical ()? vertCursor : horzCursor); bHandled = FALSE; } return 0; } // start splitbar drag LRESULT OnLButtonDown (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL & bHandled) { POINT Point = { GET_X_LPARAM (lParam), GET_Y_LPARAM (lParam) }; resizingPane = IsOverSplitBar (Point); if (resizingPane >= 0) { T * pT = static_cast (this); pT->SetCapture (); ::SetCursor (IsVertical ()? vertCursor : horzCursor); if (!drawContentWhileResizing) GhostBarDraw (); } bHandled = FALSE; return 1; } // end splitbar drag LRESULT OnLButtonUp (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL & bHandled) { if (resizingPane >= 0 && !drawContentWhileResizing) { GhostBarDraw (); T * pT = static_cast (this); UpdateLayout (); UpdateScrollbar (); pT->UpdateWindow (); } resizingPane = -1; ::ReleaseCapture (); bHandled = FALSE; return 1; } LRESULT OnLButtonDoubleClick (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL & /*bHandled*/) { if (PanesCount () < 2) return 0; POINT Point = { GET_X_LPARAM (lParam), GET_Y_LPARAM (lParam) }; int PaneIndex = IsOverSplitBar (Point); if (PaneIndex >= 0 && EvenPanes (static_cast(PaneIndex))) static_cast (this)->UpdateWindow (); return 0; } LRESULT OnScroll (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { int NewPos; switch (LOWORD (wParam)) { case SB_LINEDOWN : NewPos = scrollPosition + 5; break; case SB_LINEUP : NewPos = scrollPosition - 5; break; case SB_THUMBPOSITION : case SB_THUMBTRACK : NewPos = HIWORD (wParam); break; case SB_PAGEDOWN : NewPos = scrollPosition + 100; break; case SB_PAGEUP : NewPos = scrollPosition - 100; break; case SB_TOP : NewPos = 0; break; case SB_BOTTOM : NewPos = scrollMax; break; default : NewPos = scrollPosition; } if (NewPos < 0) NewPos = 0; else if (NewPos > scrollMax) NewPos = scrollMax; T * pT = static_cast (this); if (NewPos == pT->GetScrollPos (scrollBar)) return 0; scrollPosition = NewPos; pT->SetScrollPos (scrollBar, scrollPosition); UpdateLayout (); pT->UpdateWindow (); // looks better with this return 0; } LRESULT OnMouseWheel (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { // ATLTRACE(_T("CMultiSplitImpl::OnMouseWheel\n")); int WheelDelta = GET_WHEEL_DELTA_WPARAM(wParam);// +=up, -=down int ScrollCode = (mouseWheelLines == WHEEL_PAGESCROLL)? ((WheelDelta > 0)? SB_PAGEUP : SB_PAGEDOWN) : ((WheelDelta > 0)? SB_LINEUP : SB_LINEDOWN); OnScroll (WM_VSCROLL, ScrollCode, 0, bHandled); return 0; // indicate handled so message not passed to parent } LRESULT OnSetFocus (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM, BOOL & bHandled) { // give focus to defaultPane child // ATLTRACE(_T("CMultiSplitImpl::OnSetFocus: %d\n"), defaultFocusPane); if (static_cast(defaultFocusPane) < PanesCount () && panes [defaultFocusPane].window) ::SetFocus (panes [defaultFocusPane].window); bHandled = FALSE; return 1; } LRESULT OnMouseActivate (UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & /*bHandled*/) { T * pT = static_cast (this); LRESULT Result = pT->DefWindowProc (uMsg, wParam, lParam); /* LPCTSTR Text; switch (Result) { case MA_ACTIVATE : Text = _T("MA_ACTIVATE"); break; case MA_ACTIVATEANDEAT : Text = _T("MA_ACTIVATEANDEAT"); break; case MA_NOACTIVATE : Text = _T("MA_NOACTIVATE"); break; case MA_NOACTIVATEANDEAT : Text = _T("MA_NOACTIVATEANDEAT"); break; } ATLTRACE(_T("CMultiSplitImpl::OnMouseActivate: %s\n"), Text); */ if (Result == MA_ACTIVATE || Result == MA_ACTIVATEANDEAT) { // select focus pane from mouse position DWORD Position = ::GetMessagePos (); POINT Point = { GET_X_LPARAM (Position), GET_Y_LPARAM (Position) }; pT->ScreenToClient (&Point); CRect PaneRect; size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) { if (PaneRectGet (PaneIndex, &PaneRect) && PaneRect.PtInRect (Point)) { defaultFocusPane = PaneIndex; break; } } pT->SetFocus (); // focus child window } return Result; } LRESULT OnSettingChange (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL & /*bHandled*/) { SystemSettingsGet (true); return 0; } // Implementation - internal helpers // Invalidate splitter bar areas and empty spaces, set child window pane positions & sizes. // Calculate contentSize. // Called whenever SystemSettings changed, RectSet, LButtonUp, child windows changed void UpdateLayout (void) { T * pT = static_cast (this); CRect PaneRect; contentSize = 0; size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) { if (PaneRectGet (PaneIndex, &PaneRect)) { if (panes [PaneIndex].window != 0) ::SetWindowPos (panes [PaneIndex].window, 0, PaneRect.left, PaneRect.top, PaneRect.Width (), PaneRect.Height (), SWP_NOZORDER | SWP_NOACTIVATE); else pT->InvalidateRect (&PaneRect); contentSize += IsVertical ()? PaneRect.Width () : PaneRect.Height (); } if (SplitBarRectGet (PaneIndex, &PaneRect)) { pT->InvalidateRect (&PaneRect); contentSize += IsVertical ()? PaneRect.Width () : PaneRect.Height (); } } FillRectSet (); pT->InvalidateRect (&fillRect); contentSize += (IsVertical ()? fillRect.Width () : fillRect.Height ()); } // set scroll bar properties void UpdateScrollbar (void) { scrollMax = max (contentSize - visibleSpace, 0); scrollPosition = min (scrollPosition, scrollMax); if (scrollBar >= 0) { // ATLTRACE(_T("CMultiSplitImpl::UpdateScrollbar: space %d size %d range %d pos %d\n"), // visibleSpace, contentSize, scrollMax, scrollPosition); SCROLLINFO ScrollInfo; ScrollInfo.cbSize = sizeof (ScrollInfo); ScrollInfo.fMask = SIF_RANGE | SIF_PAGE | SIF_POS; ScrollInfo.nMin = 0; ScrollInfo.nMax = contentSize; ScrollInfo.nPage = visibleSpace; ScrollInfo.nPos = scrollPosition; T * pT = static_cast (this); pT->SetScrollInfo (scrollBar, &ScrollInfo, TRUE); } } // pane's drawing area // Normally all panes return their stored width, but the last pane in limited mode // is to be drawn extending to the window edge regardless of its stored width, so // the window edge is returned as the right edge for that case. The width is // retained in the pane info, though, so that when switched to scroll mode it draws // properly, or when another pane is appended, the new pane is positioned properly. bool PaneRectGet (unsigned paneIndex, LPRECT paneRect) const { ATLASSERT (paneRect != 0); ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return false; // last pane in limited mode (except if there's only one) always fills to edge bool LastPane = IsLimited () && PanesCount () > 1 && paneIndex == (PanesCount () - 1); if (IsVertical ()) { paneRect->left = panes [paneIndex].position - scrollPosition; paneRect->top = visibleRect.top; paneRect->right = LastPane? visibleRect.right : paneRect->left + panes [paneIndex].size; paneRect->bottom = visibleRect.bottom; } else { paneRect->left = visibleRect.left; paneRect->top = panes [paneIndex].position - scrollPosition; paneRect->right = visibleRect.right;; paneRect->bottom = LastPane? visibleRect.bottom : paneRect->top + panes [paneIndex].size; } // ATLTRACE(_T("CMultiSplitImpl::PaneRectGet %d: %d %d %d %d (%d %d)\n"), // paneIndex, paneRect->left, paneRect->top, paneRect->right, paneRect->bottom, // paneRect->right - paneRect->left, paneRect->bottom - paneRect->top); return true; } // pane's split bar drawing area // Normally all panes have a split bar to their right, but the last pane in limited mode // extends to the window edge, so false is returned for that case. bool SplitBarRectGet (unsigned paneIndex, LPRECT barRect) const { // splitter bar preceding specified pane ATLASSERT(barRect != 0); ATLASSERT(paneIndex < PanesCount ()); if (paneIndex >= PanesCount ()) return false; // invalid index, or array empty if (IsLimited () && PanesCount () > 1 && paneIndex == PanesCount () - 1) { // ATLTRACE(_T("CMultiSplitImpl::SplitBarRectGet %d: no splitter for last pane in limited mode\n"), paneIndex); return false; // no splitter for last pane in limited mode } if (IsVertical ()) { // IsVertical () bars barRect->left = panes [paneIndex].position + panes [paneIndex].size - scrollPosition; barRect->top = visibleRect.top; barRect->right = barRect->left + barWidth + edgeWidth; barRect->bottom = visibleRect.bottom; } else { // horizontal bars barRect->left = visibleRect.left; barRect->top = panes [paneIndex].position + panes [paneIndex].size - scrollPosition; barRect->right = visibleRect.right;; barRect->bottom = barRect->top + barWidth + edgeWidth; } // ATLTRACE(_T("CMultiSplitImpl::SplitBarRectGet %d: %d %d %d %d (%d %d)\n"), // paneIndex, barRect->left, barRect->top, barRect->right, barRect->bottom, // barRect->right - barRect->left, barRect->bottom - barRect->top); return true; } // returns paneIndex of pane to left of splitbar under point // -1 if none int IsOverSplitBar (POINT point) const { if (PanesCount () == 0) return -1; CRect BarRect; unsigned PaneIndex = 0; while (PaneIndex < PanesCount () && SplitBarRectGet (PaneIndex, &BarRect)) { if (BarRect.PtInRect (point)) return PaneIndex; PaneIndex++; } return -1; } // get the bar & edge widths void SystemSettingsGet (bool update) { #ifndef SPI_GETWHEELSCROLLLINES const UINT SPI_GETWHEELSCROLLLINES = 104; #endif //!SPI_GETWHEELSCROLLLINES ::SystemParametersInfo (SPI_GETWHEELSCROLLLINES, 0, &mouseWheelLines, 0); barWidth = ::GetSystemMetrics (IsVertical () ? SM_CXSIZEFRAME : SM_CYSIZEFRAME); int EdgeWidth = ::GetSystemMetrics (IsVertical () ? SM_CXEDGE : SM_CYEDGE) * 2; T * pT = static_cast (this); if ((pT->GetExStyle () & WS_EX_CLIENTEDGE)) { edgeWidth = EdgeWidth; minPaneSize = 0; } else { edgeWidth = 0; minPaneSize = EdgeWidth; } ::SystemParametersInfo (SPI_GETDRAGFULLWINDOWS, 0, &drawContentWhileResizing, 0); if (update && pT->IsWindow ()) { UpdateLayout (); UpdateScrollbar (); pT->UpdateWindow (); } } // check for value too big, set pane size & position & other affected panes too bool PaneSizeSetScroll (unsigned paneIndex, int newSize) { bool Changed = PaneSizeStore (paneIndex, newSize); PanePositionsSet (paneIndex); // move all following panes return Changed; } bool PaneSizeSetLimited (unsigned paneIndex, int newSize) { int UsedPane = -1; int RemainingSize; if (visibleSpace) // no upper limit if visibleSpace unknown newSize = LimitedSizeGet (paneIndex, panes [paneIndex].size, newSize, UsedPane, RemainingSize); bool Changed = PaneSizeStore (paneIndex, newSize); if (UsedPane >= 0) { // neighbor pane's size changed PaneSizeStore (static_cast(UsedPane), RemainingSize); PanePositionsSet (static_cast(UsedPane)); // move all following panes } return Changed; } // return even spacing value: if newSize is -1 or too big to fit return valid size int EvenPaneSizeScroll (int newSize) { if (newSize >= 0) // no limit return newSize; // -1 = all the same if (PanesCount () == 1 && visibleSpace) return visibleSpace - barWidth - edgeWidth; // always have a splitter bar visible at the end of the list int KnownSize = 0; size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) { if (panes [PaneIndex].size > 0) KnownSize += panes [PaneIndex].size + barWidth + edgeWidth; } if (KnownSize) // average of windows with sizes set return KnownSize / PanesCount () - barWidth - edgeWidth; // all -1!, use visibleSpace return EvenPaneSizeLimited (newSize); } int EvenPaneSizeLimited (int newSize) { // when all panes are in list, calculate average size if (visibleSpace == 0) return newSize; // size does not include separation bars int MaxSize = (visibleSpace - (barWidth + edgeWidth) * (PanesCount () - 1)) / PanesCount (); if (newSize < 0 || newSize > MaxSize) // -1 = all the same newSize = MaxSize; // limit specified size to maximum return newSize; } int LimitedSizeGet (unsigned paneIndex, int oldSize, int newSize, int & usedPaneIndex, int & remainingSize) { int LastPaneSize = visibleSpace; if (PanesCount ()) LastPaneSize -= panes [PanesCount () - 1].position; int MaxSpace; if (PanesCount () == 1) { usedPaneIndex = -1; // only pane gets all Rect MaxSpace = LastPaneSize; } else if (paneIndex >= PanesCount ()) { usedPaneIndex = PanesCount () - 1; // appended new pane can use all of last pane MaxSpace = LastPaneSize; } else if (paneIndex == PanesCount () - 1) { usedPaneIndex = PanesCount () - 2; // last can use all area of 2nd last pane MaxSpace = oldSize + panes [usedPaneIndex].size; } else if (paneIndex + 1 == PanesCount () - 1) { usedPaneIndex = PanesCount () - 1; // 2nd last can use all area of last pane MaxSpace = oldSize + LastPaneSize; } else { usedPaneIndex = paneIndex + 1; // 1st or mid panes can only expand into next MaxSpace = oldSize + panes [usedPaneIndex].size; } if (newSize > MaxSpace) { // used all of neighbor's space! remainingSize = minPaneSize + edgeWidth; // ATLTRACE(_T("CMultiSplitImpl::LimitedSizeGet %u: %d > max %d. Pane %d remains with %d\n"), paneIndex, newSize, MaxSpace, usedPaneIndex, remainingSize); newSize = MaxSpace - minPaneSize - edgeWidth; } else { remainingSize = MaxSpace - newSize; // ATLTRACE(_T("CMultiSplitImpl::LimitedSizeGet %u: %d allowed (max %d). Pane %d remains with %d\n"), paneIndex, newSize, MaxSpace, usedPaneIndex, remainingSize); } return newSize; } // squeeze new item into limited space // neighbor item may have to reduce size to accomodate new // if that's not enough space then new size has to reduce too // paneIndex is what the new item's index should be after it's been added to list bool PaneInsertLimited (unsigned paneIndex, HWND hWnd, int newSize, bool update) { ATLASSERT(paneIndex <= PanesCount ()); if (paneIndex > PanesCount ()) // ==count is new additional item return false; if (newSize < -1) // convert proportional to real units newSize = ::MulDiv (-newSize, visibleSpace, proportionalMax); if (newSize < 0) { // all must change panes.InsertAt (paneIndex, CMultiSplitPane (hWnd)); // must be done after adding to list newSize = EvenPaneSizeLimited (-1); if (newSize < minPaneSize + edgeWidth) newSize = minPaneSize + edgeWidth; AllPaneSizesSet (newSize); } else { int Size; // for small int UsedPane = -1; int RemainingSize = 0; if (newSize < minPaneSize + edgeWidth) Size = minPaneSize + edgeWidth; else Size = newSize; // no upper limit if visibleSpace unknown if (visibleSpace) // yes this is a mess (use i-1 for inserts, i=GetCount for adds) Size = LimitedSizeGet ((paneIndex >= PanesCount ())? paneIndex : paneIndex - 1, 0, Size, UsedPane, RemainingSize); panes.InsertAt (paneIndex, CMultiSplitPane (hWnd)); // set new width and position PaneSizeStore (paneIndex, Size); if (UsedPane >= 0) { // a neighbor's size changed if (static_cast(UsedPane) >= paneIndex) UsedPane++; // kludge for inserted panes // ATLTRACE(_T("CMultiSplitImpl::PaneInsertLimited %u: %d resized to %d\n"), paneIndex, UsedPane, RemainingSize); PaneSizeStore (static_cast(UsedPane), RemainingSize); // adjust positions starting at previous changed neighbor or new item PanePositionsSet ((static_cast(UsedPane) < paneIndex)? static_cast(UsedPane) : paneIndex); } else // no neighbors changed, set positions for new item and followers PanePositionsSet (paneIndex); } T * pT = static_cast (this); if (update && pT->IsWindow ()) // update if requested { UpdateLayout (); UpdateScrollbar (); } return true; } // store pane's actual and proportionate size bool PaneSizeStore (unsigned paneIndex, int newSize) { bool Changed = panes [paneIndex].size != newSize; panes [paneIndex].size = newSize; panes [paneIndex].proporSize = (visibleSpace <= 0)? 0 : ::MulDiv (newSize, proportionalMax, visibleSpace); // ATLTRACE(_T("CMultiSplitImpl::PaneSizeStore %u: %d (%d)\n"), paneIndex, newSize, panes [paneIndex].proporSize); return Changed; } // set all panes to the same size & adjust positions void AllPaneSizesSet (int newSize) { size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) { PaneSizeStore (PaneIndex, newSize); PanePositionSet (PaneIndex); } } // proportionately size panes from scrolling space into limited space void LimitedUpdate (void) { int TotalSize = contentSize - (IsVertical ()? fillRect.Width () : fillRect.Height ()); if (TotalSize > visibleSpace) { size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) PaneSizeStore (PaneIndex, ::MulDiv (panes [PaneIndex].size, visibleSpace, TotalSize)); PanePositionsSet (); // bump all positions to match sizes } } // proportionately size panes between visible spaces // called when visibleRect has changed void ProportionalUpdate (void) { if (visibleSpace > 0) { size_t PaneIndex; for (PaneIndex = 0; PaneIndex < PanesCount (); PaneIndex++) panes [PaneIndex].size = ::MulDiv (panes [PaneIndex].proporSize, visibleSpace, proportionalMax); PanePositionsSet (); // bump all positions to match sizes } } // set one pane's position // default position (-1) positions pane next to its predecessor void PanePositionSet (unsigned paneIndex, int newPosition = -1) { if (paneIndex == 0) panes [0].position = 0; else if (newPosition < 0) { int PrevSize = (panes [paneIndex - 1].size < 0)? 0 : panes [paneIndex - 1].size; panes [paneIndex].position = panes [paneIndex - 1].position + PrevSize + barWidth + edgeWidth; } else panes [paneIndex].position = newPosition; // ATLTRACE(_T("CMultiSplitImpl::PanePositionSet %d: %d = %d\n"), paneIndex, newPosition, panes [paneIndex].position); } // set positions of all panes, starting at index, based on predecessor's position & size // positions panes appropriately after sizes change void PanePositionsSet (unsigned paneIndex = 0) { while (paneIndex < PanesCount ()) PanePositionSet (paneIndex++); } // sets fillRect to clear entire area if no panes // if limited mode and one pane, it fills other pane // if limited mode and more panes, rect is empty // if scrolling mode it fills beyond last pane to visibleRect or square void FillRectSet (void) { if (PanesCount () == 0) { fillRect = visibleRect; return; } int LastPaneIndex = PanesCount () - 1; if (IsLimited () && LastPaneIndex > 0) { fillRect.SetRectEmpty (); // ATLTRACE(_T("CMultiSplitImpl::FillRectSet: (%d %d) limited, panes > 1\n"), fillRect.Width (), fillRect.Height); return; } CMultiSplitPane * LastPane = &panes [LastPaneIndex]; int Position = LastPane->position + LastPane->size + barWidth + edgeWidth - scrollPosition; if (IsVertical ()) { fillRect.left = Position; fillRect.top = 0; } else { // horizontal bars fillRect.left = 0; fillRect.top = Position; } if (IsLimited ()) { // only fill unoccupied area fillRect.right = visibleRect.right; fillRect.bottom = visibleRect.bottom; } else // fill a square or unoccupied { // area, whichever is larger int Size = IsVertical ()? visibleRect.Height () : visibleRect.Width (); fillRect.right = fillRect.left + Size; fillRect.bottom = fillRect.top + Size; if (IsVertical ()) { if (fillRect.right < visibleRect.right) fillRect.right = visibleRect.right; } else { // horizontal bars if (fillRect.bottom < visibleRect.bottom) fillRect.bottom = visibleRect.bottom; } } // ATLTRACE(_T("CMultiSplitImpl::FillRectSet: %d %d %d %d (%d %d)\n"), // fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, // fillRect.Width (), fillRect.Height ()); } }; template HCURSOR CMultiSplitImpl::vertCursor = 0; template HCURSOR CMultiSplitImpl::horzCursor = 0; ///////////////////////////////////////////////////////////////////////////// // CMultiSplitWindowImpl - Implements a splitter window template class ATL_NO_VTABLE CMultiSplitWindowImpl : public CWindowImpl< T, TBase, TWinTraits >, public CMultiSplitImpl< T > { public : DECLARE_WND_CLASS_EX(0, CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, COLOR_WINDOW) typedef CMultiSplitWindowImpl< T , TBase, TWinTraits > thisClass; typedef CMultiSplitImpl< T > baseClass; BEGIN_MSG_MAP (thisClass) MESSAGE_HANDLER (WM_ERASEBKGND, OnEraseBackground) MESSAGE_HANDLER (WM_SIZE, OnSize) CHAIN_MSG_MAP (baseClass) FORWARD_NOTIFICATIONS () END_MSG_MAP () LRESULT OnEraseBackground (UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL & /*bHandled*/) { return 1; // handled, no background painting needed } LRESULT OnSize (UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL & bHandled) { CRect Rect (0, 0, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); RectSet (&Rect); return 0; // handled } }; ///////////////////////////////////////////////////////////////////////////// // CMultiSplitWindow - Implements a splitter window to be used as is class CMultiSplitWindow : public CMultiSplitWindowImpl { public : DECLARE_WND_CLASS_EX (_T ("MultiSplitWindow"), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, COLOR_WINDOW) }; }; //namespace WTL #endif // __ATLMULTISPLIT_H__