Click here to Skip to main content
15,892,059 members
Articles / Desktop Programming / Windows Forms

Adding Mouse Gesture Functionality to Your .NET Application Within Seconds

Rate me:
Please Sign up or sign in to vote.
4.90/5 (56 votes)
15 May 2008CPOL10 min read 112.1K   2.9K   151  
This project allows you to add mouse gesture functionality to your Windows Forms application with just a few lines of code.
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;
using System.ComponentModel;


namespace DcamMouseGesture 
{
    #region delegates & event args

    public class MouseGestureEventArgs : EventArgs
    {
        public string Key { get; set; }
        public string Command { get; set; }
        public Rectangle Bounds { get; set; }
        public Control Control { get; set; }

        public MouseGestureEventArgs( string strKey, string strCommand, Rectangle rBounds, Control ctrl )
        {
            Key = strKey;
            Command = strCommand;
            Bounds = rBounds;
            Control = ctrl;
        }
    }

    public delegate void MouseGestureEventHandler( object sender, MouseGestureEventArgs e );

    #endregion // delegates & event args

    public class MouseGesture : IDisposable
    {
        #region enums

        internal enum EKeyOperation
        {
            MirrorX,
            MirrorY,
            MirrorXY,
            RotateCW,
            RotateCCW
        }

        #endregion // enums

        #region constants & variables

        // for Base64 mapping table see http://msdn2.microsoft.com/en-us/library/cc422512.aspx 
        private const string __strBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        private const int __iSize = 6;
        internal const int __iGestureThumbSize = 32;

        // capturing 
        private bool _bDisposed = false;                                // for the implementation of the IDisposable interface
        private MouseGestureData _data = MouseGestureData.Instance;     // use a variable to have a shorter name
        private Graphics _g = null;
        private Pen _penLine = null;
        private Control _ctrlParent = null;                             // the parent control
        private CaptureForm _formCapture = new CaptureForm();           // the form to draw the capture points onto
        private List<Point> _listMousePos = new List<Point>();          // the captured mouse positions, relative to 0/0 of the parent 
                                                                        // control or of the client rectangle of the parent Form
        private List<Point> _listPath = new List<Point>();              // the path of the mouse gesture in x/y coordinates of a square of __iSize * __iSize fields
                                                                        // used to draw the gesture while entering it
        private bool _bCapturing = false;                               // true if capturing is in progress
        private int _iDrawOffsetX = 0;                                  // because of client and screen coordinates difference
        private int _iDrawOffsetY = 0;
        private int _iMouseOffsetX = 0;
        private int _iMouseOffsetY = 0;
        private Point _pntNew;
        private Point _pntLast;                                         // last point drawn, ussed for drawing the lines
        private Timer _timerDelay = new Timer();                        // timer to delay hiding the capture window

        // when last gesture found...
        public Rectangle LastBounds { get; private set; }               // bounding rectangle; 0/0/0/0 if not found
        public string LastKey { get; private set; }                     // key; empty if not found
        public string LastCommand { get; private set; }                 // appropriate command; empty if not found
        public Control LastControl { get; private set; }                // the control the gesture started in; null if not found

        // events
        public event MouseGestureEventHandler MouseGestureEntered;

        #endregion // constants & variables

        #region construction, initialization & destruction

        public MouseGesture( Control ctrlParent, List<Type> listAllowedControls )
        {
            if( ctrlParent == null ) throw new ArgumentNullException();
            _ctrlParent = ctrlParent;

            // init automatic properties
            LastBounds = new Rectangle();
            LastKey = string.Empty;
            LastCommand = string.Empty;
            InitData();

            // setup the timer
            _timerDelay.Interval = _data.HideDelay;
            _timerDelay.Tick += new EventHandler( OnTimerTick );

            // setup the capturing window
            _formCapture.Hide();
            _formCapture.Name = "CaptureForm";
            _formCapture.Text = "CaptureForm";
            _formCapture.ClientSize = new System.Drawing.Size( 500, 500 );
            _formCapture.ShowInTaskbar = false;
            _formCapture.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
            _formCapture.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
            _formCapture.BackColor = Color.WhiteSmoke;
            _formCapture.Opacity = _data.Opacity;

            // make sure null uses default list
            RegisterMouseEvents( _ctrlParent, listAllowedControls == null ? GetDefaultAllowedControls() : listAllowedControls );
        }

        ~MouseGesture()
        {
            Dispose( false );
            DisposeGlobalResources();
        }

        public void Dispose()
        {
            Dispose( true );
            GC.SuppressFinalize( this );
        }

