Thursday, August 13, 2009

General shape drawing application JShapeMaster (Jython & JFace example)

Links:
  • Read here how to setup environment.
  • If you need to check on SWT/JFace API here is the proper place to do so.

New things you will learn in this example: Rectangle, Path, Canvas, Concept of Abstract Base Class

Rectangle class (org.eclipse.swt.graphics.Rectangle)

Fields
int xthe x coordinate of the rectangle
int ythe y coordinate of the rectangle
int widththe width of the rectangle
int heightthe height of the rectangle
Constructors
Rectangle(int x, int y, int width, int height)Construct a new instance of this class given the x, y, width and height values.
Methods
void add(Rectangle rect)Destructively replaces the x, y, width and height values in the receiver with ones which represent the union of the rectangles specified by the receiver and the given rectangle.
boolean contains(int x, int y)Returns true if the point specified by the arguments is inside the area specified by the receiver, and false otherwise.
boolean contains(Point pt)Returns true if the given point is inside the area specified by the receiver, and false otherwise.
void intersect(Rectangle rect)Destructively replaces the x, y, width and height values in the receiver with ones which represent the intersection of the rectangles specified by the receiver and the given rectangle.
Rectangle intersection(Rectangle rect)Returns a new rectangle which represents the intersection of the receiver and the given rectangle.
boolean intersects(int x, int y, int width, int height)Returns true if the rectangle described by the arguments intersects with the receiver and false otherwise.
boolean intersects(Rectangle rect)Returns true if the given rectangle intersects with the receiver and false otherwise.
boolean isEmpty()Returns true if the receiver does not cover any area in the (x, y) coordinate plane, and false if the receiver does cover some area in the plane.
Rectangle union(Rectangle rect)Returns a new rectangle which represents the union of the receiver and the given rectangle.

Path class (org.eclipse.swt.graphics.Path)

Constructors
Path(Device device)Constructs a new empty Path.
Path(Device device, PathData data)Constructs a new Path with the specified PathData.
Path(Device device, Path path, float flatness)Constructs a new Path that is a copy of path.
Methods
void addArc(float x, float y, float width, float height, float startAngle, float arcAngle)Adds to the receiver a circular or elliptical arc that lies within the specified rectangular area.
void addPath(Path path)Adds to the receiver the path described by the parameter.
void addRectangle(float x, float y, float width, float height)Adds to the receiver the rectangle specified by x, y, width and height.
void addString(String string, float x, float y, Font font)Adds to the receiver the pattern of glyphs generated by drawing the given string using the given font starting at the point (x, y).
void close()Closes the current sub path by adding to the receiver a line from the current point of the path back to the starting point of the sub path.
boolean contains(float x, float y, GC gc, boolean outline)Returns true if the specified point is contained by the receiver and false otherwise.
void cubicTo(float cx1, float cy1, float cx2, float cy2, float x, float y)Adds to the receiver a cubic bezier curve based on the parameters.
void getBounds(float[] bounds)Replaces the first four elements in the parameter with values that describe the smallest rectangle that will completely contain the receiver (i.e. the bounding box).
void getCurrentPoint(float[] point)Replaces the first two elements in the parameter with values that describe the current point of the path.
PathData getPathData()Returns a device independent representation of the receiver.
boolean isDisposed()Returns true if the Path has been disposed, and false otherwise.
void lineTo(float x, float y)Adds to the receiver a line from the current point to the point specified by (x, y).
void moveTo(float x, float y)Sets the current point of the receiver to the point specified by (x, y).
void quadTo(float cx, float cy, float x, float y)Adds to the receiver a quadratic curve based on the parameters.

Canvas class (org.eclipse.swt.widgets.Canvas)

Constructors
Canvas(Composite parent, int style)Constructs a new instance of this class given its parent and a style value describing its behavior and appearance.
Methods
void redraw()Causes the entire bounds of the receiver to be marked as needing to be redrawn.
void redraw(int x, int y, int width, int height, boolean all)Causes the rectangular area of the receiver specified by the arguments to be marked as needing to be redrawn.

Concept of Abstract Base Class

Abstract base classes are useful in that they can be used to define and enforce a contract: a set of methods which all classes that will subclass it must support. Disallow instantiating of abstract class helps to ensure program correctness.

class AbstractBase:
    def __init__(self):
        raise NotImplementedError, "Could not instantiate abstract class (Subclass it)"
