Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / WTL
Article

Snappable tool windows that auto-hide

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
20 Feb 20026 min read 102.3K   3.2K   43   10
An article on the framework for implementing snapping windows.

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.

snapFloat0x00000000
snapLeft0x00000001
snapTop0x00000002
snapRight0x00000004
snapBottom0x00000008
snapTopLeft0x00000003
snapTopRight0x00000006
snapBottomLeft0x00000009
snapBottomRight0x0000000C
snapHidden0x00000010

Additional flags and masks to manage the dwFlags attribute:

State flags
snapPinned0x00000100
snapMinibar0x00000200
 
Masks
snapPosition0x000000FF
snapState0x00FFFF00
snapReserved0xFF000000

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  hWndSnappedSnapped window handle
HWND  hWndFloatedFloating window handle
HWND  hWndViewView window handle
HWND  hWndViewSnap window manager
DWORD dwFlagsPosition 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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Sweden Sweden
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionCan we use this with window mdi application Pin
deepasaral1-Dec-06 23:53
deepasaral1-Dec-06 23:53 
GeneralA very good article, ... full of technical expertise. Pin
WREY10-Jul-05 9:51
WREY10-Jul-05 9:51 
GeneralGreate work! Make it better! Pin
dido2k20-Nov-04 5:50
dido2k20-Nov-04 5:50 
GeneralSuggestion Pin
Rui Lopes22-Feb-02 3:12
Rui Lopes22-Feb-02 3:12 
GeneralRe: Suggestion Pin
Jens Nilsson25-Feb-02 12:20
Jens Nilsson25-Feb-02 12:20 
GeneralRe: Suggestion Pin
Rui Lopes25-Feb-02 12:29
Rui Lopes25-Feb-02 12:29 
GeneralThank you Pin
Ramon Casellas21-Feb-02 22:06
Ramon Casellas21-Feb-02 22:06 
GeneralMFC Version Pin
Paul Selormey21-Feb-02 13:42
Paul Selormey21-Feb-02 13:42 
GeneralRe: MFC Version Pin
Ramon Casellas21-Feb-02 22:03
Ramon Casellas21-Feb-02 22:03 
Hi Paul,

I have not done it before (never used MFC), but I am definitely convinced that you can add WTL-windows into your MFC project quite easily. Of course, this may not be the answer you want, and probabily you already know what I am saying...

Just as a comment Smile | :) I'am glad to see that there are more and more WTL articles in CodeProject... usually someone has to worry to port a MFC article to WTL.

Regards,
Ramon


GeneralExcellent ! Pin
Jean-Michel LE FOL21-Feb-02 7:29
Jean-Michel LE FOL21-Feb-02 7:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.