        private void Dispose( bool disposing )
        {
            if( !_bDisposed )
            {
                if( disposing )
                {
                    UnregisterMouseEvents( _ctrlParent );
                    _timerDelay.Tick -= new EventHandler( OnTimerTick );
                }
                _bDisposed = true;
            }
        }

        #endregion // construction, initialization & destruction

        #region instance methods

        internal static Bitmap CreateThumbBitmap( string strKey )
        {
            if( string.IsNullOrEmpty( strKey ) ) throw new ArgumentNullException( Properties.Resources.MouseGesture_ErrorEmptyKey );

            Bitmap bmpRet = new Bitmap( __iGestureThumbSize, __iGestureThumbSize );
            Graphics g = Graphics.FromImage( bmpRet );

            // draw bitmap
            try
            {
                int iStretch = 3;
                int iOffset = ( __iGestureThumbSize - iStretch * __iSize ) / 2;

                // draw second to second last points
                for( int i = 1; i < strKey.Length - 1; i++ )
                {
                    int iX = __strBase64.IndexOf( strKey[ i ] ) % __iSize;
                    int iY = __strBase64.IndexOf( strKey[ i ] ) / __iSize;

                    // only draw the point if it is a valid index
                    if( iX == -1 || iY == -1 ) throw new Exception();
                        
                    g.FillRectangle( Brushes.LightPink, iOffset + iX * iStretch, iOffset + iY * iStretch, iStretch, iStretch );
                }

                // draw first and last point to make sure they are visible
                int iX0 = __strBase64.IndexOf( strKey[ 0 ] ) % __iSize;
                int iY0 = __strBase64.IndexOf( strKey[ 0 ] ) / __iSize;
                int iX1 = __strBase64.IndexOf( strKey[ strKey.Length - 1 ] ) % __iSize;
                int iY1 = __strBase64.IndexOf( strKey[ strKey.Length - 1 ] ) / __iSize;

                // both points are at the same position
                if( iX0 == iX1 && iY0 == iY1 )
                {
                    g.FillRectangle( Brushes.DarkViolet, iOffset + iX0 * iStretch, iOffset + iY0 * iStretch, iStretch, iStretch );
                }
                else
                {
                    g.FillRectangle( Brushes.Blue, iOffset + iX0 * iStretch, iOffset + iY0 * iStretch, iStretch, iStretch );
                    g.FillRectangle( Brushes.Red, iOffset + iX1 * iStretch, iOffset + iY1 * iStretch, iStretch, iStretch );
                }
            }
            catch
            {
                // draw a red X on white background with blue text "Error"
                g.Clear( Color.White );
                g.DrawLine( Pens.Red, 0, 0, __iGestureThumbSize - 1, __iGestureThumbSize - 1 );
                g.DrawLine( Pens.Red, 0, __iGestureThumbSize - 1, __iGestureThumbSize - 1, 0 );
                StringFormat sf = new StringFormat();
                sf.Alignment = StringAlignment.Center;
                sf.LineAlignment = StringAlignment.Center;
                g.DrawString( "Error", new Font( "Arial", 8f ), Brushes.Blue, __iGestureThumbSize / 2, __iGestureThumbSize / 2, sf );
            }
            g.Dispose();

            return bmpRet;
        }

        internal static string MirrorRotateKey( string strKey, EKeyOperation keyOp )
        {
            StringBuilder sbKey = new StringBuilder( strKey.Length );

            for( int i = 0; i < strKey.Length; i++ )
            {
                int iIndex = __strBase64.IndexOf( strKey[ i ] );
                int iX = iIndex % __iSize;
                int iY = iIndex / __iSize;

                switch( keyOp )
                {
                    case EKeyOperation.MirrorX:
                        sbKey.Append( __strBase64[ ( __iSize - 1 - iX ) + ( iY * __iSize ) ] );
                        break;
                    case EKeyOperation.MirrorY:
                        sbKey.Append( __strBase64[ iX + ( ( __iSize - 1 - iY ) * __iSize ) ] );
                        break;
                    case EKeyOperation.MirrorXY:
                        sbKey.Append( __strBase64[ ( __iSize - 1 - iX ) + ( ( __iSize - 1 - iY ) * __iSize ) ] );
                        break;
                    case EKeyOperation.RotateCW:
                        sbKey.Append( __strBase64[ ( __iSize - 1 - iY ) + ( iX * __iSize ) ] );
                        break;
                    case EKeyOperation.RotateCCW:
                        sbKey.Append( __strBase64[ iY + ( ( __iSize - 1 - iX ) * __iSize ) ] );
                        break;
                    default:
                        throw new NotImplementedException();
                }
            }

            return sbKey.ToString();
        }