Drawing on Canvas

Canvas control is specifically designed for drawing operations. You could make use of this control in two ways: you could add listener to it or subclass it. In our example we will use a listener approach. To reducing flickering I recommend to pass SWT.DOUBLE_BUFFERED style bite into Canvas constructor.

We will start by designing abstract shape class

Well every shape has position (two coordinates x and y) and size (x – width and y -height). Shapes also have border and fill color. Every shape should be able to draw itself. Here it goes our shape class:

class AbstractShape:
    def __init__(self, x, y, width, height):
        self._path = None
        self._position = Point(x, y)
        self._size = Point(width, height)
        self._fillColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GREEN)
        self._borderColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_BLUE)
    
    def getBounds(self):
        b = zeros(4, 'f')
        self._path.getBounds(b)
        # Thanks Frank Wierzbicki (http://stackoverflow.com/users/78599/frank-wierzbicki)
        # and Charlie Groves (http://stackoverflow.com/users/157016/charlie-groves)
        # for pointing out * 'explode operator'
        return Rectangle(*[int(round(x)) for x in b])
    
    def createShape(self):
        raise NotImplementedError, "Abstract method implement it in Subclass"

    def setPosition(self, l):
        self._position = l

    def setSize(self, s):
        self._size = s

    def draw(self, gc):
        if (self._path):
            self._path.dispose()
        
        self._path = Path(Display.getCurrent())
        self.createShape()

        gc.setAntialias(SWT.ON) 
        gc.setBackground(self._fillColor)
        gc.setForeground(self._borderColor)
        gc.fillPath(self._path)
        gc.drawPath(self._path)

Every concrete class will properly implement createShape() method. Let's look at createShape() method of RectangleShape class:

class RectangleShape(AbstractShape):
    def createShape(self):
        self._path.addRectangle(self._position.x,
                                 self._position.y,
                                 self._size.x,
                                 self._size.y)
code
"""
General shape drawing application JShapeMaster (Jython & JFace example)
GUID of this code snippet: 12aa740f-5bb5-40be-8409-2c0a01ef37cf
[ extends code snippet: None ]
Author: Darius Kucinskas (c) 2008-2009
Email: d[dot]kucinskas[eta]gmail[dot]com
Blog: http://blog-of-darius.blogspot.com/
License: GPL
"""

from org.eclipse.swt import *
from org.eclipse.swt.SWT import *
from org.eclipse.swt.widgets import *
from org.eclipse.swt.layout import *
from org.eclipse.jface.window import *
from org.eclipse.jface.action import *
from org.eclipse.jface.resource import *
from org.eclipse.swt.graphics import *
from org.eclipse.swt.events import *
from jarray import array, zeros

class AbstractShape:
    def __init__(self, x, y, width, height):
        self._path = None
        self._position = Point(x, y)
        self._size = Point(width, height)
        self._fillColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GREEN)
        self._borderColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_BLUE)
    
    def getBounds(self):
        b = zeros(4, 'f')
        self._path.getBounds(b)
        # Thanks Frank Wierzbicki (http://stackoverflow.com/users/78599/frank-wierzbicki)
        # and Charlie Groves (http://stackoverflow.com/users/157016/charlie-groves)
        # for pointing out * 'explode operator'
        return Rectangle(*[int(round(x)) for x in b])
    
    def createShape(self):
        raise NotImplementedError, "Abstract method implement it in Subclass"

    def setPosition(self, l):
        self._position = l

    def setSize(self, s):
        self._size = s

    def draw(self, gc):
        if (self._path):
            self._path.dispose()
        
        self._path = Path(Display.getCurrent())
        self.createShape()

        gc.setAntialias(SWT.ON) 
        gc.setBackground(self._fillColor)
        gc.setForeground(self._borderColor)
        gc.fillPath(self._path)
        gc.drawPath(self._path)

class RectangleShape(AbstractShape):
    def createShape(self):
        self._path.addRectangle(self._position.x,
                                 self._position.y,
                                 self._size.x,
                                 self._size.y)

class CanvasPaintListener(Listener):
    def __init__(self, app):
        self.app = app

    def handleEvent(self, e):
        self.app.shape.draw(e.gc)

