|
|
|
Select your preferred language |
A constant requirement of the Windows Forms programmer is to represent a
document or drawn object on screen as it would appear on a printer. This is to
say of course in a WYSIWYG fashion.
Often, the constraints of the screen and the need for the user to do detailed
work require that complex zooming and panning of the document is required. To do
this, a Canvas analog is needed but unfortunately, Windows Forms doesn't provide
this facility as standard and rolling your own can be a daunting task.
In this article you will discover how to create a control that takes
advantage of the best features of GDI+ to make a canvas style window that can
handle infinite zoom levels and scrolling of a virtual document with ease.
The final product, a Canvas class based on the Scrollable Control is shown in
figure 1. Canvas maintains a page size, may be zoomed to any level large or
small and may be scrolled as you would expect. Furthermore, Canvas backtracks
the mouse input so that the position of the mouse on the screen is translated to
the exact virtual coordinates of the mouse in the page.

Figure 1: The Canvas in action.
Getting started.
The canvas class needs to be able to display a virtual page of any size and be
able to intelligently decide which scrollbars should be displayed and where in
the page the viewport is situated.
The ScrollableControl class in Windows Forms is an ideal basis for this kind of
functionality but it lacks the finesse needed to make it perfect. As a first
step the code in the following listing creates a Windows Forms control which can
be dropped onto a form to create a canvas object.
This first incarnation of the canvas control sets the stage for the next by creating
a custom Windows Forms object that obeys some simple ground
rules to make the users experience, and here I refer to the person using the
tool as well as the end user, a consistent and productive one.
The basic class derives from ScrollableControl and uses the
System, System.ComponentModel, System.Drawing, System.Drawing.Drawing2d and
System.Windows.Forms DLL's. In the constructor the control styles are set to
ensure that the painting style is suitable for double buffering if required.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace BobPowellDotNet
{
/// <summary>
/// The canvas class
/// </summary>
public
class Canvas : ScrollableControl
{
/// <summary>
/// Constructor
/// </summary>
public
Canvas()
{
this.SetStyle(
ControlStyles.AllPaintingInWmPaint
|
ControlStyles.UserPaint |
ControlStyles.ResizeRedraw ,true);
}
}
}
Imports System
Imports
System.ComponentModel
Imports System.Drawing
Imports
System.Drawing.Drawing2D
Imports
System.Windows.Forms
Public
Class Canvas
Inherits ScrollableControl
Public
Sub New()
me.SetStyle( _
ControlStyles.AllPaintingInWmPaint or _
ControlStyles.UserPaint or _
ControlStyles.ResizeRedraw ,true)
End
Sub
The fundamental property of the Canvas control is the page size. This provides
the limits for all zooming, scrolling and editing operations. So that the Canvas
control behaves just like a Microsoft produced item, the PageSize property is
constructed as follows. This design guideline may be used on any property to
create a robust and consistent control.
-
A private field is used to store the basic information
-
An accessor property is provided and is supplied with the appropriate attributes
for design time user feedback
-
If required, an event that signals a property change is provided
-
if the event is provided, a protected Onxxx method is provided to raise that
event.
The PageSize property for the Canvas control is constructed as follows.
private
Size _pageSize=new Size(640,480);
[
Category("Appearance"),
Description("The size of the virtual page")
]
public
Size PageSize
{
get{return
_pageSize;}
set{
_pageSize=value;
OnPageSizeChanged(EventArgs.Empty);
}
}
protected
virtual
void OnPageSizeChanged(EventArgs e)
{
if(PageSizeChanged!=null)
PageSizeChanged(this,e);
}
public
event EventHandler PageSizeChanged;
Private _pageSize
As Size =
New
Size(640, 480)
< _
Category("Appearance"), _
Description("The size of the virtual
page") _
> _
Public Property
PageSize() As Size
Get
Return
_pageSize
End
Get
Set(ByVal Value As Size)
_pageSize
= Value
OnPageSizeChanged(EventArgs.Empty)
End
Set
End Property
Protected Overridable
Sub OnPageSizeChanged(ByVal
e As EventArgs)
RaiseEvent
PageSizeChanged(Me, e)
End Sub
Public Event
PageSizeChanged As EventHandler
We will return to the OnPageSizeChanged method shortly.
Initializing the scroll bars.
ScrollableControl enables us to set the minimum scroll size for the page. If the
window becomes smaller than this size, scrollbars will appear and the page may
be scrolled to it's maximum extents.
This is done in the CalcScroll method which will evolve in next part of the article
void
CalcScroll()
{
Size cs =
new Size(this._pageSize.Width,this._pageSize.Height);
this.AutoScrollMinSize=cs;
Invalidate();
}
Protected Sub
CalcScroll()
Dim
cs As New Size(Me._pageSize.Width,
Me._pageSize.Height)
Me.AutoScrollMinSize
= cs
Invalidate()
End Sub
The scroll sizes need to be recalculated in several places. In this article they
will be calculated for a page size change and for a control size change. In the
next issue, the scroll sizes will be calculated when the zoom level changes.
Revisiting the OnPageSizeChanged method, the CalcScroll method is added to the
code.
protected
virtual void
OnPageSizeChanged(EventArgs e)
{
CalcScroll();
if(PageSizeChanged!=null)
PageSizeChanged(this,e);
}
Protected Overridable
Sub OnPageSizeChanged(ByVal
e As EventArgs)
CalcScroll()
RaiseEvent
PageSizeChanged(Me, e)
End Sub
Public Event
PageSizeChanged As EventHandler
The base class OnSizeChanged method is also overridden to call CalcSroll.
protected override
void OnSizeChanged(EventArgs e)
{
CalcScroll();
base.OnSizeChanged(e);
}
Protected Overloads
Overrides Sub
OnSizeChanged(ByVal e As
EventArgs)
CalcScroll()
MyBase.OnSizeChanged(e)
End Sub
Painting the control
To make the appearance of the control professional and provide some flexibility
the control needs a basic page colour property and a flag that will specify
whether drawing will be clipped to the page or allowed to overflow the page
boundaries. The two properties follow.
private Color _pageColor=Color.White;
[
Category("Appearance"),
Description("The base
color of the page")
]
public Color PageColor
{
get{return
_pageColor;}
set{
_pageColor=value;
Invalidate();
}
}
private bool
_clipToPage;
[
Category("Behavior"),
Description("Gets or
sets the clipping flag. When true no drawing is allowed outside page
boundaries")
]
public bool
ClipToPage
{
get{return
_clipToPage;}
set{
_clipToPage=value;
Invalidate();
}
}
Private _pageColor
As Color = Color.White
< _
Category("Appearance"), _
Description("The base color of the
page") _
> _
Public Property
PageColor() As Color
Get
Return
_pageColor
End
Get
Set(ByVal Value As Color)
_pageColor
= Value
Invalidate()
End
Set
End Property
Private _clipToPage
As Boolean
< _
Category("Behavior"), _
Description("Gets or sets the clipping flag.
When true no drawing is allowed outside page boundaries") _
> _
Public Property
ClipToPage() As Boolean
Get
Return
_clipToPage
End
Get
Set(ByVal Value As Boolean)
_clipToPage
= Value
Invalidate()
End
Set
End Property
Drawing of the page must start with drawing of the base page appearance. This
sets the stage for any drawing operations the user should wish to add.
The Paint routine itself follows. An analysis of the method follows the code..
protected
override void
OnPaint(PaintEventArgs e)
{
base.OnPaintBackground(e);
Matrix mx=new Matrix(1,0,0,1,0,0);
Size s=new Size(this.ClientSize.Width,this.ClientSize.Height);
if(s.Width>PageSize.Width)
mx.Translate((s.Width/2)-(_pageSize.Width/2),0);
else
mx.Translate((float)this.AutoScrollPosition.X,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);
e.Graphics.Transform=mx;
SolidBrush b=new SolidBrush(Color.FromArgb(64,Color.Black));
e.Graphics.FillRectangle(b,new Rectangle(new
Point(20,20),PageSize));
b.Color=PageColor;
e.Graphics.FillRectangle(b,new Rectangle(new
Point(0,0),PageSize));
if(ClipToPage)
e.Graphics.SetClip(new
Rectangle(0,0,PageSize.Width,PageSize.Height));
base.OnPaint(e);
}
Protected Overloads
Overrides Sub
OnPaint(ByVal e As
PaintEventArgs)
MyBase.OnPaintBackground(e)
Dim
mx As Matrix = New
Matrix(1, 0, 0, 1, 0, 0)
Dim
s As Size = New
Size(Me.ClientSize.Width, Me.ClientSize.Height)
If
s.Width > PageSize.Width Then
mx.Translate((s.Width
/ 2) - (_pageSize.Width / 2), 0)
Else
mx.Translate(CType(Me.AutoScrollPosition.X,
Single), 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,
CType(Me.AutoScrollPosition.Y,
Single))
End
If
e.Graphics.Transform = mx
Dim
b As SolidBrush = New
SolidBrush(Color.FromArgb(64, Color.Black))
e.Graphics.FillRectangle(b,
New Rectangle(New
Point(20, 20), PageSize))
b.Color = PageColor
e.Graphics.FillRectangle(b,
New Rectangle(New
Point(0, 0), PageSize))
If
(ClipToPage) Then
e.Graphics.SetClip(New
Rectangle(0, 0, PageSize.Width, PageSize.Height))
End
If
MyBase.OnPaint(e)
End Sub
Because the UserPaint style was used, we must explicitly call the
OnPaintBackground method or it will not be cleared.
The paint method sets up a transformation matrix that shifts the origin of the
current Graphics object to the required position.
An identity matrix is created, then the client size is used to determine which,
if any, of the dimensions of the page are larger than the visible client area.
If the width or height of the page is less than the visible client area, the
matrix is translated so that the origin is placed in such a way that the middle
of the page corresponds with the middle of the client area. If the page size is
larger in any dimension, the matrix is transformed by the corresponding
scroll-bar offset.
This matrix is applied to the Graphics object and the page is drawn, first the
shadow, created from an offset rectangle of semi-transparent black, then the
page itself.
If the ClipToPage flag is set, a clipping region is imposed that restricts
further drawing to the visible page boundaries.
Finally, the base OnPaint method and hence the PaintEvent is called giving the
user opportunity to continue painting. At this point, the graphics output may be
sent to the page without worrying about the positions of the scroll bars and
indeed, as you'll see in the next issue, the current zoom level.
Tidying up the control for design time use.
Functionally, the control is complete for this issue. However, a couple of small
items will make it immediately usable and a good experience all round.
This control provides a drawing surface that can be dropped onto a page so it
would be nice if a suitable icon were provided so that it can be recognized in
the toolbox.
Furthermore, the control as it is flickers badly because of the background,
shadow and page drawing so an option to double buffer the page is a must.
To create the icon, a suitable graphic must be added to the project solution.
This simple graphic is a 16 by 16 image. The garish Magenta provides a
transparent background.

Figure 2. The Canvas icon
In the image properties, the Build Action is set to "Embedded Resource"

Figure 3. Embedding the bitmap in the resources
Finally, the class can be adorned with the ToolboxBitmap attribute.
[
ToolboxItem(true),
ToolboxBitmap(typeof(Canvas),"CanvasIcon.bmp")
]
public
class Canvas : ScrollableControl
{
<
_
ToolboxItem(True),
_
ToolboxBitmap(GetType(Canvas),
"CanvasIcon.bmp") _
>
_
Public
Class Canvas
Inherits
ScrollableControl
To cure the flicker of the redraw cycles, the standard control styles are used
to provide a double buffered solution. A property is used to set this up.
private bool _doubleBuffer;
[
Category("Behavior"),
Description("Set true to enable double buffering")
]
public bool
DoubleBuffer
{
get{return
_doubleBuffer;}
set{
_doubleBuffer=value;
if(value)
{
SetStyle(ControlStyles.DoubleBuffer,true);
}
else
{
SetStyle(ControlStyles.DoubleBuffer,false);
}
Invalidate();
}
}
Remember
that these control style changes will accumulate with the ones set up in the
constructor.
Dim _doubleBuffer
As Boolean
<Category("Behavior"), _
Description("Set true to enable double
buffering")> _
Public Property
DoubleBuffer() As Boolean
Get
Return
_doubleBuffer
End
Get
Set(ByVal Value As Boolean)
_doubleBuffer
= Value
If
Value = True Then
SetStyle(ControlStyles.DoubleBuffer, True)
Else
SetStyle(ControlStyles.DoubleBuffer, False)
End
If
End
Set
End Property
Remember that these control style changes will accumulate with the ones set up
in the constructor.
Testing the control
Once built, the control can be dragged onto any form, it's properties set up and
the events such as Paint handled to provide the first stage of the Canvas
control. Figure 4 shows the Canvas control on the design surface and running.

Figure 4. The Canvas Control (Mk I)
Summary.
In this first part, you've seen how to create a graphical control which uses a
transformation matrix to position a virtual page within a visible area and
prepares the drawing surface for subsequent operations.
On the next Page of this article you will see how to adapt this control to cope
with infinitely variable zooming which will allow the user to stand back from a
detailed page or get to grips with the individual pixels.
CS Version
VB Version
Read on...