        public static List<Type> GetDefaultAllowedControls()
        {
            return new List<Type>() { typeof( Label ), typeof( GroupBox ), typeof( PictureBox ), 
                                      typeof( ProgressBar ), typeof( ScrollableControl ), typeof( TabControl ) };

        }

        private void RegisterMouseEvents( Control ctrlParten, List<Type> listAllowedControls )
        {
            // Note: 
            //  We are not registering for the mouse events on all type of controls,
            //  only some sort of "container controls". Meaning that these are controls that "show" 
            //  some sort of the parent control's background and not having selectable elements and 
            //  not being data entering controls.
            //  Also assuming that only such controls can have other "container controls", if any; 
            //  (e.g. a GroupBox that is not allowed can't contain a ListBox that is allowed)
            bool bOk = false;
            foreach( Type t in listAllowedControls )
            {
                if( t.IsInstanceOfType( ctrlParten ) )
                {
                    bOk = true;
                    break;
                }
            }
            if( !bOk ) return;

            // register passed control
            ctrlParten.MouseDown += new MouseEventHandler( OnMouseDown );
            ctrlParten.MouseUp += new MouseEventHandler( OnMouseUp );
            ctrlParten.MouseMove += new MouseEventHandler( OnMouseMove );

            // recursive call of all containing controls 
            foreach( Control ctrl in ctrlParten.Controls )
            {
                RegisterMouseEvents( ctrl, listAllowedControls );
            }
        }

        private void UnregisterMouseEvents( Control ctrlParten )
        {
            // unregister for all control, so we don't need a list of the allowed controls
            ctrlParten.MouseDown -= new MouseEventHandler( OnMouseDown );
            ctrlParten.MouseUp -= new MouseEventHandler( OnMouseUp );
            ctrlParten.MouseMove -= new MouseEventHandler( OnMouseMove );

            // recursive call of all containing controls 
            foreach( Control ctrl in ctrlParten.Controls )
            {
                UnregisterMouseEvents( ctrl );
            }
        }

        private void DisposeGlobalResources()
        {
            if( _g != null )
            {
                _g.Dispose();
                _g = null;
            }

            if( _penLine != null )
            {
                _penLine.Dispose();
                _penLine = null;
            }

            _formCapture.DisposeBackgroundBitmap();
        }

        private void InitData()
        {
            _listMousePos.Clear();
            _listPath.Clear();
            _iDrawOffsetX = 0;
            _iDrawOffsetY = 0;
            _pntLast = new Point( -1000000, -1000000 );     // make this invalid

            LastBounds = new Rectangle();
            LastKey = string.Empty;
            LastCommand = string.Empty;
            LastControl = null;
        }

        private void AddMousePoint( Point pnt )
        {
            _pntNew = new Point( _iMouseOffsetX + pnt.X, _iMouseOffsetY + pnt.Y );
            _listMousePos.Add( _pntNew );
        }

        private void DrawMousePoint()
        {
            switch( _data.GestureAppearance )
            {
                case EGestureAppearance.Points:
                    _g.DrawLine( _penLine, _iDrawOffsetX + _pntNew.X, _iDrawOffsetY + _pntNew.Y,
                                _iDrawOffsetX + _pntNew.X + 1, _iDrawOffsetY + _pntNew.Y + 1 );
                    break;
                case EGestureAppearance.Lines:
                    if( _pntLast.X > -1000000 )
                    {
                        _g.DrawLine( _penLine, _iDrawOffsetX + _pntLast.X, _iDrawOffsetY + _pntLast.Y,
                                    _iDrawOffsetX + _pntNew.X, _iDrawOffsetY + _pntNew.Y );
                    }
                    _pntLast = _pntNew;
                    break;
            }
        }

