Click here to Skip to main content
15,886,026 members
Articles / Programming Languages / C++

The Poor Man's Mouse Gesture

Rate me:
Please Sign up or sign in to vote.
4.84/5 (42 votes)
15 Jun 2005CPOL5 min read 88K   2.4K   55  
An easy to use class for adding basic mouse gesture recognition to your application.
/****************************************************************************
MouseGesture.cpp: implementation of the CMouseGesture class.
written by PJ Arends
pja@telus.net

Copyright (c) 2005

-----------------------------------------------------------------------------
This code is provided as is, with no warranty as to it's suitability or usefulness
in any application in which it may be used. This code has not been tested for
UNICODE builds, nor has it been tested on a network ( with UNC paths ).

This code may be used in any way you desire. This file may be redistributed by any
means as long as it is not sold for profit, and providing that this notice and the
authors name are included.

If any bugs are found and fixed, a note to the author explaining the problem and
fix would be nice.
-----------------------------------------------------------------------------

Revision History:
    June 2005 - initial release on The CodeProject - http://www.codeproject.com

****************************************************************************/

#include "stdafx.h"
#include "MouseGesture.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Registered message sent to the window that initiated the gesture
// when a gesture is recognized
//   WPARAM: The ID of the recognized gesture
//   LPARAM: A CMouseGesture* pointer to the CGesture object that sent the message
//////////////////////////////////////////////////////////////////////

const UINT WMU_MOUSEGESTURE = RegisterWindowMessage(MOUSE_GESTURE_MESSAGE_STRING);

//////////////////////////////////////////////////////////////////////
// DEBUG only stuff
//////////////////////////////////////////////////////////////////////

#ifdef _DEBUG

//////////////////////////////////////////////////////////////////////
// Uncomment the #define _SHOW_GESTURE line to draw mouse points and
// bounding squares on screen when the gesture is being made. 
//////////////////////////////////////////////////////////////////////

#   define _SHOW_GESTURE  // this line controls visual feedback

#   ifdef _SHOW_GESTURE

        void SHOW_BOUNDING_SQUARE (MOUSEHOOKSTRUCT *pMHS,
                                   RECT rc)
        {
            HWND hWnd = WindowFromPoint(pMHS->pt);
            MapWindowPoints(NULL, hWnd, (POINT *)&rc, 2);
            HDC hdc = GetDC(hWnd);
            DrawFocusRect(hdc, &rc);
            ReleaseDC(hWnd, hdc);
        }

#   else // _SHOW_GESTURE
#       define SHOW_BOUNDING_SQUARE(pMHS, rc)
#   endif // _SHOW_GESTURE
#else // _DEBUG
#   define SHOW_BOUNDING_SQUARE(pMHS, rc)
#endif // _DEBUG

//////////////////////////////////////////////////////////////////////
// handy functions borrowed from Chris Maunder's CGridCtrl class
//////////////////////////////////////////////////////////////////////

#define IsSHIFTpressed() ( (GetKeyState(VK_SHIFT) & (1 << (sizeof(SHORT)*8-1))) != 0   )
#define IsCTRLpressed()  ( (GetKeyState(VK_CONTROL) & (1 << (sizeof(SHORT)*8-1))) != 0 )

//////////////////////////////////////////////////////////////////////
// Static private member variables
//////////////////////////////////////////////////////////////////////

CMouseGesture::_WndMap CMouseGesture::WindowMap;
CMouseGesture* CMouseGesture::Current = NULL;
HHOOK CMouseGesture::MainHook = NULL;

//////////////////////////////////////////////////////////////////////
// Static functions
//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////
// GetGesturePointer
//    static private member function
//
// Gets a pointer to the CGesture object that will handle mouse
// gestures for the given window.
//////////////////////////////////////////////////////////////////////

