How to back-track the mouse to the virtual page

WYSIWYG (What You See Is What You Get) drawing systems rely on representing data on-screen in an accurate manner and usually enable the user to interact with the graphical information using the mouse. Such systems normally use a representation of data stored in a structured model and draw that data on-screen accordingly.

Being able to match the position of the mouse to the exact position of the data representation is therefore very important.

The mouse position is always reported as relative to the origin of the client-area of a window and so the relationship of mouse to model will only ever be accurate in the following circumstances:

  • When the origin of the virtual page coincides with the origin of the window. In other words, the window has not been scrolled.

  • When the scale of the virtual data model is a 1:1 relation to the window on which it's represented. For example no zooming or rotation of the virtual page.

Most WYSIWYG drawing systems have some sort of zoom or pan facility and the simplest way of implementing this is by creating a matrix that describes the scale and offset of the axes and then draw all the items with that transform in-place. The great thing is that the same transform that enables you to draw the panned or zoomed output also enables you to track the mouse's position on your screen to the corresponding position in the virtual page using the inverse of the drawing matrix.

The following listing shows how pan and zoom are catered for in the paint event.

//Creates the drawing matrix with the right zoom;

Matrix mx=new Matrix(_zoom,0,0,_zoom,0,0);

//pans it according to the scroll bars

mx.Translate(this.AutoScrollPosition.X * (1.0f/_zoom), this.AutoScrollPosition.Y * (1.0f/_zoom));

//use it for drawing

e.Graphics.Transform=mx;

'Creates the drawing matrix with the right zoom;

Dim mx As New Matrix(_zoom, 0, 0, _zoom, 0, 0)

'pans it according to the scroll bars

mx.Translate(Me.AutoScrollPosition.X * (1.0F / _zoom), Me.AutoScrollPosition.Y * (1.0F / _zoom))

'use it for drawing

e.Graphics.Transform = mx

Now, any drawing performed on the control's surface will be scaled and positioned according to the zoom factor and the scroll-bar positions. When the mouse is moved around over the window however, the coordinates are still in relation to the control, not the data drawn in it. The following listing shows how to use the drawing matrix to backtrack the mouse.

    protected Point BacktrackMouse(MouseEventArgs e)

    {

      //Creates the drawing matrix with the right zoom;

      Matrix mx=new Matrix(_zoom,0,0,_zoom,0,0);

      //pans it according to the scroll bars

      mx.Translate(this.AutoScrollPosition.X * (1.0f/_zoom), this.AutoScrollPosition.Y * (1.0f/_zoom));

      //inverts it

      mx.Invert();

      //uses it to transform the current mouse position

      Point[] pa=new Point[]{new Point(e.X,e.Y)};

      mx.TransformPoints(pa);

      return pa[0];

    }

 

    Protected Function BacktrackMouse(ByVal e As MouseEventArgs) As Point

      'Creates the drawing matrix with the right zoom;

      Dim mx As New Matrix(_zoom, 0, 0, _zoom, 0, 0)

      'pans it according to the scroll bars

      mx.Translate(Me.AutoScrollPosition.X * (1.0F / _zoom), Me.AutoScrollPosition.Y * (1.0F / _zoom))

      'inverts it

      mx.Invert()

      'uses it to transform the current mouse position

      Dim pa() As Point = {New Point(e.X, e.Y)}

      mx.TransformPoints(pa)

      Return pa(0)

    End Function 'BacktrackMouse

 

Now, whenever the mouse button is clicked or the mouse moved, it is possible to condition the coordinates provided by the MouseEventArgs to provide a coordinate that corresponds to the virtual page no-matter what the zoom or pan values may be. The following demonstration application enables the user do draw spots of colour in response to mouse clicks and drawing strokes. The drawing is performed by storing a long list of points in an ArrayList and redrawing them at each draw cycle. It is possible to input points at any zoom level or pan position.

Figure 1. The demo application at work.

using System;

using System.Drawing;

using System.Drawing.Drawing2D;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

 

namespace backtrack

