Thursday, October 4, 2012

Drawing Objects in a Canvas with the Paint Event

The Canvas control is a great way to draw pretty much anything to a window. We have been recommending for some time that all Canvas-based drawing be done only from the Paint event (or events called from it). This reduces flicker and increases performance.

I've had many people ask for an example for how to create a Canvas that allows you to:
  • Draw pictures within it (as objects)
  • Move these objects
  • Remove them
  • Add labels to them
  • Programmatically select one
ObjectsInCanvas Example Project
This example demonstrates how to do all these things. It has a large Canvas on the window with several buttons that let you add and manage the objects on the Canvas.

Use the SegmentControl to select the type of object you want to add to the Canvas and click the Add Object button.  You can optionally specify text to label the object using the field below the button.

Add as many objects as you want. You can click to select an object and it will have a black border drawn around it. You can drag any object within the Canvas.

The other buttons center or remove a selected object.  You can use the Select Object # button to select the object number you specify (objects are numbered starting at 0).

And there is no flickering on Windows.

About the Project

The CanvasObject class represents an object. It contains its coordinates, label text, the icon to display and other properties.

The ObjectContainerCanvas class is a subclass of Canvas that is responsible for all the drawing, moving and removing of CanvasObjects. This class has an array of CanvasObjects that it knows about. The objects in this array are drawn by the Canvas Paint event handler when appropriate.

To allow you to drag an object within the Canvas, the MouseDrag event handler is used. It knows what the currently selected CanvasObject is and as you drag the mouse, it updates the coordinates for the CanvasObject.

When the Canvas is told to redraw itself, usually through a call to Invalidate, the Paint event is called. Here each CanvasObject in the array is drawn on the screen.  This is the Paint event:

  If Background <> Nil Then
    g.DrawPicture(Background, 0, 0)
  End If
  For Each co As CanvasObject In mCanvasObjects
    Draw(g, co)

The Draw method draws the image for the CanvasObject at the specified coordinates and if it is selected also draws a border around it. This is the Draw method:

  // Draws a object on the canvas.
  g.DrawPicture(co.Image, co.Left, co.Top)
  If co.Selected Then
    Const kBorder = 2
    // Draw selection
    g.PenHeight = kBorder
    g.PenWidth = kBorder
    g.DrawRect(co.Left-kBorder, co.Top-kBorder, co.Width+kBorder*2, co.Height+kBorder*2)
  End If

In addition, ObjectContainerCanvas has two events that are called when an object is selected (ObjectSelected) and when an object is moved (ObjectMoved).

The ObjectsInCanvas example is included in your Xojo download:

Examples/Graphics and Multimedia/ObjectsInCanvas

You can also check out the other methods on ObjectContainerCanvas that are used to add, move, remove and select objects. I hope you find it useful in your projects.


Bob Keeney said...

This is okay until you get a couple hundred (try thousands of) objects you need to draw onscreen. Performance drops significantly and really the only way to get better is to draw a picture of everything not selected and blast that to the screen and then draw the selected object(s).

Retina display graphics makes this even more difficult because the graphics object in the paint event converts to high resolution automatically. However, your pictures are not automatically set to the correct resolution so you end up having to a) check for retina display and b) make a picture twice as big (if Retina) and drawing your objects in it.

Unknown said...

This is simple. But try implement to scale all those objects. Now THAT is difficult!
If anyone could add that code I would be very happy. :)

Bob Keeney said...

Scaling is easy if you're using Object2D drawing. If you need scaling and/or rotation it's pretty much the only way to go unless you look at 3rd party plugins.