.
Xamarin
GDI+ FAQ
Skip Navigation LinksWelcome : In Depth articles : Canvas (Part 1)

 

Canvas

A three part series

A constant requirement of the Windows Forms programmer is to represent a document or drawn objecton 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 three part series 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.

The first incarnation of the canvas control sets the stage for the next two articles 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 WellFormed

{

  /// <summary>

  /// The canvas class

  /// </summary>

  public class Canvas : ScrollableControl

  {

    /// <summary>

    /// Constructor

    /// </summary>

    public Canvas()

    {

      this.SetStyle(

        ControlStyles.AllPaintingInWmPaint |

        ControlStyles.UserPaint |

        ControlStyles.ResizeRedraw ,true);

    }

  }

}

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.

  1. A private field is used to store the basic information

  2. An accessor property is provided and is supplied with the appropriate attributes for design time user feedback

  3. If required, an event that signals a property change is provided

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

 

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 the next part of this article.

    void CalcScroll()

    {

      Size cs = new Size(this._pageSize.Width,this._pageSize.Height);

      this.AutoScrollMinSize=cs;

      Invalidate();

    }

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);

    }

The base class OnSizeChanged method is also overridden to call CalcSroll.

    protected override void OnSizeChanged(EventArgs e)

    {

      CalcScroll();

      base.OnSizeChanged(e);

    }

 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();

      }

    }

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);

 

    }

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

  {

 

 

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.

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

In the next part of the 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.

Carry on now to read part 2...

 

Bob Powell

Create your badge

Copyright © Bob Powell 2000-2013.  All rights reserved.