{

  /// <summary>

  /// Summary description for Form1.

  /// </summary>

  public class Form1 : System.Windows.Forms.Form

  {

    private System.Windows.Forms.MainMenu mainMenu1;

    private System.Windows.Forms.MenuItem menuItem1;

    private System.Windows.Forms.MenuItem menuItem2;

    private System.Windows.Forms.MenuItem menuItem3;

    private System.Windows.Forms.MenuItem menuItem4;

    private System.Windows.Forms.MenuItem menuItem5;

    private System.Windows.Forms.MenuItem menuItem6;

    /// <summary>

    /// Required designer variable.

    /// </summary>

    private System.ComponentModel.Container components = null;

 

    float _zoom=1.0f;

    public float Zoom

    {

      get{return _zoom;}

      set{

        _zoom=value;

        RecalcAutosize();

      }

    }

 

    ArrayList _blobs=new ArrayList();

 

    bool _mouseDown=false;

 

 

    protected void RecalcAutosize()

    {

      this.AutoScrollMinSize=new Size((int)(_zoom*1024), (int)(_zoom*768));

      this.AutoScrollPosition=new Point(0,0);

      Invalidate();

    }

 

 

    public Form1()

    {

      //

      // Required for Windows Form Designer support

      //

      InitializeComponent();

 

      SetStyle(ControlStyles.AllPaintingInWmPaint |

        ControlStyles.DoubleBuffer |

        ControlStyles.UserPaint |

        ControlStyles.ResizeRedraw,

        true);

    }

 

    /// <summary>

    /// Clean up any resources being used.

    /// </summary>

    protected override void Dispose( bool disposing )

    {

      if( disposing )

      {

        if (components != null)

        {

          components.Dispose();

        }

      }

      base.Dispose( disposing );

    }

 

    #region Windows Form Designer generated code

    /// <summary>

    /// Required method for Designer support - do not modify

    /// the contents of this method with the code editor.

    /// </summary>

    private void InitializeComponent()

    {

      this.mainMenu1 = new System.Windows.Forms.MainMenu();

      this.menuItem1 = new System.Windows.Forms.MenuItem();

      this.menuItem2 = new System.Windows.Forms.MenuItem();

      this.menuItem3 = new System.Windows.Forms.MenuItem();

      this.menuItem4 = new System.Windows.Forms.MenuItem();

      this.menuItem5 = new System.Windows.Forms.MenuItem();

      this.menuItem6 = new System.Windows.Forms.MenuItem();

      //

      // mainMenu1

      //

      this.mainMenu1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {

                                            this.menuItem1,

                                            this.menuItem2});

      //

      // menuItem1

      //

      this.menuItem1.Index = 0;

      this.menuItem1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {

                                            this.menuItem6});

      this.menuItem1.Text = "&File";

      //

      // menuItem2

      //

      this.menuItem2.Index = 1;

