|
|
|
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.

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);
Dim mx As
New Matrix(_zoom, 0, 0, _zoom, 0, 0)
Dim s As
New Size(CInt(Me.ClientSize.Width * (1.0F / _zoom)),
CInt(Me.ClientSize.Height * (1.0F / _zoom)))
If (s.Width > PageSize.Width)
Then
mx.Translate((s.Width / 2) - (_pageSize.Width / 2), 0)
Else
mx.Translate(CSng(Me.AutoScrollPosition.X
* (1.0F / _zoom)), 0)
End If
If (s.Height > PageSize.Height)
Then
mx.Translate(0, (s.Height / 2) - (Me._pageSize.Height
/ 2) + (Me.AutoScrollPosition.Y))
Else
mx.Translate(0, CSng(Me.AutoScrollPosition.Y
* (1.0F / _zoom)))
End If
mx.Invert()
Dim px As
Point() = New Point() {New
Point(e.X, e.Y)}
mx.TransformPoints(px)
Dim et As
New MouseEventArgs(e.Button, e.Clicks, px(0).X,
px(0).Y, e.Delta)
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);
}
Protected Function
BacktrackMouse(ByVal e
As MouseEventArgs) As MouseEventArgs
Dim mx As
New Matrix(_zoom, 0, 0, _zoom, 0, 0)
Dim s As
New Size(CInt(Me.ClientSize.Width * (1.0F / _zoom)),
CInt(Me.ClientSize.Height * (1.0F / _zoom)))
If (s.Width > PageSize.Width)
Then
mx.Translate((s.Width / 2) - (_pageSize.Width / 2), 0)
Else
mx.Translate(CSng(Me.AutoScrollPosition.X
* (1.0F / _zoom)), 0)
End If
If (s.Height > PageSize.Height)
Then
mx.Translate(0, (s.Height / 2) - (Me._pageSize.Height
/ 2) + (Me.AutoScrollPosition.Y))
Else
mx.Translate(0, CSng(Me.AutoScrollPosition.Y
* (1.0F / _zoom)))
End If
mx.Invert()
Dim px As
Point() = New Point() {New
Point(e.X, e.Y)}
mx.TransformPoints(px)
Dim et As
New MouseEventArgs(e.Button, e.Clicks, px(0).X,
px(0).Y, e.Delta)
Return et
End Function
Protected Overrides
Sub OnMouseMove(ByVal
e As MouseEventArgs)
Dim et As
MouseEventArgs = Me.BacktrackMouse(e)
MyBase.OnMouseMove(et)
End Sub
Protected Overrides
Sub OnMouseUp(ByVal
e As MouseEventArgs)
Dim et As
MouseEventArgs = Me.BacktrackMouse(e)
MyBase.OnMouseUp(et)
End Sub
Protected Overrides
Sub OnMouseDown(ByVal
e As MouseEventArgs)
Dim et As
MouseEventArgs = Me.BacktrackMouse(e)
MyBase.OnMouseDown(et)
End Sub
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();
}
}
}
Public
Class Form1
Inherits System.Windows.Forms.Form
Private items As
ArrayList = New ArrayList
Private line As
ArrayList
Private _drawing As
Boolean
#Region " Windows Form Designer generated code "
Public Sub
New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
'Add any initialization after the
InitializeComponent() call
End Sub
'Form overrides dispose to clean up the component
list.
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
'Required by the Windows Form Designer
Private components As
System.ComponentModel.IContainer
'NOTE: The following procedure is required by the
Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents
Canvas1 As WellFormed.Canvas
Friend WithEvents
trackBar1 As System.Windows.Forms.TrackBar
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
Me.Canvas1 = New
WellFormed.Canvas
Me.trackBar1 = New
System.Windows.Forms.TrackBar
CType(Me.trackBar1,
System.ComponentModel.ISupportInitialize).BeginInit()
Me.SuspendLayout()
'
'Canvas1
'
Me.Canvas1.Anchor =
CType((((System.Windows.Forms.AnchorStyles.Top
Or System.Windows.Forms.AnchorStyles.Bottom) _
Or System.Windows.Forms.AnchorStyles.Left) _
Or System.Windows.Forms.AnchorStyles.Right),
System.Windows.Forms.AnchorStyles)
Me.Canvas1.AutoScroll =
True
Me.Canvas1.AutoScrollMinSize =
New System.Drawing.Size(640, 480)
Me.Canvas1.ClipToPage =
True
Me.Canvas1.DoubleBuffer =
True
Me.Canvas1.Location =
New System.Drawing.Point(48, 0)
Me.Canvas1.Name = "Canvas1"
Me.Canvas1.PageColor =
System.Drawing.Color.White
Me.Canvas1.PageSize =
New System.Drawing.Size(640, 480)
Me.Canvas1.Size = New
System.Drawing.Size(368, 264)
Me.Canvas1.TabIndex = 0
Me.Canvas1.Text = "Canvas1"
'
'trackBar1
'
Me.trackBar1.Dock =
System.Windows.Forms.DockStyle.Left
Me.trackBar1.Location =
New System.Drawing.Point(0, 0)
Me.trackBar1.Maximum = 100
Me.trackBar1.Name = "trackBar1"
Me.trackBar1.Orientation =
System.Windows.Forms.Orientation.Vertical
Me.trackBar1.Size = New
System.Drawing.Size(45, 262)
Me.trackBar1.TabIndex = 2
'
'Form1
'
Me.AutoScaleBaseSize =
New System.Drawing.Size(5, 13)
Me.ClientSize = New
System.Drawing.Size(416, 262)
Me.Controls.Add(Me.trackBar1)
Me.Controls.Add(Me.Canvas1)
Me.Name = "Form1"
Me.Text = "Form1"
CType(Me.trackBar1,
System.ComponentModel.ISupportInitialize).EndInit()
Me.ResumeLayout(False)
End Sub
#End Region
Private Sub
Canvas1_MouseDown(ByVal sender
As Object,
ByVal e As
System.Windows.Forms.MouseEventArgs) Handles
Canvas1.MouseDown
_drawing = True
line = New ArrayList
line.Add(New Point(e.X, e.Y))
End Sub
Private Sub
Canvas1_MouseMove(ByVal sender
As Object,
ByVal e As
System.Windows.Forms.MouseEventArgs) Handles
Canvas1.MouseMove
If _drawing Then
Me.line.Add(New
Point(e.X, e.Y))
Me.Canvas1.Invalidate()
End If
End Sub
Private Sub
Canvas1_MouseUp(ByVal sender
As Object,
ByVal e As
System.Windows.Forms.MouseEventArgs) Handles
Canvas1.MouseUp
_drawing = False
line.Add(New Point(e.X, e.Y))
Dim pts As
Point() = New Point(line.Count - 1) {}
line.CopyTo(pts, 0)
items.Add(pts)
Me.Canvas1.Invalidate()
End Sub
Private Sub
Canvas1_Paint(ByVal sender
As Object,
ByVal e As
System.Windows.Forms.PaintEventArgs) Handles
Canvas1.Paint
Dim pa As
Point()
For Each pa
In items
If pa.Length > 0 Then
If pa.Length = 1 Then
e.Graphics.DrawLine(Pens.Black, pa(0), pa(0))
Else
e.Graphics.DrawLines(Pens.Black, pa)
End If
End If
Next
If _drawing Then
If line.Count > 1 Then
Dim pts As
Point() = New Point(line.Count - 1) {}
line.CopyTo(pts, 0)
e.Graphics.DrawLines(Pens.Black, pts)
End If
End If
End Sub
Private Sub
trackBar1_Scroll(ByVal sender
As System.Object, ByVal
e As System.EventArgs)
Handles trackBar1.Scroll
Canvas1.Zoom = 0.025F * trackBar1.Value
End Sub
End
Class

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
This brings us to the end of the first In Depth article. The
complete VB.Net 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.