class CanvasMouseListener(MouseListener):
    def __init__(self, app):
        self.app = app

    def mouseDoubleClick(self, evt):
        pass

    def mouseDown(self, evt):
        self.app.isDraging = True
        if (self.app.shape.getBounds().contains(evt.x, evt.y) == True):
            self.app.clickOffsetX = evt.x - self.app.shape.getBounds().x
            self.app.clickOffsetY = evt.y - self.app.shape.getBounds().y

    def mouseUp(self, evt):
        self.app.isDraging = False

class CanvasMouseMoveListener(MouseMoveListener):
    def __init__(self, app):
        self.app = app

    def mouseMove(self, evt):
        if (self.app.isDraging == True and
            self.app.shape.getBounds().contains(evt.x, evt.y) == True):
        
            oldRect = self.app.shape.getBounds()
            self.app.shape.setPosition(Point(evt.x - self.app.clickOffsetX,
                                             evt.y - self.app.clickOffsetY))
            uRect = oldRect.union(self.app.shape.getBounds())
            self.app.canvas.redraw(uRect.x - 1, uRect.y - 1, uRect.width + 3, uRect.height + 3, True)

# Skip this for now
# Draw glider emblem (I use it as icon for this example)
def drawIcon(display):
    image = Image(display, 16, 16)
    gc = GC(image)

    gc.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_TITLE_BACKGROUND_GRADIENT))
    gc.fillRectangle(0, 0, 16, 16)

    gc.setBackground(Color(display, 193, 39, 45))
    gc.fillRectangle( 1, 11, 4, 4)
    gc.fillRectangle( 6, 11, 4, 4)
    gc.fillRectangle(11, 11, 4, 4)
    gc.fillRectangle(11,  6, 4, 4)
    gc.fillRectangle( 6,  1, 4, 4)

    gc.setBackground(Color(display, 253, 185, 19))
    gc.fillRectangle( 1,  1, 4, 4)
    gc.fillRectangle( 1,  6, 4, 4)
    gc.fillRectangle(11,  1, 4, 4)

    gc.setBackground(Color(display, 0, 106, 68))
    gc.fillRectangle( 6,  6, 4, 4)

    gc.dispose()
    return image

class App(ApplicationWindow):
    """ Main class for JFace application drived from ApplicationWindow class """

    # add static image registry
    imageRegistry = None

    # add class method for image registry
    @classmethod
    def getImageRegistry(cls):
        if (not cls.imageRegistry):
            cls.imageRegistry = ImageRegistry()
            cls.imageRegistry.put("app.icon", ImageDescriptor.createFromImage(drawIcon(Display.getCurrent())))
        
        return cls.imageRegistry

    def __init__(self, shell):
        """ application constructor """
        # let parent class to do it's stuff first
        ApplicationWindow.__init__(self, shell)

        self.canvas = None
        self.shape = RectangleShape(50, 50, 100, 100)
        self.isDraging = False
        self.clickOffsetX = 0
        self.clickOffsetY = 0
    
    def dispose(self):
        """ dispose resources here """
        pass
    
    def createContents(self, parent):
        self.shell.setImage(drawIcon(Display.getCurrent()))
        self.shell.text = 'JShapeMaster (Jython & JFace example)'
        self.shell.setMinimumSize(650, 480)

        # create canvas for drawing
        self.canvas = Canvas(parent, SWT.BORDER|SWT.DOUBLE_BUFFERED|SWT.NO_REDRAW_RESIZE|SWT.NO_MERGE_PAINTS)
        self.canvas.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_TITLE_BACKGROUND_GRADIENT))
        self.canvas.setLayoutData(GridData(SWT.FILL, SWT.FILL, True, True, 1, 1))

        # add listeners to canvas, so we could react to events
        self.canvas.addListener(SWT.Paint, CanvasPaintListener(self))
        ##self.canvas.addListener(SWT.Resize, CanvasResizeListener(self))
        self.canvas.addMouseListener(CanvasMouseListener(self))
        self.canvas.addMouseMoveListener(CanvasMouseMoveListener(self))

        return self.canvas

if __name__ == "__main__":
    """ The entry point for our application """
    display = Display()
    shell = Shell(display)

    app = App(shell)
    # Voodoo of SWT message loop
    try:
        app.setBlockOnOpen(True)
        app.open()
    finally:
        if app != None:
            app.dispose()

    display.dispose()
So you now have learned basics of animation of canvas.