      this.menuItem2.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {

                                            this.menuItem3,

                                            this.menuItem4,

                                            this.menuItem5});

      this.menuItem2.Text = "Zoom";

      //

      // menuItem3

      //

      this.menuItem3.Index = 0;

      this.menuItem3.Text = "50%";

      this.menuItem3.Click += new System.EventHandler(this.menuItem3_Click);

      //

      // menuItem4

      //

      this.menuItem4.Index = 1;

      this.menuItem4.Text = "100%";

      this.menuItem4.Click += new System.EventHandler(this.menuItem4_Click);

      //

      // menuItem5

      //

      this.menuItem5.Index = 2;

      this.menuItem5.Text = "200%";

      this.menuItem5.Click += new System.EventHandler(this.menuItem5_Click);

      //

      // menuItem6

      //

      this.menuItem6.Index = 0;

      this.menuItem6.Text = "E&xit";

      this.menuItem6.Click += new System.EventHandler(this.menuItem6_Click);

      //

      // Form1

      //

      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

      this.AutoScroll = true;

      this.AutoScrollMinSize = new System.Drawing.Size(1024, 768);

      this.BackColor = System.Drawing.Color.White;

      this.ClientSize = new System.Drawing.Size(275, 249);

      this.Menu = this.mainMenu1;

      this.Name = "Form1";

      this.Text = "Form1";

 

    }

    #endregion

 

    /// <summary>

    /// The main entry point for the application.

    /// </summary>

    [STAThread]

    static void Main()

    {

      Application.Run(new Form1());

    }

 

    private void menuItem6_Click(object sender, System.EventArgs e)

    {

      Application.Exit();

    }

 

    private void menuItem3_Click(object sender, System.EventArgs e)

    {

      Zoom=0.5f;

    }

 

    private void menuItem4_Click(object sender, System.EventArgs e)

    {

      Zoom=1.0f;

    }

 

    private void menuItem5_Click(object sender, System.EventArgs e)

    {

      Zoom=2.0f;

    }

 

    protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)

    {

      base.OnMouseDown(e);

      _mouseDown=true;

      Point pt=BacktrackMouse(e);

      this._blobs.Add(pt);

      Invalidate();

    }

 

    protected override void OnMouseMove(System.Windows.Forms.MouseEventArgs e)

    {

      base.OnMouseMove(e);

      if(_mouseDown)

      {

        Point pt=BacktrackMouse(e);

        this._blobs.Add(pt);

        Invalidate();

      }

    }

 

    protected override void OnMouseUp(System.Windows.Forms.MouseEventArgs e)

    {

      base.OnMouseUp(e);

      _mouseDown=false;

    }

 

    protected Point BacktrackMouse(MouseEventArgs e)

    {

      //Creates the drawing matrix with the right zoom;

      Matrix mx=new Matrix(_zoom,0,0,_zoom,0,0);

      //pans it according to the scroll bars

      mx.Translate(this.AutoScrollPosition.X * (1.0f/_zoom), this.AutoScrollPosition.Y * (1.0f/_zoom));

      //inverts it

      mx.Invert();

      //uses it to transform the current mouse position

      Point[] pa=new Point[]{new Point(e.X,e.Y)};

      mx.TransformPoints(pa);

      return pa[0];

    }

 

    protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)

    {

 

      //Creates the drawing matrix with the right zoom;

      Matrix mx=new Matrix(_zoom,0,0,_zoom,0,0);

      //pans it according to the scroll bars

      mx.Translate(this.AutoScrollPosition.X * (1.0f/_zoom), this.AutoScrollPosition.Y * (1.0f/_zoom));

      //use it for drawing

      e.Graphics.Transform=mx;

 

      SolidBrush sb=new SolidBrush(Color.Red);

      //draw all the points

      foreach(Point pt in _blobs)

        e.Graphics.FillEllipse(sb,pt.X-5, pt.Y-5, 10,10);

      sb.Dispose();

    }

 

 

 

  }

}

 

 

Imports System

Imports System.Drawing

Imports System.Drawing.Drawing2D

Imports System.Collections

Imports System.ComponentModel

Imports System.Windows.Forms

Imports System.Data

 

 

