GEOG 489
Advanced Python Programming for GIS

4.9.2 Adapting the geometry example

PrintPrint

We are now going to revist the geometry example we saved in Section 4.8.

To prepare our geometry classes to be drawn on the screen, we first need to modify their definitions by

  1. introducing the new “color” instance variable for storing a QColor object,
  2. defining a method called “paint” for drawing the respective object on a screen. This method will have one parameter called “painter” for passing an object of the QPainter class that can be used for drawing onto the widget.

As an exercise, think about where in the class hierarchy you would need to make changes to address points (1) and (2). Once you have thought about this for a bit, read on.

The new attribute “color” is something that all our geometry classes have in common. Therefore, the best place to introduce it is in the root class Geometry. In all subclasses (Circle, Rectangle, Square), you then only have to adapt the constructor to include an additional keyword parameter for the color. Regarding point (2): As we saw above, drawing a circle or a rectangle requires different methods of the QPainter object to be called with different kinds of parameters specific to the particular geometry type. Therefore, we define the method paint(…) in class Geometry but then override it in the subclasses Circle and Rectangle. For class Square, the way it is based on class Rectangle allows us to directly use the implementation of paint(…) from the Rectangle class, so we do not have to override the method in the definition of Square. Here are the changes made to the four classes.

Class Geometry: as discussed, in class Geometry we introduce the “color” variable, so we need to change the constructor a bit. In addition, we add the method paint(…) but without an implementation. The rest of the definition remains unchanged:

class Geometry: 

	def __init__(self, x = 0.0, y = 0.0, color = Qt.black): 
       	 self.x = x 
         self.y = y 
         self.color = color 

    ...

    def paint(self, painter): 
         pass 

Classes Circle and Rectangle: for the classes Circle and Rectangle, we adapt the constructors to also include a keyword argument for “color”. The color is directly passed on to the constructor of the base class, while the rest of the constructor remains unchanged. We then provide a definition for method paint(…) that sets up the Pen object to use the right color and then uses the corresponding QPainter method for drawing the object (drawEllipse(…) for class Circle and drawRect(…) for class Rectangle) providing the different instance variables as parameters. The rest of the respective class definitions stay the same:

class Circle (Geometry): 

    def __init__(self, x = 0.0, y = 0.0, radius = 1.0, color = Qt.black): 
         super(Circle,self).__init__(x,y,color) 
         self.radius = radius 

    ...

    def paint(self, painter): 
         painter.setPen(QtGui.QPen(self.color, 2)) 
         painter.drawEllipse(QPoint(self.x, self.y), self.radius, self.radius) 


class Rectangle(Geometry): 

    def __init__(self, x = 0.0, y = 0.0, width = 1.0, height = 1.0, color = Qt.black): 
         super(Rectangle,self).__init__(x,y, color) 
         self.width = width 
         self.height = height 

    ...

    def paint(self, painter): 
         painter.setPen(QtGui.QPen(self.color, 2,  join = Qt.MiterJoin)) 
         painter.drawRect(self.x, self.y, self.width, self.height)

Class Square: sor the Square class, we just adapt the constructor to include the color, the rest remains unchanged:

class Square(Rectangle): 

    def __init__(self, x = 0.0, y = 0.0, sideLength = 1.0, color = Qt.black): 
         super(Square,self).__init__(x, y, sideLength, sideLength, color)

Now that we have the geometry classes prepared, let us again derive a specialized class GeometryDrawingWidget from QWidget that stores a list of geometry objects and in the paintEvent(…) method sets up a QPainter object for drawing on its content area and then invokes the paint(…) methods for all objects from the list. This is bascially the same thing we did towards the end of Section 4.7.2 but now happens inside the new widget class. The list of objects is supposed to be given as a parameter to the constructor of GeometryDrawingWidget:

import math, sys 
from PyQt5 import QtGui, QtWidgets 
from PyQt5.QtCore import Qt, QPoint

class GeometryDrawingWidget(QtWidgets.QWidget): 

     def __init__(self, objects): 
         super(GeometryDrawingWidget,self).__init__() 
         self.objectsToDraw = objects 

     def paintEvent(self, event): 
         qp = QtGui.QPainter() 
         qp.begin(self) 
         for obj in self.objectsToDraw: 
             obj.paint(qp) 
         qp.end()

Finally, we create another new class called MyMainWindow that is derived from QMainWindow for the main window containing the drawing widget. The constructor takes the list of objects to be drawn and then creates a new instance of GeometryDrawingWidget passing the object list as a parameter and makes it its central widget in line 6:

class MyMainWindow(QtWidgets.QMainWindow): 

    def __init__(self, objects): 
	    super(MyMainWindow, self).__init__() 
	    self.resize(300,300) 
	    self.setCentralWidget(GeometryDrawingWidget(objects))

In the main code of the script, we then simply create an instance of MyMainWindow with a predefined list of geometry objects and then call its show() method to make the window appear on the screen:

app = QtWidgets.QApplication(sys.argv) 

objects = [ Circle(93,83,45, Qt.darkGreen), Rectangle(10,10,80,50, QtGui.QColor(200, 0, 250)), Square(30,70,38, Qt.blue)] 

mainWindow = MyMainWindow (objects) 
mainWindow.show() 

sys.exit(app.exec_())

When you run the program, the produced window should look like in the figure below. While this is not really visible, every time you resize the window, the paintEvent(…) method of GeometryDrawingWidget will be called and the content consisting of the three geometric objects will be redrawn. While in this simple widget, we only use fixed absolute coordinates so that the drawn content is independent of the size of the widget, one could easily implement some logic that would scale the drawing based on the available space.

shapes in the left hand corner. Pink rectangle above a blue square. Both are intersected by a green circle      
Figure 4.20 Window produced by the previous code example with the drawings of the different geometric objects