CMouseGesture* CMouseGesture::GetGesturePointer(HWND hWnd,
                                                POINT pt)
{
    if (!IsWindow(hWnd))
    {
        return NULL;
    }

    _WndMap::iterator it = WindowMap.find(hWnd);
    if (it == WindowMap.end())
    {
        // hWnd not found, move up the parent chain
        return GetGesturePointer(GetParent(hWnd), pt);
    }

    // ensure given point is in the client area of the window
    RECT rc;
    GetClientRect(hWnd, &rc);
    MapWindowPoints(hWnd, NULL, (POINT *)&rc, 2);
    if (!PtInRect(&rc, pt))
    {
        return NULL;
    }

    // ensure the CGesture object has gestures to recognize
    CMouseGesture *pGestures = (CMouseGesture *)((*it).second);
    if (pGestures->m_GestureMap.empty())
    {
        return NULL;
    }

    return pGestures;
}

//////////////////////////////////////////////////////////////////////
// MouseHookProc
//    static private member function
//////////////////////////////////////////////////////////////////////

LRESULT CMouseGesture::MouseHookProc(int nCode,
                                     WPARAM wp,
                                     LPARAM lp)
{
    MOUSEHOOKSTRUCT *pMHS = (MOUSEHOOKSTRUCT *)lp;
    
    if (wp == WM_LBUTTONDOWN ||
        wp == WM_MBUTTONDOWN ||
        wp == WM_RBUTTONDOWN)
    {
        if (Current != NULL)
        {
            Current->KillGesture();
        }

        Current = GetGesturePointer(pMHS->hwnd, pMHS->pt);
    }

    // mouse wheel messages are ignored
    if (wp != WM_MOUSEWHEEL && Current != NULL)
    {
        Current = Current->MouseMessage(wp, lp);
    }

    return CallNextHookEx(MainHook, nCode, wp, lp);
}

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CMouseGesture::CMouseGesture() :
m_hWnd(NULL),
m_bCaptured(false),
m_bShift(false),
m_bControl(false),
m_ButtonFlag(0),
m_ButtonDown(None),
m_CurrentGesture(NULL),
m_nDistance(0),
m_LastDirection(None)
{
}

CMouseGesture::~CMouseGesture()
{
    Detach();
}

//////////////////////////////////////////////////////////////////////
// Initialization functions
//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////
// Attach
//    public member function
//
// Sets up the CMouseGesture object for use
//
// Parameters:
//    hWnd     - The window that uses the gestures
//    Distance - The minimum distance, in pixels, that the mouse must
//               travel in order for a motion to be recognized
//
// Returns:
//    true if successful
//    false if not successful
//////////////////////////////////////////////////////////////////////

bool CMouseGesture::Attach(HWND hWnd,
                           UINT Distance /* = 25 */)
{
    _ASSERT (Distance > 9);
    _ASSERT (::IsWindow(hWnd));

    if (Distance < 10 || !IsWindow(hWnd))
    {
        return false;
    }

    if (m_hWnd != NULL)
    {
        // This CMouseGesture is already attached to a window
        // call Detach() before calling Attach()
        _ASSERT (false);
        return false;
    }

    _WndMap::iterator it = WindowMap.find(hWnd);
    if (it != WindowMap.end())
    {
        // This window already has a CMouseGesture object associated with it
        _ASSERT (false);
        return false;
    }

    if (WindowMap.insert(std::make_pair(hWnd, this)).second != true)
    {
        // failed to add this window to the WindowMap
        _ASSERT (false);
        return false;
    }

    if (MainHook == NULL)
    {
        DWORD ThreadID = GetWindowThreadProcessId(hWnd, NULL);
        MainHook = SetWindowsHookEx(WH_MOUSE, MouseHookProc, NULL, ThreadID);
        if (MainHook == NULL)
        {
            // failed to create the hook
            WindowMap.erase(hWnd);
            _ASSERT (false);
            return false;
        }
    }

    m_hWnd = hWnd;
    m_nDistance = Distance;

    return true;
}

//////////////////////////////////////////////////////////////////////
// AddGesture
//    public member functions
//
// Adds mouse movement patterns for the CMouseGesture object to recognize
//
// Parameters:
//    ID       - An identifier number for the pattern
//    Motions  - A pointer to an array of Motions
//    count    - The number of Motions in the array
//    rGesture - A Gesture vector containing the Motions
//
// Returns:
//    -2 : gesture already present
//    -1 : a gesture with given ID already present
//    0  : error in the supplied gesture or invalid ID
//    >0 : number of gestures
//////////////////////////////////////////////////////////////////////

int CMouseGesture::AddGesture(UINT ID,
                              const Motion *Motions,
                              size_t count)
{
    _ASSERT (Motions != NULL);
    _ASSERT (count > 1);

    if (Motions == NULL || count < 2)
    {
        return 0;
    }

    Gesture gesture;
    for (size_t i = 0; i < count; ++i)
    {
        gesture.push_back(Motions[i]);
    }

    return AddGesture(ID, gesture);
}

int CMouseGesture::AddGesture(UINT ID,
                              const Gesture &rGesture)
{
    _ASSERT (ID > 0);
    _ASSERT (rGesture.size() > 1);

    if (rGesture.size() < 2 || ID < 1)
    {
        return 0;
    }

    // Make sure the supplied gesture is valid
    if (rGesture[0] != LeftButton &&
        rGesture[0] != MiddleButton &&
        rGesture[0] != RightButton)
    {
        // The first element must be the mouse button
        _ASSERT (false);
        return 0;
    }

    Motion last = None;
    for (Gesture::const_iterator it = rGesture.begin(); it != rGesture.end(); ++it)
    {
        if (*it == LeftButton ||
            *it == MiddleButton ||
            *it == RightButton)
        {
            if (it != rGesture.begin())
            {
                // mouse button can only be first
                _ASSERT(false);
                return 0;
            }
            continue;
        }

        if (*it == Shift)
        {
            if (rGesture.size() < 3)
            {
                // Gesture must be more than just Shift
                _ASSERT (false);
                return 0;
            }

            if (it != rGesture.begin() + 1)
            {
                // Shift must be in second spot
                _ASSERT (false);
                return 0;
            }
            continue;
        }

        if (*it == Control)
        {
            if (rGesture.size() < 3)
            {
                // Gesture must be more than just Control
                _ASSERT (false);
                return 0;
            }

            if (it != rGesture.begin() + 1 &&
                it != rGesture.begin() + 2)
            {
                // Control must be either second or third
                _ASSERT (false);
                return 0;
            }

            if (it == rGesture.begin() + 2 &&
                (rGesture[1] != Shift || rGesture.size() < 4))
            {
                // if control is third, shift must be second, and there must be more motions
                _ASSERT (false);
                return 0;
            }
            continue;
        }

        if (*it == last ||
            *it < Up ||
            *it > Right)
        {
            // The motion has to be Up, Down, Left, or Right
            // The same motion can not immediately repeat
            _ASSERT (false);
            return 0;
        }
        last = *it;
    }

    // make sure the supplied ID is unique
    if (m_GestureMap.find(ID) != m_GestureMap.end())
    {
        _ASSERT(false);
        return -1;
    }

    // make sure the supplied gesture is unique
    if (GetGestureIdFromMap(rGesture) != 0)
    {
        _ASSERT(false);
        return -2;
    }

    m_ButtonFlag |= rGesture[0];
    m_GestureMap.insert(std::make_pair(ID, rGesture));

    return m_GestureMap.size();
}

//////////////////////////////////////////////////////////////////////
// Deinitialize functions
//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////
// RemoveGesture
//    public member function
//
// Removes the gesture pattern
//
// Parameters:
//    nID - The ID of the pattern to remove
//
// Returns
//   true : the pattern was found and removed
//   false : the pattern was not found
//////////////////////////////////////////////////////////////////////

bool CMouseGesture::RemoveGesture(UINT nID)
{
    bool ret = m_GestureMap.erase(nID) == 1;

    if (ret)
    {
        // reset the m_ButtonFlag in case a mouse button is eliminated
        m_ButtonFlag = 0;
        for (GestureMap::const_iterator it = m_GestureMap.begin(); it != m_GestureMap.end(); ++it)
        {
            m_ButtonFlag |= (*it).second[0];
        }
    }

    return ret;
}

//////////////////////////////////////////////////////////////////////
// Detach
//    public member function
//
// Detaches this CWindowsGesture object from the window
//////////////////////////////////////////////////////////////////////

void CMouseGesture::Detach()
{
    if (m_hWnd != NULL)
    {
        int erased = WindowMap.erase(m_hWnd);
        _ASSERT (erased == 1);
        m_hWnd = NULL;
    }

    if (WindowMap.size() == 0 && MainHook != NULL)
    {
        UnhookWindowsHookEx(MainHook);
        MainHook = NULL;
    }
}

//////////////////////////////////////////////////////////////////////
// Mouse message handler functions
//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////
// MouseMessage
//    private member function
//
// Called by the MouseHookProc to handle the mouse messages
//
// Returns:
//    the this pointer if handling a gesture
//    NULL if not
//////////////////////////////////////////////////////////////////////

CMouseGesture* CMouseGesture::MouseMessage(WPARAM wp, LPARAM lp)
{
    CMouseGesture *ret = NULL;
    DWORD Message = (DWORD)wp;
    MOUSEHOOKSTRUCT *pMHS = (MOUSEHOOKSTRUCT *)lp;

    switch (Message)
    {
    case WM_LBUTTONDOWN:
        if (m_ButtonFlag & LeftButton)
        {
            m_CurrentGesture.clear();
            m_CurrentGesture.push_back(LeftButton);
            m_ButtonDown = LeftButton;
            ret = OnButtonDown(pMHS);
        }
        break;

    case WM_MBUTTONDOWN:
        if (m_ButtonFlag & MiddleButton)
        {
            m_CurrentGesture.clear();
            m_CurrentGesture.push_back(MiddleButton);
            m_ButtonDown = MiddleButton;
            ret = OnButtonDown(pMHS);
        }
        break;

    case WM_RBUTTONDOWN:
        if (m_ButtonFlag & RightButton)
        {
            m_CurrentGesture.clear();
            m_CurrentGesture.push_back(RightButton);
            m_ButtonDown = RightButton;
            ret = OnButtonDown(pMHS);
        }
        break;

    case WM_LBUTTONUP:
        if (m_ButtonDown == LeftButton)
        {
            ret = OnButtonUp(pMHS);
        }
        break;

    case WM_MBUTTONUP:
        if (m_ButtonDown == MiddleButton)
        {
            ret = OnButtonUp(pMHS);
        }
        break;

    case WM_RBUTTONUP:
        if (m_ButtonDown == RightButton)
        {
            ret = OnButtonUp(pMHS);
        }
        break;

    case WM_MOUSEMOVE:
    case WM_NCMOUSEMOVE:
        if (m_ButtonDown != None)
        {
            ret = OnMouseMove(pMHS);
        }
    }

    if (ret == NULL)
    {
        KillGesture();
    }

    return ret;
}

//////////////////////////////////////////////////////////////////////
// OnButtonDown
//    protected virtual member function
//
// Handles the start of a gesture
//////////////////////////////////////////////////////////////////////

CMouseGesture* CMouseGesture::OnButtonDown(MOUSEHOOKSTRUCT *pMHS)
{
    m_BoundingSquare.left = pMHS->pt.x - m_nDistance;
    m_BoundingSquare.top = pMHS->pt.y - m_nDistance;
    m_BoundingSquare.right = pMHS->pt.x + m_nDistance;
    m_BoundingSquare.bottom = pMHS->pt.y + m_nDistance;

    SHOW_BOUNDING_SQUARE (pMHS, m_BoundingSquare);

    m_LastDirection = None;

    if (IsSHIFTpressed())
    {
        m_bShift = true;
        m_CurrentGesture.push_back(Shift);
    }

    if (IsCTRLpressed())
    {
        m_bControl = true;
        m_CurrentGesture.push_back(Control);
    }

    return this;
}

//////////////////////////////////////////////////////////////////////
// OnMouseMove
//    protected virtual member function
//
// Handles the building of the gesture
//////////////////////////////////////////////////////////////////////

CMouseGesture* CMouseGesture::OnMouseMove(MOUSEHOOKSTRUCT *pMHS)
{
#ifdef _SHOW_GESTURE
    {
        POINT pt;
        pt.x = pMHS->pt.x;
        pt.y = pMHS->pt.y;
        HWND hWnd = WindowFromPoint(pt);
        ScreenToClient(hWnd, &pt);
        HDC hdc = GetDC(hWnd);
        SetPixel(hdc, pt.x, pt.y, RGB(255, 0, 0));
        ReleaseDC(hWnd, hdc);
    }
#endif

    // make sure the SHIFT and CTRL keys are in
    // the same state as when the gesture started
    if (m_bShift != IsSHIFTpressed() ||
        m_bControl != IsCTRLpressed())
    {
        return NULL;
    }

    // looking for next direction mouse is taking
    if (PtInRect(&m_BoundingSquare, pMHS->pt))
    {
        // not yet able to determine
        return this;
    }

    Motion Direction = None;
    if ((pMHS->pt.x >= m_BoundingSquare.right) &&
        (pMHS->pt.y >= m_BoundingSquare.top) &&
        (pMHS->pt.y <= m_BoundingSquare.bottom))
    {
        Direction = Right;
    }
    else if ((pMHS->pt.x <= m_BoundingSquare.left) &&
             (pMHS->pt.y >= m_BoundingSquare.top) &&
             (pMHS->pt.y <= m_BoundingSquare.bottom))
    {
        Direction = Left;
    }
    else if ((pMHS->pt.y >= m_BoundingSquare.bottom) &&
             (pMHS->pt.x >= m_BoundingSquare.left) &&
             (pMHS->pt.x <= m_BoundingSquare.right))
    {
        Direction = Down;
    }
    else if ((pMHS->pt.y <= m_BoundingSquare.top) &&
             (pMHS->pt.x >= m_BoundingSquare.left) &&
             (pMHS->pt.x <= m_BoundingSquare.right))
    {
        Direction = Up;
    }

    if (Direction == None)
    {
        // Unable to calculate the direction, probably went
        // diagonally off a corner of the bounding square
        return NULL;
    }

    SHOW_BOUNDING_SQUARE (pMHS, m_BoundingSquare);

    // move the bounding square to follow the mouse
    if (m_LastDirection == Direction)
    {
        switch (Direction)
        {
        case Left:
        case Right:
            m_BoundingSquare.left = pMHS->pt.x - m_nDistance;
            m_BoundingSquare.right = pMHS->pt.x + m_nDistance;
            break;
        case Up:
        case Down:
            m_BoundingSquare.top = pMHS->pt.y - m_nDistance;
            m_BoundingSquare.bottom = pMHS->pt.y + m_nDistance;
        }
    }
    else // m_LastDirection != Direction
    {
        m_BoundingSquare.left = pMHS->pt.x - m_nDistance;
        m_BoundingSquare.right = pMHS->pt.x + m_nDistance;
        m_BoundingSquare.top = pMHS->pt.y - m_nDistance;
        m_BoundingSquare.bottom = pMHS->pt.y + m_nDistance;

        // direction changed, save the new direction
        m_CurrentGesture.push_back(Direction);
        m_LastDirection = Direction;
    }

    SHOW_BOUNDING_SQUARE (pMHS, m_BoundingSquare);

    if (GetCapture() == NULL)
    {
        // capture the mouse so we can continue recieving messages
        // if the mouse leaves the window
        SetCapture(m_hWnd);
        m_bCaptured = true;
    }

    return this;
}

//////////////////////////////////////////////////////////////////////
// OnButtonUp
//    protected virtual member function
//
// Handles the end of the mouse gesture
//////////////////////////////////////////////////////////////////////

CMouseGesture* CMouseGesture::OnButtonUp(MOUSEHOOKSTRUCT *pMHS)
{
    UNREFERENCED_PARAMETER(pMHS);

    // make sure the SHIFT and CTRL keys are in
    // the same state as when the gesture started
    if (m_bShift != IsSHIFTpressed() || m_bControl != IsCTRLpressed())
    {
        return NULL;
    }


#ifdef _DEBUG
    OutputDebugString (_T("\nDumping gesture:\n"));

    for (Gesture::iterator it = m_CurrentGesture.begin(); it != m_CurrentGesture.end(); ++it)
    {
        switch (*it)
        {
        case Up:
            OutputDebugString (_T("Up, "));
            break;
        case Down:
            OutputDebugString (_T("Down, "));
            break;
        case Left:
            OutputDebugString (_T("Left, "));
            break;
        case Right:
            OutputDebugString (_T("Right, "));
            break;
        case LeftButton:
            OutputDebugString (_T("Left Button, "));
            break;
        case MiddleButton:
            OutputDebugString (_T("Middle Button, "));
            break;
        case RightButton:
            OutputDebugString (_T("Right Button, "));
            break;
        case Shift:
            OutputDebugString (_T("Shift, "));
            break;
        case Control:
            OutputDebugString (_T("Control, "));
            break;
        default:
            OutputDebugString (_T("Invalid Gesture"));
        }
    }
    OutputDebugString (_T("\n\n"));
#endif

    UINT GestureID = GetGestureIdFromMap(m_CurrentGesture);

    if (GestureID > 0 && IsWindow(m_hWnd))
    {
        PostMessage(m_hWnd, WMU_MOUSEGESTURE, (WPARAM)GestureID, (LPARAM)this);
    }

    return NULL;
}

//////////////////////////////////////////////////////////////////////
// Helper functions
//////////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////////
// GetGestureIdFromMap
//    private member function
//
// Finds the gesture in the map of recognized patterns
//
// Returns:
//    The ID of the pattern found
//    0 if not found
//////////////////////////////////////////////////////////////////////

UINT CMouseGesture::GetGestureIdFromMap(const Gesture &gesture)
{
    UINT ret = 0;

    for (GestureMap::const_iterator it = m_GestureMap.begin(); it != m_GestureMap.end(); ++it)
    {
        if ((*it).second == gesture)
        {
            ret = (*it).first;
            break;
        }
    }

    return ret;
}

//////////////////////////////////////////////////////////////////////
// KillGesture
//    private member function
//
// Cleans up when a gesture ends or is abandoned
//////////////////////////////////////////////////////////////////////

void CMouseGesture::KillGesture()
{
#ifdef _SHOW_GESTURE
    // remove any drawing done
    RedrawWindow(m_hWnd, NULL, NULL, RDW_INVALIDATE | RDW_ERASE);
#endif
    OutputDebugString (_T("Gesture Ended\n"));

    if (m_bCaptured && GetCapture() == m_hWnd)
    {
        ReleaseCapture();
    }

    m_ButtonDown = None;
    m_bShift = false;
    m_bControl = false;
    m_CurrentGesture.clear();
    m_bCaptured = false;
}

//////////////////////////////////////////////////////////////////////
// End
//////////////////////////////////////////////////////////////////////

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President
Canada Canada
Father of two, brother of two, child of two.
Spouse to one, uncle to many, friend to lots.
Farmer, carpenter, mechanic, electrician, but definitely not a plumber.
Likes walks with the wife, board games, card games, travel, and camping in the summer.
High school graduate, college drop-out.
Hobby programmer who knows C++ with MFC and the STL.
Has dabbled with BASIC, Pascal, Fortran, COBOL, C#, SQL, ASM, and HTML.
Realized long ago that programming is fun when there is nobody pressuring you with schedules and timelines.

Comments and Discussions