Namespace backtrack

   '/ <summary>

   '/ Summary description for Form1.

   '/ </summary>

  

   Public Class Form1

    Inherits System.Windows.Forms.Form

    Private mainMenu1 As System.Windows.Forms.MainMenu

    Private menuItem1 As System.Windows.Forms.MenuItem

    Private menuItem2 As System.Windows.Forms.MenuItem

    Private WithEvents menuItem3 As System.Windows.Forms.MenuItem

    Private WithEvents menuItem4 As System.Windows.Forms.MenuItem

    Private WithEvents menuItem5 As System.Windows.Forms.MenuItem

    Private WithEvents menuItem6 As System.Windows.Forms.MenuItem

    '/ <summary>

    '/ Required designer variable.

    '/ </summary>

    Private components As System.ComponentModel.Container = Nothing

    

    Private _zoom As Single = 1F

    

    Public Property Zoom() As Single

     Get

      Return _zoom

     End Get

     Set

      _zoom = value

      RecalcAutosize()

     End Set

    End Property

    

    Private _blobs As New ArrayList()

    

    Private _mouseDown As Boolean = False

    

    

    

    Protected Sub RecalcAutosize()

     Me.AutoScrollMinSize = New Size(CInt(_zoom * 1024), CInt(_zoom * 768))

     Me.AutoScrollPosition = New Point(0, 0)

     Invalidate()

    End Sub 'RecalcAutosize

    

    

    

    Public Sub New()

     '

     ' Required for Windows Form Designer support

     '

     InitializeComponent()

     

     SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.DoubleBuffer Or ControlStyles.UserPaint Or ControlStyles.ResizeRedraw, True)

    End Sub 'New

    

    

    '/ <summary>

    '/ Clean up any resources being used.

    '/ </summary>

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)

      If disposing Then

        If Not (components Is Nothing) Then

          components.Dispose()

        End If

      End If

      MyBase.Dispose(disposing)

    End Sub 'Dispose

 

#Region "Windows Form Designer generated code"

 

    '/ <summary>

    '/ Required method for Designer support - do not modify

    '/ the contents of this method with the code editor.

    '/ </summary>

    Private Sub InitializeComponent()

      Me.mainMenu1 = New System.Windows.Forms.MainMenu

      Me.menuItem1 = New System.Windows.Forms.MenuItem

      Me.menuItem2 = New System.Windows.Forms.MenuItem

      Me.menuItem3 = New System.Windows.Forms.MenuItem

      Me.menuItem4 = New System.Windows.Forms.MenuItem

      Me.menuItem5 = New System.Windows.Forms.MenuItem

      Me.menuItem6 = New System.Windows.Forms.MenuItem

      '

      ' mainMenu1

      '

      Me.mainMenu1.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.menuItem1, Me.menuItem2})

      '

      ' menuItem1

      '

      Me.menuItem1.Index = 0

      Me.menuItem1.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.menuItem6})

      Me.menuItem1.Text = "&File"

      '

      ' menuItem2

      '

      Me.menuItem2.Index = 1

      Me.menuItem2.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.menuItem3, Me.menuItem4, Me.menuItem5})

      Me.menuItem2.Text = "Zoom"

      '

      ' menuItem3

      '

      Me.menuItem3.Index = 0

      Me.menuItem3.Text = "50%"

      '

      ' menuItem4

      '

      Me.menuItem4.Index = 1

      Me.menuItem4.Text = "100%"

      '

      ' menuItem5

      '

      Me.menuItem5.Index = 2

      Me.menuItem5.Text = "200%"

      '

      ' menuItem6

      '

      Me.menuItem6.Index = 0

      Me.menuItem6.Text = "E&xit"

      '

      ' Form1

      '

      Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)

      Me.AutoScroll = True

      Me.AutoScrollMinSize = New System.Drawing.Size(1024, 768)

      Me.BackColor = System.Drawing.Color.White

      Me.ClientSize = New System.Drawing.Size(275, 249)

      Me.Menu = Me.mainMenu1

      Me.Name = "Form1"

      Me.Text = "Form1"

    End Sub 'InitializeComponent

#End Region

 

 

    '/ <summary>

    '/ The main entry point for the application.

    '/ </summary>

    <STAThread()> _

    Shared Sub Main()

      Application.Run(New Form1)

    End Sub 'Main

 

 

    Private Sub menuItem6_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles menuItem6.Click

      Application.Exit()

    End Sub 'menuItem6_Click

 

 

    Private Sub menuItem3_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles menuItem3.Click

      Zoom = 0.5F

    End Sub 'menuItem3_Click

 

 

    Private Sub menuItem4_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles menuItem4.Click

      Zoom = 1.0F

    End Sub 'menuItem4_Click

 

 

    Private Sub menuItem5_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles menuItem5.Click

      Zoom = 2.0F

    End Sub 'menuItem5_Click

 

 

    Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)

      MyBase.OnMouseDown(e)

      _mouseDown