Snappable tool windows that auto-hide

Snappable tool windows that auto-hide

Jens Nilsson
Rate me:
 
5.00/5 (9 votes)
21 Feb 20026 min read
An article on the framework for implementing snapping windows.
Is your email address OK? You are signed up for our newsletters or notifications but your email address is either unconfirmed, or has not been reconfirmed in a long time. Please click here to have a confirmation email sent so we can start sending you newsletters again.

Sample Image

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 be NULL if not successful
   Remark: 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 with AddSnappableWindow
   const POINT& ptPos top left coordinates where the window will be placed
   DWORD dwFlags additional flags, defaults to zero
   Remark: 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 with AddSnappableWindow
   <A href="#SnapPosition">SnapPosition</A> spPos snapped position
   int cxy offset from the top or left edge depending on spPos, defaults to zero
   DWORD dwFlags additional flags, defaults to snapMinibar
   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 with AddSnappableWindow

<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 and snapMinibar 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 CSnapWindowInfoCSnapTrackInfo 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!

posted @ 2022-12-13 13:57  小风风的博客  阅读(14)  评论(0编辑  收藏  举报