Skip to content

Latest commit

 

History

History
2256 lines (1593 loc) · 78 KB

manual.md

File metadata and controls

2256 lines (1593 loc) · 78 KB

OOFCanvas Manual

This is the manual for OOFCanvas 1.1.1.

Please see the Disclaimer and Copyright notice at the bottom of this document.

OOFCanvas is a replacement for libgnomecanvas, designed for use in OOF2, but hopefully useful elsewhere. OOFCanvas is based on Cairo and is compatible with gtk3. It might eventually also be compatible with gtk+2 or gtk4. It can be called from C++, Python2, or Python3.

OOF2 used libgnomecanvas to display and interact with images and meshes. But libgnomecanvas requires gtk+2, and gtk+2 works only with Python3, not Python2, and Python2 is being phased out. In order to upgrade OOF2 to Python3, we need to first upgrade it from gtk+2 to gtk+3, and to do that we need to first replace libgnomecanvas.

The canvas is a drawing area that can display a variety of shapes, including text. It can be scrolled, zoomed, and printed. It can report which shapes are drawn at a mouse click location.

OOFCanvas is not a drop-in replacement for libgnomecanvas. It's also not a full-fledged gtk widget. It's a set of classes that does some of what libgnomecanvas did and uses gtk.

All of the code is in C++. Wrappers for Python 2 or 3 are generated by SWIG.

Contents

Installation

Prequisites

Before installing OOFCanvas, install the following programs and libraries. If you're using a package manager that packages header files in separate developer packages, install the developer packages. Headers aren't necessary for cmake, SWIG, or pkg-config.

  • A C++ compiler
  • cmake, version 3.18 or later
  • SWIG, version 4.0 or later, and its python module if provided separately
  • pkg-config
  • Python, version 2.7, or 3.8 or later
  • Gtk3, version 3.22.0 or later, but not 4.0 or later
  • PyGObject, version 3.22 or later
  • CairoMM, version 1.12 or later
  • Pango, version 1.40 or later
  • PangoCairo, version 1.40 or later

If you want OOFCanvas to display images loaded by the ImageMagick library, optionally install

  • ImageMagick, version 6.0 or later. (OOFCanvas has not been tested with ImageMagick 7.) It must be ImageMagick, not GraphicsMagick. Make sure to install the C++ header files for Magick++.

If you want to try OOFCanvas's experimental support for numpy and scikit-image images, you will need to also install

but note that OOF2 does not work well if numpy support is enabled in OOFCanvas, at least in OOF2 version 2.3.0

We don't really know the minimum acceptable version numbers for the prerequisites. The listed versions are the ones that we've been able to use and test. It's quite possible that earlier versions will work as well.

For detailed instructions on installing the prerequisites using package managers on various systems are on the OOFCanvas prerequisites page. Those instructions aren't included in this file because they may change and aren't under our control. This file is included in the OOFCanvas distribution and we don't want to create a new version everytime installation instructions need to be updated.

Installing OOFCanvas

After installing the prerequisites, build and install OOFCanvas by following these steps. Lines beginning with '%' should be typed in a terminal window. Type everything after the initial '%'.

  1. Download the latest OOFCanvas tar (.tar.gz) file from

     https://www.ctcms.nist.gov/oof/oofcanvas/
    
  2. Create a working directory. In your home directory or some other convenient location, enter

    % mkdir oofcanvas
    % cd oofcanvas
    
  3. Unpack the tar file. If it's in your Downloads directory, type

     % tar -xf ~/Downloads/oofcanvas-1.1.0.tar.gz
    

    (changing the version number if necessary). This will create a directory called oofcanvas-1.1.0, or something like that.

  4. Create a build directory.

     % mkdir build
     % cd build
    
  5. Configure OOFCanvas by running ccmake:

     % ccmake ../oofcanvas-1.1.0
    
    • Type "c" to start the configuration.

    • Use the up and down arrow keys to move between fields. To change a text field, press return and type a value. You can use the left and right arrow keys to move around within the existing txt. To accept your changes, press return. To discard your changes, press escape. To toggle a boolean (ON/OFF) field, press return. In a field that takes a preset list of values, the left and right arrow keys cycle through the possibilities.

    • Set CMAKE_BUILD_TYPE to Release.

    • Change CMAKE_INSTALL_PREFIX to the location where you want OOFCanvas to be installed. The default value is probably a system directory which you don't have permission to modify. Setting the prefix to your home directory (~) is a good choice. If you're installing into an Anaconda enviroment named OOF2, set CMAKE_INSTALL_PREFIX to ~/Anaconda3/envs/OOF2

    • Set OOFCANVAS_PYTHON_API to Python2 or Python3 if you want to generate the Python interface for OOFCanvas. Set it to None if you don't need Python. Leave it at Python3 if you're using OOFCanvas with OOF2.

    • If you're using Python3, set OOFCANVAS_PYTHON3_VERSION to the Python3 version number by using the right and left arrows to flip through the versions. The default value, Latest, tells cmake to use the latest version it finds. Make sure to choose the same version number that you used when installing the OOFCanvas prerequisites.

      If you have just switched OOFCANVAS_PYTHON_API to Python3, you will have to type "c" before OOFCANVAS_PYTHON3_VERSION appears in the list of settings.

    • Set OOFCANVAS_SWIG_VERSION to the version of SWIG that you have.

    • Set OOFCANVAS_USE_IMAGEMAGICK to ON if you want to be able to use the ImageMagick library to load image files into the canvas.

    • If you want to be able to display images contained in NumPy arrays, set OOFCANVAS_USE_NUMPY to ON. You will first need to enable advanced settings by typing "t". This feature is not currently recommended if you're using OOFCanvas with OOF2.

    • Type "c" again to re-configure.

    • Type "g" to generate the build scripts.

    See the CMake manual for full instructions on how to use ccmake.

  6. Build OOFCanvas:

     % make
    
  7. Install OOFCanvas:

    If CMAKE_INSTALL_PREFIX was set to a system directory, type

     % sudo make install
    

    otherwise type

     % make install
    

    This will create shared libraries called liboofcanvas*.so or liboofcanvas*.dylib in <prefix>/lib, a directory called oofcanvas in <prefix>/lib/pythonX.Y/site-packages (where X.Y is your python version number), a file called oofcanvas.pc in <prefix>/lib/pkgconfig, and a directory called oofcanvas in <prefix>/include.

  8. When building a program that uses OOFCanvas, use the compiler and linker options provide by pkg-config oofcanvas:

     % c++ `pkg-config --cflags oofcanvas` -c myfile.cpp ...
     % c++ `pkg-config --libs oofcanvas` myfile.o ... -o myapp
    

    If you've installed OOFCanvas in a nonstandard location (such as an Anaconda environment or your home directory), you may have to tell pkg-config where to find it by setting the environment variable PKG_CONFIG_PATH, e.g.

     % export PKG_CONFIG_PATH=<prefix>/lib/pkgconfig
    

    where <prefix> is the value of CMAKE_INSTALL_PREFIX you used when configuring OOFCanvas.

  9. When running a program that uses the OOFCanvas python interface, you may need to tell python where to find the OOFCanvas modules, e.g

     % export PYTHONPATH=<prefix>/lib/python3.10/site-packages
    

    if you're using python 3.10.

Uninstalling OOFCanvas

Go to the build directory and run make uninstall. This deletes all the installed files but unfortunately leaves empty directories behind.

Programming with OOFCanvas

All the classes and functions described here are defined in the C++ OOFCanvas namespace. For simplicity we haven't included it explicitly in the discussion below.

Header Files

<prefix>/include/oofcanvas/oofcanvas.h declares all OOFCanvas classes, functions, and constants. If your program uses pkg-config oofcanvas to build, then you can use

#include <oofcanvas.h>   // does not include any gtk code

or

#include <oofcanvasgui.h>  // includes the gtk code as well

in C++, or

import oofcanvas
from oofcanvas import oofcanvasgui

in Python. If you don't include/import the oofcanvasgui components, you can use still use the OffScreenCanvas and save its contents, but can't display it on the screen.

Initialization

When using OOFCanvas in Python, it must be initialized by calling

init_OOFCanvas(threaded)