        private void DrawBoundsGridPath()
        {
            if( _data.ShowBounds )
            {
                Pen penBounds = new Pen( _data.BoundsColor );
                Rectangle rect = new Rectangle( _iDrawOffsetX + LastBounds.X, _iDrawOffsetY + LastBounds.Y, LastBounds.Width, LastBounds.Height );
                _g.DrawRectangle( penBounds, rect );
                penBounds.Dispose();

                if( _data.ShowGrid )
                {
                    Pen penGrid = new Pen( _data.GridColor );
                    for( int i = 1; i < __iSize; i++ )
                    {
                        _g.DrawLine( penGrid, rect.X + rect.Width * i / __iSize, rect.Y, rect.X + rect.Width * i / __iSize, rect.Bottom );
                        _g.DrawLine( penGrid, rect.X, rect.Y + rect.Height * i / __iSize, rect.Right, rect.Y + rect.Height * i / __iSize );
                    }
                    penGrid.Dispose();
                }
            }

            if( _data.ShowPath )
            {
                Brush brush = new SolidBrush( _data.PathColor );
                Rectangle rect = new Rectangle( 0, 0, LastBounds.Width / __iSize, LastBounds.Height / __iSize );
                for( int i = 0; i < _listPath.Count; i++ )
                {
                    rect.X = _iDrawOffsetX + LastBounds.X + _listPath[ i ].X * LastBounds.Width / __iSize;
                    rect.Y = _iDrawOffsetY + LastBounds.Y + _listPath[ i ].Y * LastBounds.Height / __iSize;
                    _g.FillRectangle( brush, rect );
                }
                brush.Dispose();
            }
        }

        private void CreateKeyAndPath()
        {
            // Note: 
            //  - The path of the mouse gesture is in x/y coordinates of a square of __iSize * __iSize fields
            //    --> so top/left square = 0/0, bottom/right square = 7/7.
            //  - The key is a string of Base64 maped charactes, where each character defines a field 
            //    number in the square of __iSize * __iSize fields. The fields are numbered from 0 to 63 from 
            //    left to right and from top to buttom.
            //    --> so top/left square = 0, bottom/right square = 63.
            StringBuilder sbKey = new StringBuilder( 100 );
            Point pntLast = new Point( -1, -1 );
            Point pntPath;

            // using only X axis (Y axis is always 0 and won't be added to the key)
            if( LastBounds.Height / __iSize < _data.UnidimensionalLimit )
            {
                for( int i = 0; i < _listMousePos.Count; i++ )
                {
                    pntPath = new Point( ( _listMousePos[ i ].X - LastBounds.X ) * __iSize / LastBounds.Width, 0 );
                    if( pntPath.X != pntLast.X )
                    {
                        _listPath.Add( pntPath );
                        pntLast = pntPath;
                        sbKey.Append( __strBase64[ pntPath.X + pntPath.Y * __iSize ] );
                    }
                }
            }
            // ...using only Y axis (X axis is always 0 and won't be added to the key)
            else if( LastBounds.Width / __iSize < _data.UnidimensionalLimit )
            {
                for( int i = 0; i < _listMousePos.Count; i++ )
                {
                    pntPath = new Point( 0, ( _listMousePos[ i ].Y - LastBounds.Y ) * __iSize / LastBounds.Height );
                    if( pntPath.Y != pntLast.Y )
                    {
                        _listPath.Add( pntPath );
                        pntLast = pntPath;
                        sbKey.Append( __strBase64[ pntPath.X + pntPath.Y * __iSize ] );
                    }
                }
            }
            // ...using x and Y axis = two-dimensional
            else
            {
                for( int i = 0; i < _listMousePos.Count; i++ )
                {
                    pntPath = new Point( ( _listMousePos[ i ].X - LastBounds.X ) * __iSize / LastBounds.Width,
                                         ( _listMousePos[ i ].Y - LastBounds.Y ) * __iSize / LastBounds.Height );
                    if( pntPath.X != pntLast.X || pntPath.Y != pntLast.Y )
                    {
                        _listPath.Add( pntPath );
                        pntLast = pntPath;
                        sbKey.Append( __strBase64[ pntPath.X + pntPath.Y * __iSize ] );
                    }
                }
            }

            LastKey = sbKey.ToString();
        }

        private void AnalysePoints()
        {
            // search bounding rectangle
            int iMinX, iMaxX, iMinY, iMaxY;
            iMinX = iMaxX = _listMousePos[ 0 ].X;
            iMinY = iMaxY = _listMousePos[ 0 ].Y;
            for( int i = 1; i < _listMousePos.Count; i++ )
            {
                Point pnt = _listMousePos[ i ];
                if( pnt.X < iMinX ) iMinX = pnt.X;
                if( pnt.Y < iMinY ) iMinY = pnt.Y;
                if( pnt.X > iMaxX ) iMaxX = pnt.X;
                if( pnt.Y > iMaxY ) iMaxY = pnt.Y;
            }
            LastBounds = new Rectangle( iMinX, iMinY, iMaxX - iMinX + 1, iMaxY - iMinY + 1 );

            // check for minimum movement
            if( LastBounds.Width >= _data.MinimumMovement || LastBounds.Height >= _data.MinimumMovement )
            {
                CreateKeyAndPath();
                LastCommand = _data.Commands.GetCommand( LastKey );
            }
            else
            {
                InitData();
            }
        }

