.
In Depth Banner
Skip Navigation LinksWelcome > In Depth articles > Canvas (Part 3)

Select your preferred language

In the previous pages you've seen how the canvas class has developed through scrolling and zooming and now it's time to wrap the project up by enabling the user to actually use all that virtual visual goodness. Before starting on the nitty-gritty task of coding lets take a look at the ideas in the Canvas class and see how they can be related to the position of the mouse.

The page as it stands at the moment is defined by the PageSize property which is a size containing width and height. When your code draws on the page or when the user clicks the mouse in the page, you should only have to deal with those defined dimensions. The display however may be shifted to a different position by the scroll bars or have a different apparent size due to zooming. Internally, for the purposes of drawing the page contents, a virtual size is calculated that may be larger or smaller than the actual page size. For example, a page which is 1024 X 768 zoomed 200% has an apparent size of 2048 X 1536. The mouse position is reported relative to the window so to find the effective position of the mouse in the real page, the scale and scrollbar offsets need to be taken into consideration. Figure 1 illustrates this.

pan and zoom dot net drawing surface

Figure 1: How the virtual page relates to the actual page and the mouse position.

Backtracking the mouse

Whenever the Canvas class draws a page, it sets up a matrix that provides scaling and transformations that enable the user to simply draw on the page as if it were a one to one representation of the graphical data. It's important therefore to ensure that information streaming back to the application via the mouse is also in the same scale otherwise a confusing situation would be created where a non-symmetrical conversion would be required.

Because the Matrix can be used to locate a particular pixel on screen from a position in the page, the screen pixel under the mouse can be translated to the document using the inverse of that matrix.

Backtracking the mouse requires that the drawing matrix is calculated, inverted and then applied to the mouse point to find the equivalent position in the document page.

The code shown in Listing 1 performs this task.

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

 

Size s=new Size((int)(this.ClientSize.Width*(1f/_zoom)), (int)(this.ClientSize.Height*(1f/_zoom)));

 

if(s.Width>PageSize.Width)

  mx.Translate((s.Width/2)-(_pageSize.Width/2),0);

else

  mx.Translate((float)this.AutoScrollPosition.X*(1f/_zoom),0);

 

if(s.Height>PageSize.Height)

  mx.Translate(0,(s.Height/2)-(this._pageSize.Height/2)+(this.AutoScrollPosition.Y));

else

  mx.Translate(0,(float)this.AutoScrollPosition.Y*(1f/_zoom));

 

mx.Invert();

 

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

 

mx.TransformPoints(px);

 

Keeping the design time clean

Events and delegates in .NET are simple to use and are easily managed by the design time environment. Furthermore, people are used to using particular events so it doesn't make the canvas control easier to use if events and methods are added that confuse the interface. For this reason, it's a great idea to have the canvas class return mouse information via the standard mouse events but provide coordinates that relate to the page size chosen regardless of scale or scrollbar offsets.

This can be accomplished by overriding the OnMouse<operation> methods and modifying the mouse position according to the scale and offset. The Backtrack method can be made to modify the standard MouseEventArgs so that the  coordinates passed to the application relates directly to the document's size instead of to the window.

This method takes advantage of a design feature built into Windows Forms controls that illustrates the amount of time and effort a good initial design can pass on to the users of a class. Where a Windows Forms control provides an event, it usually provides a protected method which raises that event, so OnMouseMove checks to see if there are handlers attached to the MouseMove event and raises the event if there are any. By overriding OnMouseMove we can modify it's behaviour so that the programmer who uses the control in their own code, sees the same interface and familiar events but with greatly modified behaviour. Isn't OOP wonderful?

Listing 2 shows the full backtrack mouse routine and the modified mouse event methods.

    protected virtual MouseEventArgs BacktrackMouse(MouseEventArgs e)

    {

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

 

      Size s=new Size( (int)(this.ClientSize.Width*(1f/_zoom)), (int)(this.ClientSize.Height*(1f/_zoom)));

 

      if(s.Width>PageSize.Width)

        mx.Translate((s.Width/2)-(_pageSize.Width/2),0);

      else

        mx.Translate((float)this.AutoScrollPosition.X*(1f/_zoom),0);

 

      if(s.Height>PageSize.Height)

        mx.Translate(0,(s.Height/2)-(this._pageSize.Height/2)+ (this.AutoScrollPosition.Y));

      else

        mx.Translate(0,(float)this.AutoScrollPosition.Y*(1f/_zoom));

 

      mx.Invert();

 

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

 

      mx.TransformPoints(px);

 

      MouseEventArgs et=new MouseEventArgs(e.Button,e.Clicks,px[0].X,px[0].Y,e.Delta);

 

      return et;

    }

 

    protected override void OnMouseMove(MouseEventArgs e)

    {

      MouseEventArgs et=this.BacktrackMouse(e);

      base.OnMouseMove(et);

    }

 

    protected override void OnMouseUp(MouseEventArgs e)

    {

      MouseEventArgs et=this.BacktrackMouse(e);

      base.OnMouseUp(et);

    }

 

    protected override void OnMouseDown(MouseEventArgs e)

    {

      MouseEventArgs et=this.BacktrackMouse(e);

      base.OnMouseDown(et);

    }

 

Testing the control

Final testing of the Canvas control will enable you to zoom, pan and modify the document with the mouse. You don't need to do any strange calculations to ascertain the mouse position so the points returned by the mouse are valid at any zoom level. Storing these points in an array enables us to create a Scribble like application.