before any other calls. Calling init_OOFCanvas more than once is harmless. The threaded argument is a boolean value indicating whether or not OOFCanvas will be used in a multithreaded environment.

Class Overview

In general, you create a Canvas object and add it to your Gtk3 user interface (or an OffScreenCanvas if you don't have a GUI). The Canvas contains CanvasLayers, and CanvasLayers contain CanvasItems, such as lines, circles, and text. Items have positions, sizes, and colors, among other attributes. The Canvas can be zoomed and scrolled.

Mouse clicks and motions on the canvas can invoke a callback function.

Coordinate Systems

There are two important coordinates systems: user coordinates and pixel coordinates.

Pixel coordinates measure distance in pixels, with x increasing from left to right and y increasing from top to bottom. The origin is at the upper left corner of the Canvas, which may or may not be visible on the screen. In pixel coordinates, a screen pixel is 1.0 x 1.0 units.

Items drawn on the canvas are specified in user coordinates, which may be anything convenient to the user. x goes from left to right on the screen, and y goes from bottom to top. This is not the convention in many graphics libraries, but is standard in math, physics, and other parts of the real world.

The conversion from user to pixel coordinates depends on the size of the canvas and the current zoom factor, and determines the ppu (pixels per unit). Almost all objects in OOFCanvas are specified in user coordinates, so the user does not need to worry about the pixel coordinate system at all. The one exception is that the sizes of some objects can be specified in pixels.

The Canvas Classes

Three kinds of Canvas objects are defined.

  • OffScreenCanvas is the base class. It can be used to make drawings that will be printed or saved to a file, but not displayed.

  • Canvas is derived from OOFScreenCanvas. It creates a Gtk.Layout which can be used in Gtk3 to put the Canvas in a GUI. It calls user-provided callback functions in response to mouse events.

  • A slightly different Canvas class is available in Python. It's derived in Python from a SWIG-wrapped C++ class called PythonCanvas. The main difference between the C++ and Python Canvas classes is that the Python class expects callback functions to be Python methods, and the GtkLayout is created in Python.

The pixel size of a Canvas or PythonCanvas is determined by the Gtk window that it's part of. The pixel size of an OffScreenCanvas is only computed when it's saved as an image and the size of the image is given.

The CanvasLayer Class

Drawing is done by creating one or more CanvasLayers and adding CanvasItems to them. Opaque items in higher layers obscure the items in lower layers. A newly created layer is always topmost. CanvasLayers can be shown, hidden, and reordered, making it easy to change what's visible on the canvas.

CanvasLayers are created by calling OffScreenCanvas::newLayer() and destroyed by calling either CanvasLayer::destroy() or OffScreenCanvas::deleteLayer().

The CanvasItem Classes

Everything drawn on a Canvas is an instance of a CanvasItem subclass. Pointers to CanvasItems are passed to CanvasLayer::addItem. The CanvasLayer will destroy its CanvasItems when appropriate -- the user should never destroy them explicitly.

Each CanvasItem has a bunch of parameters that determine its position, shape, color, and transparency. Position parameters are always given in user coordinates. Some parameters, such as line widths, can be given in either user or pixel units.

In C++, CanvasItems can be created either by calling their constructors, or calling the subclass's static create method. In Python, only the create method is available, which ensures that ownership of the object remains in C++, and that Python's garbage collector will not delete an object. The arguments to the create method are always the same as the arguments to the constructor.

Details of each CanvasItem subclass are given somewhere below.

The Mouse

The Canvas's setMouseCallback method installs a mouse event handler, which will be called whenever a mouse button is pressed or released, the mouse is moved, or the scroll wheel is turned.

Call Canvas::setMouseCallback(MouseCallback callback, void *data) to install a mouse event handler. callback will be called whenever a mouse button is pressed, the mouse is moved, or the window is scrolled.

To install a rubberband that will be displayed when the mouse is moving, call Canvas::setRubberBand(RubberBand*) from the callback for the mouse-down event. The various types of RubberBand and details of how to use them are described in the section on the RubberBand class, below. To stop displaying the RubberBand, pass a null pointer (in C++) or None (in Python) to setRubberBand().

OOFCanvas does not handle selection of objects with the mouse, but it does provide the position of a mouse click as part of the data passed to the callback function. Additionally, it is possible to get a list of all CanvasItems at a point with OffScreenCanvas::clickedItems(const Coord&).

Scrolling

A canvas can be scrolled in one of two ways. It can be connected to GtkScrollBars or other widgets elsewhere in the GUI, and it can respond to scroll events generated within the GtkLayout.

To connect to scroll bars, call scrollbar.set_adjustment(adj) (in Python) or gtk_range_set_adjustment(scrollbar, adj) (in C++), where adj is the GtkAdjustment returned by Canvas::getHAdjustment() or Canvas::getVAdjustment().

If the GtkLayout receives a scroll event, the mousehandler is called with event set to scroll. The x and y values are the changes in position, and can be used to modify the adjustments of the scroll bars:

def mouseCB(eventtype, x, y, button, shift, ctrl, data):
	if eventtype == "scroll":
		sx = horizontalScrollBar.get_adjustment().get_value()
		horizontalScrollBar.get_adjustment().set_value(sx + x)
		...

A Simple Example

In C++

#include "oofcanvas/guicanvas.h" // gui-dependent classes (Canvas, Rubberband)
#include "oofcanvas/oofcanvas.h" // everything else

double ppu; // pixels per unit -- initialize to something sensible
// Create a Canvas
Canvas canvas(ppu);
// Get a pointer to the GtkLayout widget
GtkWidget *widget = canvas.gtk(); 

// Install the canvas in the gui.  For example, if it's going into
// a GtkFrame,
frame.add(widget);

// Create a canvas layer
CanvasLayer *layer = canvas.newLayer("layername");

// Add items to the layer
double x=1., y=2., radius=1.4;
CanvasCircle *circle = new CanvasCircle(x, y, radius); // In user coordinates.
circle->setLineWidthInPixels(1.5); // In pixel units
Color orange(1., 0.7, 0.0, 0.5); // r, g, b, a, all in [0.0, 1.0]
circle.setFillColor(orange);
layer->addItem(circle);

// Add more items if you want
...

// Draw the items to the canvas
canvas.draw();

The equivalent Python is virtually identical

import oofcanvas
from oofcanvas import oofcanvasgui

oofcanvas.init_OOFCanvas(False)

canvas = oofcanvasgui.Canvas(width=300, height=300, ppu=1.0,
                             vexpand=True, hexpand=True)
frame.add(canvas.layout)

layer = oofcanvas.CanvasLayer("layername")

x = 1.
y = 2.
radius = 1.4
circle = oofcanvas.CanvasCircle.create(x, y, radius)
circle.setLineWidthInPixels(1.5)
orange = oofcanvas.Color(1., 0.7, 0.0).opacity(0.5)
circle.setFillColor(orange)
layer.addItem(circle)

canvas.draw()

Calling Canvas::draw doesn't actually draw anything. Instead, it generates a Gtk event that causes the real drawing method to be called from the Gtk main loop.

Details of the Classes

This section contains detailed information about all of the externally visible classes in OOFCanvas, starting with the utility classes that are used by the rest of the code.

Utility Types

These classes are defined in the OOFCanvas namespace and are used for some arguments and return values by the main OOFCanvas methods.

Coord

Coord is a position in user coordinates, the coordinate system in which CanvasItems are defined.

The Coord class is defined in C++, but not in Python. Methods that return a position to Python simply return a tuple, (x,y). When an OOFCanvas function in Python requires a position argument, any type that can be indexed can be used. That is, if you have a coordinate class called MyCoord, you can do this:

pt = MyCoord(x, y)
circle = oofcanvas.CanvasCircle(pt, 1.0)

as long as pt[0] is x and pt[1] is y. When an OOFCanvas function returns a Coord, it's really returning a tuple, so you can do this:

pt = MyCoord( * oofcanvas.someFunctionReturningACoord() )

Whenever a C++ function described below returns a Coord, assume that the Python version works as described above.

The Coord constructors are

  • Coord()

    creates a point at the origin.

  • Coord(double x, double y)

    creates a point at (x,y).

The components can be accessed via the x and y data members or via indexing. coord.x == coord[0].

Basic arithmetic, assignment, and equality operations are supported. Coord::norm2() returns the square of the L2 norm. Coord::operator*(const Coord&) is the dot product, and cross(const Coord&, const Coord&) is the cross product.

ICoord

An ICoord is a Coord with integer coefficients, used to identify pixels.

Rectangle

The Rectangle class is not the same as the CanvasRectangle, described below. CanvasRectangle is a CanvasItem that can be displayed. Rectangle is just a region of space.

A Rectangle can be constructed in several ways:

  • Rectangle() creates an empty uninitialized rectangle at an undefined position.
  • Rectangle(double x0, double y0, double x1, double y1) creates a rectangle with diagonally opposite corners at (x0, y0) and (x1, y1). It doesn't matter which pair of diagonally opposite corners are given.
  • Rectangle(const Coord &pt0, const Coord &pt1) does the same, with Coords instead of doubles.
  • From Python, only Rectangle((x0, y0), (x1, y1)) is available.

Useful methods are

  • double Rectangle::xmin() const

  • double Rectangle::ymin() const

  • double Rectangle::xmax() const

  • double Rectangle::ymax() const

  • Rectangle::swallow(const Coord&) expands the rectangle to include the given point.

    If the rectangle was uninitialized, this initializes it to an rectangle of size 0 at the given point. That is, Rectangle r; r.swallow(pt); is the same as Rectangle r(pt, pt); for some Coord pt.

  • Rectangle::swallow(const Rectangle&) expands the rectangle include the given Rectangle.

Color

Colors are stored as RGBA values, which are doubles between 0 and 1.

C++ Constructors:

  • Color()

    Initializes to black.

  • Color(double r, double g, double b)

    Alpha is 1 (opaque).

  • Color(double r, double g, double b, double a)

The only Python constructor is

  • Color(r, g, b)

To change the opacity of a Python color, use

  • Color Color::opacity(alpha)

which returns a new Color with the given opacity and the same RGB values.

Predefined constants are defined for black, white, red, green, blue, gray, yellow, magenta, and cyan.

Canvas Classes

OffScreenCanvas

OffScreenCanvas is the base class for the other Canvas classes. As the name implies, it can't be displayed on the screen, but it can be drawn to and the resulting image can be saved to a file.

OffScreenCanvas exists in both C++ and Python. The discussion below uses C++ syntax, but the translation to Python is trivial, except that (a) Coords are handled as discussed above, and (b) the methods that return a std::vector in C++ return a list in Python.

The constructor is

  • OffScreenCanvas(double ppu)

    ppu is the pixels per unit that determines the conversion between user and pixel coordinates. This is just an initial value. It can be changed later by zooming, but some nonzero initial value is required.

Layer manipulation methods in OffScreenCanvas
  • CanvasLayer* OffScreenCanvas::newLayer(const std::string& name)

    creates a new CanvasLayer with the given name. All layers should be created with this method. The name is just for convenience and debugging. It need not be unique unless OffScreenCanvas::getLayer will be used to retrieve layers by name. If OOFCanvas is built in debug mode, a warning will be printed when a non-unique name is used.

  • void OffScreenCanvas::deleteLayer(CanvasLayer *layer)

    deletes a canvas layer from the Canvas and destroys it. Do not simply delete a layer with delete layer;

  • void OffScreenCanvas::clear()

    deletes all layers.

  • CanvasLayer* OffScreenCanvas::getLayer(int) const

    gets a particular layer from the stack. Layer 0 is the bottom layer.

  • Canvaslayer* OffScreenCanvas::getLayer(const std::string &name) const

    gets a layer by name. The Python equivalent is OffScreenCanvas.getLayerByName(name). If you're going to use this, make sure that your layers have unique names.

  • std::size_t OffScreenCanvas::nLayers() const

    returns the total number of layers.

  • std::size_t OffScreenCanvas::nVisibleItems() const

    returns the total number of visible items drawn on all layers.

  • void OffScreenCanvas::raiseLayer(int n, int howfar)

    raises layer n by howfar places in the layer list. A higher layer may hide the contents of a lower layer. If n is too large, the layer will just be moved to the top.

  • void OffScreenCanvas::lowerLayer(int n, int howfar)

    lowers layer n by howfar places in the layer list. If n is too large, the layer will just be moved to the bottom.

  • void OffScreenCanvas::raiseLayerToTop(int n)

    moves layer n to the top of the layer list.

  • void OffScreenCanvas::lowerLayerToBottom(int n)

    moves layer n to the bottom of the layer list.

  • void OffScreenCanvas::reorderLayers(const std::vector<CanvasLayer*>* layerlist)

    puts the layers in the order given in layerlist. The list must contain all of the layers currently in the Canvas and no more. In Python the argument is a list [] of layers.

Output methods in OffScreenCanvas
  • bool OffScreenCanvas::saveAsPDF(const std::string& filename, int maxpix, bool bg)

    saves the entire contents of the Canvas to a pdf file with the given name.

    • Although the output should be independent of the pixel resolution, it's still necessary to pretend that there is a pixel size so that OOFCanvas can compute line thicknesses and other quantities that might be specified in pixel units. maxpix is the number of pixels to assume in the largest dimension of the image.

    • If bg is true the background will be drawn. Otherwise it will be left blank.

    • The return value is true if something was drawn successfully.

  • bool OffScreenCanvas::saveAsPNG(...)

    is the same as saveAsPDF(...) but writes a PNG file.

  • bool OffScreenCanvas::saveRegionAsPDF(const std::string& filename, int maxpix, bool bg, const Coord& pt0, const Coord& pt1)

    saves the rectangle defined by pt0 and pt1 to the given file. maxpix and bg are the same as in saveAsPDF. In the Python version, pt0[0] is the x coordinate of a corner, and pt0[1] is the y coordinate.

  • bool OffScreenCanvas::saveRegionAsPNG(...)

    is the same as saveRegionAsPDF(...) but writes a PNG file.

Miscellaneous methods in OffScreenCanvas
  • double OffScreenCanvas::getPixelsPerUnit() const

    returns the current scale factor.

  • Coord OffScreenCanvas::pixel2user(const ICoord&) const

    converts a pixel coordinate to a user coordinate. The Python equivalent is OffScreenCanvas.pixel2user(x,y), which returns a 2-tuple.

  • void OffScreenCanvas::setAntialias(bool)

    turns anti-aliasing on and off. The default value depends on your device.

  • void OffScreenCanvas::setMargin(double)

    sets the size of the margin around the items on the canvas. The size of the canvas is 1+margin times the width and height of the bounding box of its contents. The default value is 0.0.

  • bool OffScreenCanvas::empty() const

    has anything been drawn?

  • OffScreenCanvas::setBackgroundColor(const Color&)

    sets the color of the parts of the canvas where nothing has been drawn.

  • std::vector<CanvasItem*> OffScreenCanvas::clickedItems(const Coord&)

    returns a list of the CanvasItems at the given point, if the items are in clickable CanvasLayer.

  • std::vector<CanvasItem*> OffScreenCanvas::allItems() const

    returns a list all CanvasItems on the Canvas, in all CanvasLayers.

  • void OffScreenCanvas::datadump(const std::string&) const

    writes a text representation of the contents of each canvas layer to a file with the given name. This can be useful for debugging.

Canvas (C++)

Canvas is the C++ class that actually draws to the screen. It is derived from OffScreenCanvas, and it creates a GtkLayout when it is constructed. The GtkLayout should be inserted into the application's GUI.

The Canvas constructor is

Canvas::Canvas(double ppu)

ppu is the initial value to use for the pixels per unit scale factor when the canvas is empty. A new value will be computed if you call Canvas::zoomToFill() after adding some CanvasItems, so the initial ppu is nearly irrelevant.

All of the methods defined in OffScreenCanvas are available in Canvas. In addition, Canvas defines:

  • GtkWidget *Canvas::gtk() const

    returns a pointer to the Canvas's GtkLayout.

  • void Canvas::destroy()

    destroys the GtkLayout. This is called automatically by the Canvas destructor, but it can be called manually if necessary for some reason. Don't try to use the Canvas after destroying it.

  • void Canvas::show()

    calls gtk_widget_show on the Canvas's GtkLayout.

  • void Canvas::draw()

    instructs the Canvas to draw all of its CanvasItems.

  • int Canvas::widgetWidth() const

    returns the width of the space allocated in the GUI for the GtkLayout.

  • int Canvas::widgetHeight() const

    returns the height of the space allocated in the GUI for the GtkLayout.

  • void Canvas::zoom(double factor)

    zooms the canvas by the specified factor, keeping the center point fixed.

  • void Canvas::zoomAbout(const Coord& fixedpt, double factor)

    zooms the canvas by the specified factor, keeping the given point fixed. The point is specified in user coordinates.

  • void Canvas::zoomToFill()

    zooms the canvas so that all CanvasItems are visible and as large as possible.

  • void Canvas::center()

    scrolls the canvas so that the center of bounding box of all CanvasItems is centered on the Canvas, without zooming.

  • Rectangle Canvas::visibleRegion() const

    returns a Rectangle giving the user space coordinates of the visible part of the Canvas.

  • GtkAdjustment* Canvas::getHAdjustment() const

    returns the GtkAdjustment that controls horizontal position of the canvas. Connecting this object to a GtkScrollBar allows the canvas to be scrolled by the user.

  • GtkAdjustment* Canvas::getVAdjustment() const

    is the same, for the vertical position of the canvas.

  • void Canvas::setMouseCallback(MouseCallback, void *data)

    assigns a mouse click callback function, which will be called when the mouse button is pressed or released, the mouse is moved, or the scroll wheel is scrolled. To limit the proliferation of motion events, see the Canvas::allowMotionEvents function.

    The signature of the callback function is

      typedef void (*MouseCallback)(const std::string &event, 
                                    const Coord &position,
                                    int button,
                                    bool shift, bool ctrl,
                                    void *data);
    

    The following arguments are passed to the callback:

    • const std:string& eventtype

      The types are "down" (button was pressed), "up" (button was released), "move" (mouse was moved), and "scroll" (scroll wheel was turned).

    • const Coord& position

      The position of the mouse event, in user coordinates.

    • int button

      Which mouse button was used.

    • bool shift

      Whether or not the shift key was pressed.

    • bool ctrl

      Whether or not the control key was pressed.

    • void *data

      The data pointer that was passed to setMouseCallback.

  • void Canvas::removeMouseCallback()

    removes the mouse callback function.

  • MotionAllowed Canvas::allowMotionEvents(MotionAllowed ma)

    tells the canvas how to respond when the mouse moves, if a mouse callback function is installed. The values of ma in C++ are

    • MotionAllowed::NEVER: don't call the callback when the mouse moves.
    • MotionAllowed::ALWAYS: call the callback whenever the mouse moves.
    • MotionAllowed::MOUSEDOWN: call the callback when the mouse moves only if a mouse button is pressed.

    The default value is MotionAllowed::NEVER, so you must explicitly allow motion events if you want them.

    allowMotionEvents() returns the previous state of the motion handler, in case you want to restore it afterwards.

  • void Canvas::setRubberBand(RubberBand*)

    tells the canvas to start using the RubberBand object for displaying mouse motions. See RubberBand for details.

  • void Canvas::removeRubberBand()

    tells the canvas to stop using the rubberband.

  • void Canvas::setResizeCallback(ResizeCallback, void *data)

    specifies a function to call when the canvas size changes. The function must take a single void* argument, and return void. When called, the given data is passed.

Canvas (Python)

This is the Canvas class that available in Python. It is derived from a SWIG generated wrapper around a C++ class called PythonCanvas, which is derived from OffScreenCanvas.

The Python Canvas creates a GtkLayout using Gtk's Python interface. The Gtk widget can be accessed directly via Canvas.layout.

The constructor is

Canvas(width, height, ppu, **kwargs)

where width and height are the desired size of the GtkLayout, in pixels. ppu is the initial pixels per unit value. Any additional keyword arguments in kwargs are passed to the GtkLayout constructor.

All of the methods available in OffScreenCanvas and in the C++ Canvas are also available in the Python Canvas, so refer to those sections for the details.

In Python, the Canvas methods that set callback functions expect the callbacks to be Python functions, but are otherwise just like the C++ functions:

  • Canvas.setMouseCallback(callback, data)

    installs a mouse callback function. This is identical to the callback function in the C++ version, except

    • It's a Python function, not a C++ function.
    • The position argument is a tuple, not a Coord.
    • The data is a Python object, not a void*.
  • Canvas.allowMotionEvents(ma)

    is like the C++ version, telling the Canvas how to respond when the mouse moves, except that in C++ the argument is one of the constants motionAlways, motionNever, or motionMouseDown.

  • Canvas.setResizeCallback(callback, data)

    Again, this is just like the C++ version, except the callback function is a Python function and data is a Python object.

CanvasLayer

CanvasLayers hold sets of CanvasItems, which are the things that are drawn on the Canvas. Layers may be raised, lowered, shown, and hidden.

Layers should only be created by a Canvas or OffScreenCanvas, using its newLayer() method.

CanvasLayer methods include:

  • void CanvasLayer::clear()

    removes all objects from the layer and makes it transparent.

  • void CanvasLayer::clear(const Color&)

    removes all objects from the layer and fills it with the given Color.

  • void CanvasLayer::addItem(CanvasItem*)

    adds the given item to the layer. The layer owns the item. After it's been added to the layer it should not be deleted except by clearing or destroying the layer.

  • void CanvasLayer::removeAllItems()

  • bool CanvasLayer::empty() const

    returns true if the layer contains no CanvasItems.

  • void CanvasLayer::destroy()

    destroys the layer and removes it from the Canvas.

  • void CanvasLayer::show()

    makes the layer visible if it was previously hidden.  New layers
    are initially visible.
    
  • void CanvasLayer::hide()

    make the layer invisible.

  • void CanvasLayer::setClickable(bool)

    If the argument is true, objects in the layer can be listed by OffScreenCanvas::clickedItems().

  • void CanvasLayer::markDirty()

    Force the layer to be redrawn the next time the Canvas is rendered. Normally this shouldn't be needed. Adding or removing a CanvasItem from a CanvasLayer makes it dirty.

  • void CanvasLayer::setOpacity(double)

    sets the opacity with which the layer will be copied to the Canvas when it's displayed. 0.0 is fully transparent and 1.0 is fully opaque.

  • void CanvasLayer::raiseBy(int howfar) const

    raises the layer in the Canvas by the given amount. This is the same as OffScreenCanvas::raiseLayer(int n, int howfar) except that you don't need to know the layer number n.

  • void CanvasLayer::lowerBy(int howfar) const

    is the same as raiseBy, but in the other direction.

  • void CanvasLayer::raiseToTop() const

    is the same as OffScreenCanvas::raiseLayerToTop(int n).

  • void CanvasLayer::lowerToBottom() const

    is the same as OffScreenCanvas::lowerLayerToBottom(int n).

  • void CanvasLayer::writeToPNG(const std::string& filename)

    saves the contents of the layer to a PNG file.

CanvasItem

CanvasItem is the abstract base class for everything that can be drawn on the canvas. Generally you get a pointer to a new CanvasItem, call its methods to set its properties, and pass the pointer to CanvasLayer::addItem().

In C++, always allocate new CanvasItems with new or use the class's static create method. In Python, always use the create method. The arguments to create are always the same as the arguments to the constructor.

This is incorrect:

CanvasCircle circ(Coord(0.,0.), 1.0);
layer->addItem(&circ);

but this is correct:

CanvasCircle *circ1 = new CanvasCircle(Coord(0.,0.), 1.0);
layer->addItem(circ1);
CanvasCircle *circ2 = CanvasCircle::create(Coord(0.,0.), 1.);
layer->addItem(circ2);

In Python, don't do this:

circ = oofcanvas.CanvasCircle((0.,0.), 1.)
layer.addItem(circ)

Do this instead:

circ = oofcanvas.CanvasCircle.create((0.,0.), 1.)
layer.addItem(circ)

After an item has been added to a layer, the layer owns it. The item will be deleted when it is removed from the layer or when the layer is deleted.

CanvasItem defines the following method:

  • bool CanvasItem::containsPoint(const OffScreenCanvas *canvas, const Coord &point) const

    returns true if the given point is within the item on the given canvas.

Abstract CanvasItem Subclasses

CanvasShape

This is an abstract base class for most other CanvasItem classes. It describes an object that can be drawn with a line, but not necessarily filled. The default line color is black. There is no default line width. If the line width is not set, nothing will be drawn.

CanvasShape defines the following methods:

  • void CanvasShape::setLineWidth(double)

    Set the line width in user units.

  • void CanvasShape::setLineWidthInPixels(double)

    Set the line width in pixel units.

  • double CanvasShape::getLineWidth() const

    returns the numerical value of the line width, in pixels or user units.

  • bool CanvasShape::getLineWidthInPixels() const

    returns true if the line width should be interpreted in pixel units.

  • void CanvasShape::setLineJoin(Cairo::LineJoin)

    This determines how line segments are joined. In C++, the argument is a member of the LineJoin enum class:

    • LineJoin::MITER
    • LineJoin::ROUND, or
    • LineJoin::BEVEL,

    equivalent to the members of the Cairo::LineJoin class.

    In Python, the choices are lineJoinMiter, 1ineJoinRound, or lineJoinBevel, which are defined in the OOFCanvas namespace.

  • void CanvasShape::setLineCap(LineCap)

    This determines how the ends of line segments are drawn. In C++, the argument is a member of the LineCap enum class:

    • LineCap::BUTT
    • LineCap::ROUND, and
    • LineCap::BSQUARE,

    equivalent to the members of the Cairo::LineCap class.

    In Python, the choices are lineCapButt, lineCapRound, or lineCapSquare, which are defined in the OOFCanvas namespace.

  • void CanvasShape::setLineColor(const Color&)

    Sets the line color. See Color. The default color is black.

By default, lines are solid. They can be made dashed by calling one of the following methods:

  • void CanvasShape::setDash(const std::vector<double>&, int offset)

    The vector contains a pattern of dash lengths, which are in user units. The pattern repeats as necessary. offset indicates where the pattern starts. In Python, pass a list of doubles for the dash lengths.

  • void CanvasShape::setDashInPixels(const std::vector<double>&, int offset)

    The same as the above setDash, but the dash lengths are interpreted in pixel units.

  • void CanvasShape::setDash(double)

    Use a single dash length, which is in user units.

  • void CanvasShape::setDashInPixels(double)

    The same as setDash(double), but the dash lengths are in pixel units.

  • void CanvasShape::setDashColor(const Color&)

    Fill the spaces between dashes with the given Color instead of leaving them blank.

  • void CanvasShape::unsetDashes()

    Turn off dashes. Draw solid lines.

CanvasFillableShape

This abstract class is derived from CanvasShape and is used for closed shapes that can be filled with a color. It provides one method:

  • void CanvasFillableShape::setFillColor(const Color&)

    Fill the shape with the given color.

Concrete CanvasItem Subclasses

These are the actual items that can be drawn, in alphabetical order.

CanvasArrowhead

An arrowhead can be placed on a CanvasSegment. The CanvasArrowhead class is not derived from CanvasShape. Its constructor is

  • CanvasArrowHead(const CanvasSegment *segment, double position, bool reversed)

    segment is the CanvasSegment that the arrowhead will be drawn on. `

    position ranges from 0.0 to 1.0, and determines where the tip of the arrow will appear on the segment. A value of 0.0 puts the tip at the first point of the segment, and a value of 1.0 puts it at the second point. The color of the arrowhead is the same as the line color of the CanvasSegment. If reversed is true, then the arrow points toward the first point of the segment. (If position is 0.0, you probably want reversed==true, but that is not enforced.)

The size of the arrowhead is set by either

  • void CanvasArrowHead::setSize(double width, double length)

    width and length are in user units.

or

  • void CanvasArrowHead::setSizeInPixels(double width, double length)

    width and length are in pixels.

Either setSize() or setSizeInPixels() must be called before an arrowhead can be drawn.

CanvasCircle

Derived from CanvasFillableShape. Its constructor is

  • CanvasCircle(const Coord &center, double radius)

The coordinates of the center and the radius are in user units. To specify the radius in pixels, use CanvasDot instead.

CanvasCurve

A CanvasCurve is a set of line segments connected end to end. It is derived from CanvasShape. It is specified by listing the sequence of Coords joined by the segments. Its constructors are

  • CanvasCurve()

    Create an empty curve, containing no points. This form of the constructor is the only one available in Python.

  • CanvasCurve(int n)

    Create a curve with room reserved for n points, but don't actually create the points.

  • CanvasCurve(const std::vector<Coord> &points)

    Create a curve with the given points.

Points can be added to a CanvasCurve via

  • void CanvasCurve::addPoint(const Coord&)

or

  • void CanvasCurve::addPoints(const std::vector<Coord>*)

In Python, the argument to addPoints is a list of Coord-like (ie, indexable) objects.

int CanvasCurve::size() returns the number of points in the curve.

CanvasDot

Derived from CanvasFillableShape, a CanvasDot is a circle with a fixed size in pixels. Its line width is also always measured in pixels. The constructor is

  • CanvasDot(const Coord &center, double radius)
CanvasEllipse

Derived from CanvasFillableShape. The constructor is

  • CanvasEllipse(const Coord &c, const Coord &r, double angle)

where c is the center in user coordinates and the components of r are the radii in user units. r[0] is the radius in the x direction before rotation. The rotation angle in degrees is measured counterclockwise.

CanvasImage

CanvasImage can display a PNG file, or if compiled with the ImageMagick library, any file format that ImageMagick can read. It can also use an image already loaded by ImageMagick. To enable ImageMagick, define OOFCANVAS_USE_IMAGEMAGICK when building OOFCanvas.

If OOFCanvas is built with the OOFCANVAS_USE_NUMPY and PYTHON_API options, then it can display image data stored in a NumPy array, such as one created by scikit-image.

The constructor creates an empty image:

  • CanvasImage(const Coord &position, const ICooord &npixels)

where position is the position of the lower left corner of the image in user coordinates.

Confusion Opportunity! There are two kinds of pixels. There are the pixels on your computer screen, and there are the pixels in the CanvasImage. They don't have to be the same size. A CanvasImage may be displayed at a different scale from its natural size, in which case one CanvasImage pixel will be larger or smaller than one screen pixel.

Since an empty image isn't very useful, CanvasImage includes some static factory methods for creating CanvasImage objects.

  • Create a blank image:

     CanvasImage* CanvasImage::newBlankImage(
            const Coord& position,
            const ICoord& pixelsize,
            const Color &color)

    The image is filled with a single color, color, so it's not really blank. position is the user coordinate of the lower left corner of the image. size is the size that it will be drawn, in user units. pixelsize is the size of the image in pixels.

  • Read a png file:

     CanvasImage* CanvasImage::newFromPNGfile(
     	   const Coord& position,
     	   const std::string& filename)
    

    position is the position of the lower left corner of the image in user coordinates.

  • Read any file format that ImageMagick can handle:

     CanvasImage* CanvasImage::newFromImageMagickFile(
     	   const Coord& position,
     	   const std::string& filename)

    position is the position of the lower left corner of the image in user coordinates.

  • Create a CanvasImage from ImageMagick data:

     CanvasImage* CanvasImage::newFromImageMagick(
     	const Coord& position,
     	Magick::Image imagedata)

    Create a CanvasImage from image data that has already been read by ImageMagick. The data is copied from the ImageMagick structure.

  • Create a CanvasImage from NumPy data:

    CanvasImage* CanvasImage::newFromNumpy(
        const Coord& position,
        PyObject *numpyarray,
        bool flipy)

    numpyarray must contain RGB or RGBA data. The values be unsigned bytes (chars) in the range 0-255, or doubles in the range 0.0-1.0. If flipy is true, the order of the rows in the image will be reversed.

CanvasImage provides the following useful methods:

  • Set the displayed size of the image, in user coordinates:

    void CanvasImage::setSize(const Coord&)

    or in pixel (screen) coordinates:

    void CanvasImage::setSizeInPixels(const Coord&)

    Either setSize or setSizeInPixels must be called before an image can be displayed. See the note above about pixels: this size refers to screen pixels, not image pixels.

  • Set the style for drawing individual pixels

    CanvasImage::setDrawIndividualPixels(flag)

    Cairo draws pixels as small fuzzy blobs, which may or may not be what you want, especially if you need to zoom in. When examining data on the image pixel level (not the screen pixel level) it can be convenient to draw each pixel as a rectangle. Call setDrawIndividualPixels(true) to switch to this mode, or setDrawIndividualPixels(false) to turn it off.

  • Examine individual pixels

    Color CanvasImage::get(const ICoord &) const

    This returns the color of the pixel at the given point in the image. The ICoord is the location of the pixel in the image, not the canvas. As such, it uses standard image coordinates, with x increasing from left to right and y increasing from top to bottom.

  • Modify individual pixels

    void CanvasImage::set(const ICoord&, const Color&)

    The ICoord is the location of the pixel in the image, not the canvas. As such, it uses standard image coordinates, with x increasing from left to right and y increasing from top to bottom.

    If you need to make extensive modifications to an image, it's probably better to use some other tools first and then load the modified image into the CanvasImage.

  • Set overall opacity

    void CanvasImage::setOpacity(double alpha)

    This sets the opacity for the entire image, used when it is copied to the Canvas. It doesn't actually change any image data.

CanvasPolygon

A CanvasPolygon is a closed CanvasCurve, derived from CanvasFillableShape. It is specified by listing the user coordinates of the corners of the polygon, counterclockwise. Its constructors are

  • CanvasPolygon()

    Create an empty polygon, containing no points. Only this constructor is available in Python.

  • CanvasPolygon(int n)

    Create a polygon with room for n points, but don't actually create the points. Points must be added with addPoints.

  • CanvasPolygon(const std::vector<Coord>& points)

    Create a polygon from the given vector of Coords.

Points must be added to a polygon in order, clockwise. When drawn, the last point will be connected to the first. There is currently no mechanism for inserting points in the middle of the sequence, or for deleting them.

To add points to a polygon, in C++ use either

  • CanvasPolygon::addPoint(const Coord&)
  • CanvasPolygon::addPoint(const Coord*)

or

  • CanvasPolygon::addPoints(const std::vector<Coord>*)

In Python, use

  • CanvasPolygon.addCoord(pt)

    where pt is some kind of point object, with pt[0] being x and pt[1] being y.

or

  • CanvasPolygon::addPoints(ptlist)

    where ptlist is a list of point objects pt, where pt[0] is x and pt[1] is y.

CanvasRectangle

Derived from CanvasFillableShape. The constructor is

  • CanvasRectangle(const Coord&, const Coord&)

where the Coords are the user coordinates of any two opposite corners of the rectangle.

CanvasSegment

A single line segment, derived from CanvasShape. The constructor is

  • CanvasSegment(const Coord &point0, const Coord &point1)

The positions are given in user coordinates.

CanvasSegments

CanvasSegments is derived from CanvasShape and draws a set of unconnected line segments all with the same color and width.

The constructors are

  • CanvasSegments()

    creates an empty object.

  • CanvasSegments(int n)

    allocates space for n segments, but doesn't create them. This form is only available in C++.

To add segments to the object, use

  • CanvasSegments::addSegment(const Coord &pt0, const Coord &p1)

    The segment goes from pt0 to pt1.

CanvasText

CanvasText displays text at an arbitrary position and orientation. It is derived from CanvasItem. The text is drawn by the Pango library.

The constructor is

  • CanvasText(const Coord &location, const std::string &text)

where location is the position of the lower left corner of the text, in user coordinates.

CanvasText methods include

  • CanvasText::setFillColor(const Color& color)

    sets the color of the text.

  • CanvasText::setFont(const std::string &fontdesc, bool inPixels)

    fontdesc is a string that will be passed to pango_font_description_from_string() to determine the font. It includes a font family or families, style options, and size (for example, "Times Bold 0.2"). The size is interpreted in pixels if inPixels is true and in user units otherwise. The names of the installed font families are returned by the list_fonts() function, defined globally in the OOFCanvas namespace.

  • CanvasText::rotate(angle)

    rotates the text by the given angle, in degrees, about the left end of the text's baseline. Positive angles are counterclockwise.

RubberBand

Rubberbands are lines drawn on top of the rest of the Canvas to indicate mouse movements while a mouse button is pressed. To use a rubberband, create a RubberBand object and pass it to GUICanvasImpl::setRubberBand(RubberBand*). The rubberband will be redrawn every time the mouse moves until GUICanvasImpl::removeRubberBand() is called.

Notes:

  • setRubberBand can be called from the mouse-down callback, but it doesn't need to be.

  • OOFCanvas does not take ownership of the rubberband object. The calling code must delete it when done with it in C++ (if necessary) and make sure to retain a reference to it in Python (when necessary).

Various subclasses of RubberBand are defined in rubberband.h:

  • LineRubberBand is a straight line from the mouse-down position to the current position.

  • RectangleRubberBand is rectangle with one corner at the mouse-down position and the diagonally opposite corner at the current position.

  • CircleRubberBand is a circle centered on the mouse-down position and passing through the current position.

  • EllipseRubberBand is an ellipse that is fit into a rectangle, as in RectangleRubberBand.

  • SpiderRubberBand is a set of line segments, starting at given points and ending at the current mouse position. The start points are specified by calling SpiderRubberBand::addPoints(list), where in C++, list is a std::vector<Coord>*. In Python, it's a iterable collection of objects where obj[0] is the x component of obj and obj[1] is its y component.

The appearance of the rubberband is controlled by these functions in the RubberBand base class:

  • void RubberBand::setLineWidth(double width)

    width is the line width in pixel units.

  • void RubberBand::setColor(const Color& color)

    sets the color of the dashed line.

  • void RubberBand::setDashLength(double length)

    sets the length of the dashes in pixels.

  • void RubberBand::setDashColor(const Color &color)

    sets the color of the line between the dashes. If this function is not called, the spaces between the dashes are not filled.

  • void RubberBand::setDashed(bool)

    turns the dashes on and off. Undashed rubberbands drawn with solid lines may be hard to see on some backgrounds. The default is to draw dashes.

Adding new rubberband classes is described in the appendix, below.

Appendix: Debugging Tools

Building OOFCanvas with CMAKE_BUILD_TYPE set to "Debug" enables some features that can help with debugging.

  • CanvasItem::drawBoundingBox(double, const Color&)

    sets a flag indicating that the item's bounding box should be drawn when the item is drawn. The arguments are a line width and a Color.

  • OffScreenCanvas::newLayer()

    will print a message if the new layer's name is not unique.

Appendix: Adding New CanvasItem Subclasses

New CanvasItem subclasses can be derived in C++ from CanvasItem, CanvasShape, or CanvasFillableShape. A CanvasShape is a CanvasItem with predefined methods for setting line drawing parameters. A CanvasFillableShape is a CanvasShape with predefined methods for setting a fill color.

Actually, two classes must be written for each new canvas item. One, derived from CanvasItem, contains the parameters describing the item and is visible to the calling program. The other, derived from the CanvasItemImplementation template, contains the Cairo code for actually drawing the item, and is hidden from the calling program.

The template argument for CanvasItemImplementation is the CanvasItem subclass that the template implements. The template is derived from the non-templated CanvasItemImplBase class, which contains all of the methods that don't explicitly depend on the template parameter. There are also two templated classes derived from CanvasItemImplementation, CanvasShapeImplementation and CanvasFillableShapeImplementation, which are used to implement common items derived from CanvasShape and CanvasFillableShape.

This will be easier to explain with an example, so what follows is an annotation of the CanvasRectangle class and its implementation.

Bounding Boxes

First, though, comes a discussion of bounding boxes. Every item needs to be able to compute its bounding box, which is the smallest rectangle, aligned with the x and y axes, that completely encloses the item in user space. The rectangle is used to make some computations more efficient, to determine how large the bitmap needs to be, and to define what "zoom to fill" means.

If an item contains components with sizes specified in pixels, it will not be possible to compute the bounding box in user coordinates without knowing the current ppu. As a simple example, consider a circle of radius 2 in user space, with a perimeter that is drawn two pixels wide outside of that radius. Each side of the bounding box is 4 user units plus 4 pixels.

This is potentially a problem, since the bounding box is one of the things that determines the ppu in some situations. Instead, items provide their "bare" bounding box, which is what the bounding box would be if the ppu were infinite and the pixel size were zero. In the example above, the bare bounding box is a square of side 4 centered on the circle.

It is possible that an item's size is given entirely in pixels, which means that its bare bounding box has size zero in both directions. This is fine. The bounding box will be Rectangle(pt,pt) where pt is a Coord at the position of the item.

An item's bounding box is stored in its implementation, in a public data member Rectangle CanvasItemImplBase::bbox. It's public, because an implementation is only visible to its particular CanvasItem subclass. If a change to the CanvasItem changes its bounding box, it can simply reset bbox and call CanvasItemImplBase::modified().

The CanvasItem Subclass

canvasrectangle.h contains the declaration

class CanvasRectangle : public CanvasFillableShape  // [1]
{ 
  protected:
    double xmin, ymin, xmax, ymax;                  // [2]
  public:
    CanvasRectangle(const Coord&, const Coord&);    // [3]
    CanvasRectangle(const Coord*, const Coord*);    // [4]
    static CanvasRectangle *create(const Coord *p0, const Coord *p1); [5]
    virtual const std::string &classname() const;   // [6]
    void update(const Coord&, const Coord&);        // [7]
    double getXmin() const { return xmin; }         // [8]
    double getXmax() const { return xmax; }
    double getYmin() const { return ymin; }
    double getYmax() const { return ymax; }
    friend std::ostream &operator<<(std::ostream &, const CanvasRectangle&);
    virtual std::string print() const;              // [9]
};
  1. CanvasRectangle is derived from CanvasFillableShape, but the notes here apply just as well to items derived from CanvasShape or directly from CanvasItem.

  2. These are all of the parameters that define the rectangle. Line thickness, color, and dashes are not included because they're set in the CanvasShape base class, and fill color is in the CanvasFillableShape base class.

  3. The constructor needs to set the parameters that describe the rectangle, and to create the implementation:

    CanvasRectangle::CanvasRectangle(const Coord &p0, const Coord &p1)
      : CanvasFillableShape(
          new CanvasRectangleImplementation(this, Rectangle(p0, p1))),
       xmin(p0.x), ymin(p0.y),
       xmax(p1.x), ymax(p1.y)
    {}

    The constructor invokes the CanvasFillableShape constructor, whose argument is a pointer to a new implementation. The item owns the implementation and will delete it when it's done with it. The implementation class will be discussed below.

  4. This form of the constructor, using pointers instead of references for its arguments, is for use by SWIG when generating the Python interface.

  5. The create method just calls one of the constructors, and returns a pointer to the new CanvasRectangle. It will be the only form of the constructor available to Python.

  6. The classname method is used by the templates in pythonexportable.h to allow a generic CanvasItem object returned from C++ to Python to be interpreted as the correct CanvasItem subclass. The method just returns the name of the class:

    const std::string &CanvasRectangle::classname() const {
        static const std::string name("CanvasRectangle");
        return name;
    }
  7. update() is used to change the parameters of the rectangle, and is used when the rectangle is a rubberband, meaning that it will be reconfigured and redrawn repeatedly:

    void CanvasRectangle::update(const Coord &p0, const Coord &p1) {
        xmin = p0.x;
        ymin = p0.y;
        xmax = p1.x;
        ymax = p1.x;
        implementation->bbox = Rectangle(p0, p1);
        modified();
    }

    Because the change has altered the rectangle's bounding box, the implementation's bbox is updated, and modified() is called to indicate that the rectangle will need to be re-rendered.

    A CanvasItem that isn't used in a rubberband doesn't need to have an update method.

  8. getXmin(), etc, are convenience functions that might be useful to a user but aren't actually required by OOFCanvas.

  9. The print() method is required, but it's really only there for debugging. The to_string() function template in utility.h allows print to be defined in terms of operator<<:

    std::string CanvasRectangle::print() const {
     	return to_string(*this);
     }

The CanvasItemImplementation Subclass

Just as a CanvasItem subclass can be derived from CanvasItem, CanvasShape, or CanvasFillableShape, its implementation can be derived from CanvasItemImplementation, CanvasShapeImplementation, or CanvasFillableShapeImplementation. These templates are defined in canvasitemimpl.h and canvasshapeimpl.h. The template parameter is the CanvasItem class that the implementation implements. The templates share a non-templated base class, CanvasItemImplBase, which contains all the code that doesn't depend on the template parameter.

CanvasRectangleImplementation is be declared and defined entirely within the same C++ file that defines CanvasRectangle, because it is accessed only via virtual functions and the pointer that's stored in the CanvasRectangle. Because CanvasRectangle is derived from CanvasFillableShape, CanvasRectangleImplementation must be derived from CanvasFillableShapeImplementation. So canvasrectangle.C contains this declaration:

class CanvasRectangleImplementation
    : public CanvasFillableShapeImplementation<CanvasRectangle>       // [1]
  {
  public:
    CanvasRectangleImplementation(CanvasRectangle *item,              // [2]
                                  const Rectangle &bb)                // [2]
      : CanvasFillableShapeImplementation<CanvasRectangle>(item, bb)  // [3]
    {}
    virtual void drawItem(Cairo::RefPtr<Cairo::Context>) const;       // [4]
    virtual bool containsPoint(const OSCanvasImpl*, const Coord&) const; // [5]
  };
  1. The template argument is the CanvasItem subclass that this class implements.

  2. The constructor arguments must include the CanvasItem and its bounding box. The bounding box can be an uninitialized Rectangle if it's not known yet. In this case, the bounding box is known and has been provided by the caller, the CanvasRectangle constructor.

    If necessary, there can be other arguments here, since it is called only by the associated CanvasRectangle constructor.

  3. The CanvasItem and bounding box must be passed to the base class constructor. CanvasItemImplementation and CanvasShapeImplementation work the same way as the CanvasFillableShapeImplementation used here.

  4. drawItem() must be defined. Given a Cairo::Context, it creates a path, and strokes or fills it, using information in the CanvasRectangle, which it can access using its canvasitem pointer. Because of the templating, canvasitem is a pointer to the correct CanvasItem subclass, CanvasRectangle.

    void CanvasRectangleImplementation::drawItem(
     					 Cairo::RefPtr<Cairo::Context> ctxt)
       const
     {
       double w = lineWidthInUserUnits(ctxt);
       double halfw = 0.5*w;
       Rectangle r = bbox;
       r.expand(-halfw); // move all edges inward by half the line width
       ctxt->move_to(r.xmin(), r.ymin());
       ctxt->line_to(r.xmax(), r.ymin());
       ctxt->line_to(r.xmax(), r.ymax());
       ctxt->line_to(r.xmin(), r.ymax());
       ctxt->close_path();
       fillAndStroke(ctxt);
     }

    lineWidthInUserUnits() is defined in CanvasShapeImplementation and gets the desired line width from the CanvasRectangle, or rather its CanvasShape base class. fillAndStroke() is defined in CanvasFillableShapeImplementation and gets line, dash, and fill information from CanvasShape and CanvasFillableShape.

    Note that the perimeter is drawn so that the outer edges of the lines are at the nominal bounds of the rectangle. A different kind of CanvasItem might choose to center the lines on the nominal bounds, but in that case it would have to increase the size of the bounding box.

  5. Given a user-space Coord that is known to be within the item's bounding box, containsPoint() returns true if the Coord is actually within the item. containsPoint() must be defined, although if an item will never be clicked on, defining it to simply return false is legal.

    Here is the definition from CanvasRectangleImplementation:

     bool CanvasRectangleImplementation::containsPoint(
     		   const OSCanvasImpl *canvas, const Coord &pt)
     const
     {
     double lw = lineWidthInUserUnits(canvas);
     return canvasitem->filled() || 
               (canvasitem->lined() &&
                  (pt.x - bbox.xmin() <= lw || bbox.xmax() - pt.x <= lw ||
                   pt.y - bbox.ymin() <= lw || bbox.ymax() - pt.y <= lw));
     } 

    Because the given point is known to be within the bounding box, and the rectangle fills the bounding box, there's nothing to compute if the rectangle is filled. If it's not filled, it's necessary to compute whether or not the point is on a perimeter segment.

    The first argument is an OSCanvasImpl*, a pointer to the implementation class for OffScreenCanvas, which is needed for conversion between coordinate systems, if the line width was specified in pixels.

pixelExtents

One more function needs to be defined in any CanvasItemImplementation that includes graphical elements whose size is specified in pixels.

void CanvasItemImplBase::pixelExtents(double &left, double &right, double &up, double &down) const;

sets the distance, in pixel units, that the item extends past its bare bounding box, in each of the given directions. (left == -x, right == +x, up == +y, down == -y) The default version sets all four values to zero. Since CanvasRectangleImplementation draws its lines inside the bounding box, it uses the default version. If its perimeters were drawn with their centerlines on the bounding box edges, pixelExtents would set each of the four arguments to half the line width, assuming that the line width was specified in pixels.

When an item contains elements defined in pixel units as well as elements defined is user units, it's possible that the bounding box constant for large ppu and ppu-dependent for small ppu, with a crossover at some finite non-zero ppu. Such a canvas item should probably be represented as two or more better-behaved canvas items.

The default version of pixelExtents defined in CanvasItemImplBase sets all four extents to zero. The version in CanvasShapeImplementation will work for any item derived from CanvasShape or CanvasFillableShape whose perimeter line segments are given in pixel units, but has no other pixel unit dimensions. (Actually, it only works approximately, but is good enough if the line segments aren't too thick.)

Other Useful CanvasItem Methods

  • void CanvasShapeImplementation::stroke(Cairo::RefPtr<Cairo::Context>) const draws the current Cairo path using the line color, width, and dash settings from the CairoShape.

  • void CanvasFillableShapeImplementation::fillAndStroke(Cairo::RefPtr<Cairo::Context>) const draws the current Cairo path using the line color, width, and dash settings from the CairoShape, and fills it using the fill color from CanvasFillableShape.

Appendix: Adding New RubberBand Classes

Rubberbands are derived from the RubberBand class declared in oofcanvas/oofcanvasgui/rubberband.h. For simple examples, see that file and oofcanvas/oofcanvasgui/rubberband.C. Each class needs to redefine three virtual functions:

  • RubberBand::start(CanvasLayer*, const Coord&)

    is called when the mouse is clicked at the given Coord and the canvas is starting to draw a rubberband. The function should first call the base class method, and then create the CanvasItems that form the rubberband and add them to the given CanvasLayer.

    The function Rubberband::doDashes(CanvasShape*) can be used to set the dash style on any CanvasShape used by the rubberband. The line color and line width should be be set using the base class Color color and double lineWidth data members.

  • RubberBand::stop()

    is called when the canvas stops drawing the rubberband. The base class method should be called, and in many cases will be sufficient, but if the subclass needs to do any cleaning up, it can do it here.

  • RubberBand::update(const Coord &pt)

    is called whenever the mouse moves. The given Coord is the current user-space position of the mouse. The subclass method should call the base class method (which just sets RubberBand::currentPt to the current coordinate) and then update its CanvasItems to reflect the new configuration of the rubberband.

Appendix: Internal Details

It shouldn't be necessary to understand this section in order to use OOFCanvas. It's here to help development.

Class Hierarchies and Encapsulation

Encapsulation is used to separate the implementation details from the user-visible header files. Encapsulation is handled differently for different classes, depending on the complexity of their inheritance structure.

  • OffScreenCanvas and Canvas are the user-visible C++ classes for the canvas objects.

    • The hidden implementation class for OffScreenCanvas is OSCanvasImpl. OffScreenCanvas contains an opaque pointer, osCanvasImpl, to an OSCanvasImpl.

    • Canvas is derived from OffScreenCanvas, adding screen display and mouse interaction abilities.

    • The hidden implementation class for Canvas is GUICanvasImpl, which is derived from OSCanvasImpl. A Canvas contains a pointer, guiCanvasImpl, to its GUICanvasImpl. The GUICanvasImpl pointed to from a Canvas is the same object as the OSCanvasImpl pointed to in its base class.

    • The user-visible Python canvas, PythonCanvas, is derived directly from GUICanvasImpl because it is effectively encapsulated by the SWIG wrapper. There is no need to derive it from Canvas.

  • CanvasLayer is the user-visible base class for layers.

    • The implementation class, CanvasLayerImpl is derived from CanvasLayer.
  • CanvasItem is the user-visible base class for canvas items.

    • The hidden implementation class for a subclass ITEM of CanvasItem is a templated class, CanvasItemImplementation<ITEM>, which is derived from CanvasItemImplBase.

    • Each CanvasItem contains an opaque pointer, implementation, to its CanvasItemImplBase, and each CanvasItemImplementation<ITEM> contains a pointer, canvasitem to its ITEM.

The Rendering Call Sequence

Each CanvasLayer contains a Cairo::ImageSurface which contains a bitmap of what's been drawn in the layer, a Cairo::Context which controls drawing to surface, and a Rectangle which is the bounding box (in user coordinates) of all of the layer's CanvasItems.

When a CanvasItem is added to a CanvasLayer, the layer is marked "dirty" and the item is stored in the layer. No drawing is done at this point.

When all items have been added to the layers, calling GUICanvasImpl::draw() generates a draw event on the GtkLayout. This causes GUICanvasImpl::drawHandler() to be called. The argument to drawHandler is the Cairo::Context for drawing to the GtkLayout's Cairo::Surface.

GUICanvasImpl::drawHandler() begins by computing the horizontal and vertical offsets that will be used to keep the image centered in the gtk window (if the image is smaller than the window) or at the position determined by the scroll bars (if the image is larger than the window).

Next, drawHandler calls Canvas::setTransform(), which computes the matrix that converts from user coordinates to bitmap coordinates within the layer, given the ppu. The GtkLayout is resized if necessary so that it is large enough to accomodate the bounding boxes of all of the layers, plus an optional margin (set by OffScreenCanvas::setMargin()). Note that a layer's bounding box, in user units, can depend on the ppu if the layer contains items with sizes given in pixels.

What happens next depends on whether or not a rubberband is being drawn. If there is no rubberband, GUICanvasImpl::drawHandler draws the background color and then, for each layer from bottom to top, tells the layer to draw all of its CanvasItems to its own Cairo::ImageSurface (CanvasLayer::render()), and copies the layer's surface to the GtkLayout's surface (CanvasLayer::copyToCanvas()) at the position given by the scroll bars. (CanvasLayer::render() only redraws its items if any have changed since the last time they were drawn.)

If there is an active rubberband, on the first call to drawHandler after the mouse button was pressed all of the CanvasLayers other than the rubberband's layer are rendered to a separate Cairo::ImageSurface called the nonRubberBandBuffer. Then this buffer is copied to the GtkLayout and the rubberband is drawn on top of it. On subsequent calls to drawHandler, the nonRubberBandBuffer is copied and the rubberband is drawn, but the nonRubberBandBuffer is not rebuilt unless the layers have changed.


Disclaimer and Copyright

NIST-developed software is provided by NIST as a public service. You may use, copy and distribute copies of the software in any medium, provided that you keep intact this entire notice. You may improve, modify and create derivative works of the software or any portion of the software, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the software and should note the date and nature of any such change. Please explicitly acknowledge the National Institute of Standards and Technology as the source of the software. To facilitate maintenance we ask that before distributing modified versions of this software, you first contact the authors at [email protected].

NIST-developed software is expressly provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED, IN FACT OR ARISING BY OPERATION OF LAW, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NIST NEITHER REPRESENTS NOR WARRANTS THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT ANY DEFECTS WILL BE CORRECTED. NIST DOES NOT WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE USE OF THE SOFTWARE OR THE RESULTS THEREOF, INCLUDING BUT NOT LIMITED TO THE CORRECTNESS, ACCURACY, RELIABILITY, OR USEFULNESS OF THE SOFTWARE.

You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. The software developed by NIST employees is not subject to copyright protection within the United States.