        #endregion // instance methods

        #region event handlers

        private void OnMouseDown( object sender, MouseEventArgs e )
        {
            // only enter capturing if...
            if( !_bCapturing &&                                     // ...not capturing in progress
                e.Button == _data.MouseButtonTrigger &&             // ...the button pressed last to trigger
                Control.MouseButtons == _data.MouseButtonMask &&    // ...this/these button/s pressed together
                Control.ModifierKeys == _data.ModifierKeyMask       // ...this/these modifier key/s (shift/ctrl/alt)
              )
            {
                InitData();
                LastControl = (Control)sender;
                _bCapturing = true;

                // set position and size of the capture window and calc the offsets
                Point pntParent = _ctrlParent.PointToScreen( new Point() );
                Point pntSender = ( (Control)sender ).PointToScreen( new Point() );
                _iMouseOffsetX = pntSender.X - pntParent.X;
                _iMouseOffsetY = pntSender.Y - pntParent.Y;

                switch( _data.WindowAppearance )
                {
                    case ECaptureWindowAppearance.FullScreenOpaque:
                        _formCapture.Location = Screen.PrimaryScreen.Bounds.Location;
                        _formCapture.Size = Screen.PrimaryScreen.Bounds.Size;
                        _iDrawOffsetX = pntParent.X;
                        _iDrawOffsetY = pntParent.Y;
                        break;

                    case ECaptureWindowAppearance.ParentOpaque:
                    case ECaptureWindowAppearance.ParentClear:
                        _formCapture.Location = pntParent;
                        // the Form's Size includes title and border, but we only want the client area
                        Form formTmp = _ctrlParent as Form;
                        _formCapture.Size = ( formTmp != null ) ? formTmp.ClientSize : _ctrlParent.Size;

                        if( _data.WindowAppearance == ECaptureWindowAppearance.ParentClear )
                        {
                            _formCapture.CaptureBackgroundBitmap();
                        }
                        break;
                }

                // add first point
                AddMousePoint( e.Location );

                // if gesture window desired
                if( _data.WindowAppearance != ECaptureWindowAppearance.None )
                {
                    // make it visible
                    _formCapture.Opacity = _data.Opacity;
                    _formCapture.BackColor = _data.BackColor;
                    _formCapture.Show();

                    // create global resources to draw
                    _g = _formCapture.CreateGraphics();
                    _penLine = new Pen( _data.LineColor, _data.LineWidth );

                    // finaly draw the first point
                    DrawMousePoint();
                }
            }
        }

        private void OnMouseMove( object sender, MouseEventArgs e )
        {
            if( _bCapturing )
            {
                AddMousePoint( e.Location );
                if( _data.WindowAppearance != ECaptureWindowAppearance.None ) DrawMousePoint();
            }
        }

        private void OnMouseUp( object sender, MouseEventArgs e )
        {
            if( _bCapturing )
            {
                // from now on, capturing the mouse positions is finished, no matter
                // if a gesture will be found or not
                _bCapturing = false;

                // the trigger button must be released to successfully end capturing 
                if( e.Button == _data.MouseButtonTrigger )
                {
                    AddMousePoint( e.Location );
                    // DrawPoint not needed, because MouseUp event raises after last MouseMove event
                    AnalysePoints();

                    // draw result
                    if( _data.WindowAppearance != ECaptureWindowAppearance.None )
                    {
                        DrawBoundsGridPath();
                        _timerDelay.Interval = _data.HideDelay; 
                        _timerDelay.Start();
                    }
                    else
                    {
                        DisposeGlobalResources();
                        _formCapture.Hide();
                    }

                    // rais event
                    if( MouseGestureEntered != null )
                    {
                        MouseGestureEntered( this, new MouseGestureEventArgs( LastKey, LastCommand, LastBounds, LastControl ) );
                    }
                }

                // otherwise just clear the data and hide window
                else
                {
                    InitData();
                    DisposeGlobalResources();
                    _formCapture.Hide();
                }
            }
        }

        private void OnTimerTick( object sender, EventArgs e )
        {
            _timerDelay.Stop();

            if( _g != null )
            {
                // clear the bounding rectangle, otherwise it will
                // be drawn when showing the window again
                Brush brush = new SolidBrush( _formCapture.BackColor );
                _g.FillRectangle( brush, LastBounds );
                brush.Dispose();

                DisposeGlobalResources();
            }

            _formCapture.Hide();
        }

        #endregion // event handlers
    }
}

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
Switzerland Switzerland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions