Snappable tool windows that auto-hide
Snappable tool windows that auto-hide
See How the demo was built further down in this article.
Introduction
I'd like to share my first attempt at adding tool windows like the ones you can find in applications like MS Visio. So here is the implementation of a snappable tool window (snaps to the sides of a view window) that also provides auto-hide and pinning features, see the picture above.
Thanks to Bjarke Viksoe for his docking framework. Although this code is written from scratch, his framework got me started. See atldock.h.
Reference
CSnappingWindow
is the main window used for managing the snappable views. It is responsible for updating positions when the frame is resized etc. and provides the API for adding and positioning snappable views.
CSnappingWindow
members:
<A href="#SetClient">SetClient</A>
Sets the client window whose edge to snap to. <A href="#AddSnappableWindow">AddSnappableWindow</A>
Adds a view to be snappable. <A href="#FloatWindow">FloatWindow</A>
Floats the view at a specified position. <A href="#SnapWindow">SnapWindow</A>
Snaps the view to a specified edge. <A href="#HideWindow">HideWindow</A>
Hides the view independently if it is snapped or floating.
CSnappingWindow
member details:
void <A name=SetClient>SetClient</A>(HWND hWndClient)
HWND hWndClient
is the client view whose edge the views are snapping to<A href="#SNAPCONTEXT">SNAPCONTEXT</A>* <A name=AddSnappableWindow>AddSnappableWindow</A>(HWND hWndView)
HWND hWndView
is the view to be snappable (any window)<A href="#SNAPCONTEXT">SNAPCONTEXT</A>*
is a pointer to the snapping context structure, will beNULL
if not successfulRemark: The view is initially in hidden state. void <A name=FloatWindow>FloatWindow</A>(HWND hWndView, const POINT& ptPos, DWORD dwFlags)
HWND hWndView
is a view that has been added to the framework withAddSnappableWindow
const POINT& ptPos
top left coordinates where the window will be placedDWORD dwFlags
additional flags, defaults to zeroRemark: The view needs to be in a hidden state before this method is called. void <A name=SnapWindow>SnapWindow</A>(HWND hWndView, <A href="#SnapPosition">SnapPosition</A> spPos, int cxy, <BR>DWORD dwFlags)
HWND hWndView
is a view that has been added to the framework withAddSnappableWindow
<A href="#SnapPosition">SnapPosition</A> spPos
snapped positionint cxy
offset from the top or left edge depending onspPos
, defaults to zeroDWORD dwFlags
additional flags, defaults tosnapMinibar
Remark: The view needs to be in a hidden state before this method is called. void <A name=HideWindow>HideWindow</A>(HWND hWndView)
HWND hWndView
is a view that has been added to the framework withAddSnappableWindow
<A name=SnapPosition>SnapPosition</A>
enumeration enforces the allowed combination of position flags.
snapFloat
0x00000000 snapLeft
0x00000001 snapTop
0x00000002 snapRight
0x00000004 snapBottom
0x00000008 snapTopLeft
0x00000003 snapTopRight
0x00000006 snapBottomLeft
0x00000009 snapBottomRight
0x0000000C snapHidden
0x00000010
Additional flags and masks to manage the dwFlags
attribute:
State flags snapPinned
0x00000100 snapMinibar
0x00000200 Masks snapPosition
0x000000FF snapState
0x00FFFF00 snapReserved
0xFF000000 The flags
snapPinned
andsnapMinibar
have no effect when floating.
The structure <A name=SNAPCONTEXT>SNAPCONTEXT</A>
is the context of the snappable window.
HWND hWndSnapped
Snapped window handle HWND hWndFloated
Floating window handle HWND hWndView
View window handle HWND hWndView
Snap window manager DWORD dwFlags
Position and state flags Knowledge about this structure is not important when using this framework, but for extending it.
Implementation details
The following custom Windows messages are defined for the framework:
#ifndef SNAP_MSGBASE
#define SNAP_MSGBASE WM_USER+860
#endif
#define WM_SNAP_FLOAT SNAP_MSGBASE
#define WM_SNAP_SNAP SNAP_MSGBASE + 1
#define WM_SNAP_HIDE SNAP_MSGBASE + 2
#define WM_SNAP_QUERYRECT SNAP_MSGBASE + 3
#define WM_SNAP_MOVEDONE SNAP_MSGBASE + 4
#define WM_SNAP_REPOSITION SNAP_MSGBASE + 5
#define WM_SNAP_UPDATELAYOUT SNAP_MSGBASE + 6
#define WM_SNAP_QUERYSIZE SNAP_MSGBASE + 7
One of the harder issues to solve was the dragging of windows between snapping positions and between snapped and floating state. After many failing attempts, I now manage the dragging of the windows myself. In doing so, I store the starting point of the cursor and the offset to a reference point on the window. The trick was to move the reference point and update the offset depending on which side the window is snapped to.
E.g., when snapped to the bottom right corner, the lower right corner of the window is the reference point. When dragging the window to a floating position, it is important that the lower right corner stays in the same position independent of window state. If the window now is dragged and snapped to the upper left position, it is equally important to keep the upper left position of the window in the same position independent of state.
I decided to keep the size of the internal window constant, the view that is. I thought that would be helpful in case the view is based on a dialog. Well, this decision didn't make moving any easier, since the outer size of the window now is changing depending on the snapping context or if the window is floating. Hence the elaborate reference point vs. offset for managing the dragging.
The classes CSnapWindowInfo
, CSnapTrackInfo
are used to manage state while tracking the mouse move events while in window drag mode. The template class CSnapWindowMover
is used in the floating and snapping window implementations and contains the logic for moving the window and shifting its state depending on location.
The template class CSnapFloatingWindowImpl
is implementing all events and logic associated with the floating window and is derived from CSnapWindowMover
.
// CSnapFloatingWindow and CSnapFloatingWindowImpl
//
typedef CWinTraits<WS_POPUPWINDOW|WS_CLIPSIBLINGS|
WS_OVERLAPPED|WS_THICKFRAME|WS_DLGFRAME,
WS_EX_TOOLWINDOW|WS_EX_WINDOWEDGE> CSnapFloatWinTraits;
template<class T, class TBase=CWindow, class TWinTraits=CSnapFloatWinTraits>
class ATL_NO_VTABLE CSnapFloatingWindowImpl :
public CWindowImpl< T, TBase, TWinTraits >,
public CSnapWindowMover<T>
class CSnapFloatingWindow : public CSnapFloatingWindowImpl<CSnapFloatingWindow>
The template class CSnapAutoHideWindowImpl
is implementing all events and logic associated with the snapping window. This window uses a timer to track if the mouse is outside an extended boundary, if so, it will "auto-hide" to the minibar state. The timer is only created for an expanded window that is not pinned and will be destroyed when going back to minibar state. CSnapAutoHideWindowImpl
is derived from CSnapWindowMover
.
// CSnapAutoHideWindow and CSnapAutoHideWindowImpl
//
typedef CWinTraits<WS_CHILD|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_THICKFRAME,
WS_EX_WINDOWEDGE> CSnapAutoHideWinTraits;
template<class T, class TBase=CWindow, class TWinTraits=CSnapAutoHideWinTraits>
class ATL_NO_VTABLE CSnapAutoHideWindowImpl :
public CWindowImpl< T, TBase, TWinTraits >,
public CSnapWindowMover<T>
// Timer id and interval used for auto hide
enum { IDT_AUTOHIDE = 1234, IDT_INTERVAL = 500 };
class CSnapAutoHideWindow : public CSnapAutoHideWindowImpl<CSnapAutoHideWindow>
The template class CSnappingWindowImpl
is implementing all events and logic associated with the snapping window manager. As you can see in the code snippet below, snapped and floating window implementations can be replaced with your own extensions.
// CSnappingWindow and CSnappingWindowImpl
//
template<class T,
class TSnappedWindow = CSnapAutoHideWindow,
class TFloatingWindow = CSnapFloatingWindow,
class TBase = CWindow,
class TWinTraits = CControlWinTraits<
class ATL_NO_VTABLE CSnappingWindowImpl :
public CWindowImpl<T, TBase, TWinTraits>
class CSnappingWindow : public CSnappingWindowImpl<CSnappingWindow<
The floating and snapping window classes are created when a view is added to CSnappingWindow
instance using the AddSnappableWindow
member function.
SNAPCONTEXT* AddSnappableWindow(HWND hWndView)
{
ATLASSERT( ::IsWindow(hWndView) );
if (!::IsWindow(hWndView))
return NULL;
// Initialize context
SNAPCONTEXT* pCtx = new SNAPCONTEXT;
::ZeroMemory(pCtx, sizeof(SNAPCONTEXT));
pCtx->hWndView = hWndView;
pCtx->hWndRoot = m_hWnd;
pCtx->dwFlags = snapHidden; // Is in hidden state
// Create snapping window
TSnappedWindow* wndSnapped = new TSnappedWindow(pCtx);
ATLASSERT(wndSnapped);
wndSnapped->Create(m_hWnd, rcDefault, NULL);
ATLASSERT(::IsWindow(wndSnapped->m_hWnd));
pCtx->hWndSnapped = *wndSnapped;
// Create floating window
TFloatingWindow* wndFloating = new TFloatingWindow(pCtx);
ATLASSERT(wndFloating);
TCHAR szCaption[128]; // max text length is 127 for floating caption
::GetWindowText(hWndView, szCaption, sizeof(szCaption)/sizeof(TCHAR));
wndFloating->Create(m_hWnd, rcDefault, szCaption);
ATLASSERT(::IsWindow(wndFloating->m_hWnd));
pCtx->hWndFloated = *wndFloating;
// Store context pointer in the container
// (used for lookup and for memory mgmnt)
m_snappableWindows.Add(pCtx);
return pCtx;
}
The default layout is calculated from the client window rectangle and taking scrollbars into consideration. See the image at the top of the article.
void QueryRect(RECT& rect)
{
// Typically override this method to calculate your client layout
T* pT = static_cast<T*>(this);
HWND hWndClient = pT->GetClient();
::GetWindowRect(hWndClient ,&rect);
LONG style = ::GetWindowLong(hWndClient,GWL_STYLE);
if (style & WS_VSCROLL)
{
rect.right -= ::GetSystemMetrics(SM_CXVSCROLL);
}
if (style & WS_HSCROLL)
{
rect.bottom -= ::GetSystemMetrics(SM_CYHSCROLL);
}
// Compensate for 3D edge of client window
LONG styleEx = ::GetWindowLong(hWndClient,GWL_EXSTYLE);
if (styleEx & WS_EX_CLIENTEDGE)
::InflateRect(&rect, -2, -2);
}
To do
In no particular order:
- Persistence class to help store and retrieve positions between sessions.
- Synchronize active state between tool windows and frame window. This is built into MFC but not in WTL.
- Probably as part of item above, hide floating tool windows when parent frame is no longer active. It gets crowdie on screen when using floating tool windows and several instances of the application running.
How the demo was built
Running the WTL application wizard to generate a SDI application with an Edit view created the demo application. Following that, I added the wtlsnappable header file and a view window to be snapped. CSnapView
, which is presented below, is just a dummy view window with a green background to show the features of the snapping framework. Notice that no extra code is needed here for the snapping framework.
//
//A dummy view
//
class CSnapView : public CWindowImpl<CSnapView>
{
public: DECLARE_WND_CLASS(NULL)
BOOLPreTranslateMessage(MSG* pMsg)
{
pMsg;
return FALSE;
}
BEGIN_MSG_MAP(CSnapView)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
END_MSG_MAP()
LRESULT OnEraseBkgnd(UINT, WPARAM wParam, LPARAM, BOOL&)
{
HDC dc = (HDC)wParam;
HBRUSH hBrush = ::CreateSolidBrush(RGB(0,128,0));
RECT rc;
GetClientRect(&rc);
::FillRect(dc,&rc, hBrush);
::DeleteObject(hBrush);
return 1;
}
};
CSnappingWindow
was added as a member to the CMainFrame
along with a few CSnapView
members. The views are added to the snapping window the in the WM_CREATE
message handler, see the code snippet below:
//
// MainFrm.h
//
// Include the snappable framework header file
#include "wtlsnappable.h"
class CMainFrame : public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter, public CIdleHandler
{
public:
// Window for the snappable window framework
CSnappingWindow m_snapWindow;
// Snappable views (normal WTL views)
CSnapView m_view1,m_view2,m_view3,m_view4,m_view5;
// ... WTL Wizard code omitted
LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
// Initiate snapping framework
m_hWndClient = m_snapWindow.Create(m_hWnd, rcDefault, NULL,
WS_CHILD|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_VISIBLE);
// ... Menu and toolbar setup omitted (see demo project)
HWND hWndView = m_view.Create(m_hWndClient , rcDefault, NULL,
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |
WS_CLIPCHILDREN | WS_HSCROLL | WS_VSCROLL |
ES_AUTOHSCROLL | ES_AUTOVSCROLL |
ES_MULTILINE | ES_NOHIDESEL, WS_EX_CLIENTEDGE);
m_snapWindow.SetClient(hWndView);
// ... More wizard code omitted
// Create and add the five views
// Snapping window to a side will auto-hide them by default
RECT rcView1 = {0,0,200,300};
RECT rcView2 = {0,0,200,200};
POINT ptFloat = {100,100};
m_view1.Create(m_hWnd, rcView1, _T("View 1"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view1);
m_snapWindow.FloatWindow(m_view1,ptFloat);
m_view2.Create(m_hWnd, rcView2, _T("View 2"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view2);
m_snapWindow.SnapWindow(m_view2, snapTopLeft);
m_view3.Create(m_hWnd, rcView1, _T("View 3"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view3);
m_snapWindow.SnapWindow(m_view3, snapTop, 100);
m_view4.Create(m_hWnd, rcView2, _T("View 4"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view4);
// Forced view to stay open (will be pinned)
m_snapWindow.SnapWindow(m_view4,snapBottomRight,0,snapPinned);
m_view5.Create(m_hWnd, rcView2, _T("View 5"),
SNAP_DEFAULT_VIEW_STYLE, WS_EX_CLIENTEDGE);
m_snapWindow.AddSnappableWindow(m_view5);
m_snapWindow.SnapWindow(m_view5, snapTop, 350);
return 0;
}
// ... WTL Wizard code omitted
};
That's all folks!