Custom GraphicsView setup

Referring to previous prototypes this class was setup reasonably fast, the Polygon class however was built from scratch as my prototype for that did it’s own triangulation (slowly I might add) and I figured out that although QPolygon does not accept a list of points it can quite easily be populated with a concave shape with good results.

from PyQt4 import QtGui
#import classes from the same package in the same namespace
from MouseData import *
from Qtutils.LaunchAsStandalone import *
from PaintItem import *
from Polygon import *

'''
GraphicsView is a custom implementation of
the QGraphicsView that does not depend on
scenes and does not support zooming and
panning in such an awkwardly enforced way.

In the event of camera usage, the
mouse coordinates will also be in a
converted state allowing functionality
programming in a natural way.
'''
class GraphicsView(QtGui.QFrame):
    def __init__(self, parent=None):
        #inherit from QFrame for the paint function
        #and the ability to be used in a window
        QtGui.QFrame.__init__(self, parent)

        #track what items are attached to this scene
        self._paintitems = []
    
    '''
    Helper function for adding drawable items
    to the graphicsview, filters out items that do
    not support drawing
    
    @param in_item: item to add on the stage
    any class with a paint(QPainter) function
    '''
    def addItem(self, in_item):
        if isinstance(in_item, PaintItem):
            self._paintitems.append(in_item)
    
    '''
    Inherited paint event
    '''
    def paintEvent(self, e):
        #let parent handle drawing the main object
        QtGui.QFrame.paintEvent(self, e)
        #create a painter for objects to draw with
        painter = QtGui.QPainter(self)
        #draw each paintable item
        for item in self._paintitems:
            item.paint(painter)

Then to prototype this class, I use the QtStandalone class and the following main function at the bottom of my GraphicsView file:

#main function to launch as standalone app for unit-tests
def main():
    w = GraphicsView()
    w.resize(QtCore.QSize(110,110))
    p = Polygon([Vec(0,0),
                 Vec(100,0),
                 Vec(100,100),
                 Vec(66,100),
                 Vec(66,50),
                 Vec(33,50),
                 Vec(33,100),
                 Vec(0,100)])
    w.addItem(p)
    w.show()
    return w
    
QtStandalone(main)

This won’t work of course, because the Polygon and PaintItem classes are not known. Now I created the PaintItem to be able to do some type checking as well as to implement generalized behaviour as I want my drawn objects to be able to transform (scale, rotate, reposition) later on, and possibly even have a zoom feature, but for not it is pretty much empty:

'''
base class for drawable items
'''
class PaintItem:
    def __init__(self):
        pass

So here’s the polygon class, it stores it’s vertices in a list and rebuilds the internal QPolygon only when necessary to gain performance (I don’t know how slow this program will become at the end of the day so I’m taking some minor precautions).

It is made to be a finalized class so variables are made private with setter functions handling clean assignment to say brush and pen colors as well as points. The addPoint method is to be used extensively while drawing and possibly we need an optimizePoints function to cleanup redundant points after freehand drawing, but more on that later.

from PaintItem import *
from PyQt4 import QtCore, QtGui

 
class Polygon(PaintItem):
    '''
    @param in_points: list of Vec2, the points to add
    as polygon vertices. Default is an empty list.
    '''
    def __init__(self, in_points = []):
        #polygon vertices, a list of Vec2
        self._points = in_points
        #required for self._polygon to exist
        self._buildPolygon()
        
        self._pen = QtCore.Qt.NoPen;
        self._brush = QtGui.QBrush( QtGui.QColor(64,64,244,128) )
    
    '''
    Set the outline color to draw with
    @param c: QColor, color to use
    if c == None there will be no outline
    '''    
    def setOutlineQColor(self, c):
        if c == None: #remove pen
            self._pen = QtCore.Qt.NoPen
        elif self._pen == QtCore.Qt.NoPen: #rebuild pen
            self._pen = QtGui.QPen(c)
        else: #change color of existing pen
            self._pen.setColor(c)
    
    '''
    set the fill color to draw with
    @param c: QColor, color to use
    if c == None there will be no fill
    '''
    def setQColor(self, c):
        if c == None: #remove brush
            self._brush = QtCore.Qt.NoBrush
        elif self._brush == QtCore.Qt.NoBrush: #rebuild brush
            self._brush = QtGui.QBrush(c)
        else: #change color of existing brush
            self._brush.setColor(c)
    
    '''
    Helper function to generate an int-based
    QColor to set as outline color
    @param r: int, 0-255 based red value
    @param g: int, 0-255 based green value
    @param b: int, 0-255 based blue value
    '''
    def setOutlineColor(self, r, g, b, a=255):
        self.setOutlineQColor( QtGui.QColor(r,g,b,a) )
    
    '''
    Sets the fill color, see setOutlineColor
    '''
    def setColor(self, r, g, b, a):
        self.setQColor( QtGui.QColor(r,g,b,a) )
    
    '''
    Assigns a new Polygon to self._polygon
    containing the current self._points data
    Call after changeing self._points
    '''
    def _buildPolygon(self):
        npt = len(self._points)
        #create a polygon of the right size
        self._polygon = QtGui.QPolygon()
        #initialize argument list starting at point index 0
        #WARNING: documentation says this function requires index, nPoints
        #but that is not the case, the nPoints is handled automatically
        ptlist = [0]
        #add all points' x,y to the arglist
        for i in range(npt):
            ptlist.extend([self._points[i][0], self._points[i][1]])
        #apply the arglist to the yet empty self._polygon
        apply(self._polygon.putPoints, ptlist)
    
    '''
    @param in_point: Vec2, point to add to the polygon
    '''
    def addPoint(self, in_point):
        self._points.append(in_point)
        #update geometry as the polygon has been modified
        self._buildPolygon()
        
    '''
    @param in_points: list of Vec2, points to add to the polygon
    '''
    def addPoints(self, in_points):
        self._points.extend(in_points)
        #update geometry as the polygon has been modified
        self._buildPolygon()
    
    '''
    Paints the polygon using self._polygon,
    whether it is out of date or not, and by
    applying self._brush and self._pen to it
    '''
    def paint(self, painter):
        #TODO: add color and pen settings
        painter.setBrush( self._brush )
        painter.setPen( self._pen )
        
        #draw the polygon
        painter.drawPolygon(self._polygon)

Now assuming all files are named after their contained class the GraphicsView should be executable resulting in the following window:
tmp

Leave a Reply

Your email address will not be published. Required fields are marked *