Listing 3 shows the complete test program

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

 

namespace TestCanvas

{

      /// <summary>

      /// Summary description for Form1.

      /// </summary>

      public class Form1 : System.Windows.Forms.Form

      {

            //array used to store arrays of points.

            ArrayList items=new ArrayList();

            //The line under construction.

            ArrayList line;

 

            private WellFormed.Canvas canvas1;

            private System.Windows.Forms.TrackBar trackBar1;

            /// <summary>

            /// Required designer variable.

            /// </summary>

            private System.ComponentModel.Container components = null;

 

            public Form1()

            {

                  //

                  // Required for Windows Form Designer support

                  //

                  InitializeComponent();

 

            }

 

 

            /// <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.canvas1 = new WellFormed.Canvas();

                  this.trackBar1 = new System.Windows.Forms.TrackBar();

                  ((System.ComponentModel.ISupportInitialize)(this.trackBar1)).BeginInit();

                  this.SuspendLayout();

                  //

                  // canvas1

                  //

                  this.canvas1.AutoScroll = true;

                  this.canvas1.AutoScrollMinSize = new System.Drawing.Size(1600, 1200);

                  this.canvas1.ClipToPage = true;

                  this.canvas1.Dock = System.Windows.Forms.DockStyle.Fill;

                  this.canvas1.DoubleBuffer = true;

                  this.canvas1.Location = new System.Drawing.Point(0, 0);

                  this.canvas1.Name = "canvas1";

                  this.canvas1.PageColor = System.Drawing.Color.White;

                  this.canvas1.PageSize = new System.Drawing.Size(800, 600);

                  this.canvas1.Size = new System.Drawing.Size(360, 334);

                  this.canvas1.TabIndex = 0;

                  this.canvas1.Text = "canvas1";

                  this.canvas1.Zoom = 2F;

                  this.canvas1.MouseUp += new System.Windows.Forms.MouseEventHandler(this.canvas1_MouseUp);

                  this.canvas1.Paint += new System.Windows.Forms.PaintEventHandler(this.canvas1_Paint);

                  this.canvas1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.canvas1_MouseMove);

                  this.canvas1.MouseDown += new System.Windows.Forms.MouseEventHandler(this.canvas1_MouseDown);

                  //

                  // trackBar1

                  //

                  this.trackBar1.Dock = System.Windows.Forms.DockStyle.Left;

                  this.trackBar1.Location = new System.Drawing.Point(0, 0);

                  this.trackBar1.Maximum = 100;

                  this.trackBar1.Name = "trackBar1";

                  this.trackBar1.Orientation = System.Windows.Forms.Orientation.Vertical;

                  this.trackBar1.Size = new System.Drawing.Size(45, 334);

                  this.trackBar1.TabIndex = 1;

                  this.trackBar1.Scroll += new System.EventHandler(this.trackBar1_Scroll);

                  //

                  // Form1

                  //

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

                  this.ClientSize = new System.Drawing.Size(360, 334);

                  this.Controls.Add(this.trackBar1);

                  this.Controls.Add(this.canvas1);

                  this.Name = "Form1";

                  this.Text = "Form1";

                  ((System.ComponentModel.ISupportInitialize)(this.trackBar1)).EndInit();

                  this.ResumeLayout(false);

 

            }

            #endregion

 

            /// <summary>

            /// The main entry point for the application.

            /// </summary>

            [STAThread]

            static void Main()

            {

                  Application.Run(new Form1());

            }

 

            //a semaphore used to store the state of the mouse button

            bool _drawing;

 

            //Draws the lines in the items array and the one under construction.

            private void canvas1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)

            {

                  foreach(Point[] pa in items)

                  {

                        if(pa.Length>0)

                        {

                              if(pa.Length==1)

                              {

                                    e.Graphics.DrawLine(Pens.Black,pa[0],pa[0]);

                              }

                              else

                                    e.Graphics.DrawLines(Pens.Black,pa);

                        }

                  }

                  if(_drawing)

                  {

                        if(line.Count>1)

                        {

                              Point[] pts=new Point[line.Count];

                              line.CopyTo(pts,0);

                              e.Graphics.DrawLines(Pens.Black,pts);

                        }

                  }

            }

 

            //Sets the scroll according to the trackbar

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

            {

                  this.canvas1.Zoom=0.025f*this.trackBar1.Value;

            }

 

            //Begins drawing a new line

            private void canvas1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)

            {

                  _drawing=true;

                  line=new ArrayList();

            }

 

            //When drawing a line this accumulates points in the line array

            private void canvas1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)

            {

                  if(_drawing)

                  {

                        this.line.Add(new Point(e.X,e.Y));

                        this.canvas1.Invalidate();

                  }

            }

 

            //Ends accumulating a new line by creating a new point array and storing it with the ones already completed

            private void canvas1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)

            {

                  _drawing=false;

                  line.Add(new Point(e.X,e.Y));

                  Point[] pts=new Point[line.Count];

                  line.CopyTo(pts,0);

                  items.Add(pts);

                  this.canvas1.Invalidate();

            }

      }

}

 

Figure 2: The Canvas tester application in action.

This brings us to the end of the first In Depth article. The complete C# Canvas control is available in ZIP form from this link and the test harness from this link

along with it's test harness. This tool will provide you with a re-usable document display and is flexible enough to be used for any application that needs scrolling and zooming mixed with user input.

Return to the main index.

Copyright © Bob Powell 2000-.  All rights reserved.