Painting on a Widget - Overview

This was an article written for CodeProject.com, and has been hosted on here as well as on their site.

In this small article I look at porting a C# project over into PyQt and show you just how easy it is, as well as how flexible the PyQt framework is in action.

This tutorial was orginaly submitted to CodeProjects and the orginal can be found here. .

Painting on a Widget - Tutorial

Posted on 26/04/2012 at 01:08

Introduction

This article was based around a similar article I wrote about a year ago, where we implement drawing on a panel in C#, and now we're taking the main structure of this and porting it to the Python Version of Qt, PyQt. The Original article can be found here.

So we're be looking at how to design and implement the ability to paint onto a widget, drawing lines and the ability to erase them, whilst still allowing the user to customize the size and colour of the paint brush.

This approach to drawing graphics is more aligned to vector type graphics where the points are floating point values rather than rasterized bitmap graphics where each pixel has a colour.

Side Note: I use PyQt within a programme called Maya 2012, as that's my normal development environment for it, this code does not use any Maya 2012 python commands so should work perfectly fine when used with IDLE or any other python IDE.

Background

The article is set at a simple entry level; we will cover creating classes, creating widgets and using event handlers. You will need PyQt installed and working with whatever Python IDE you are using.

Using the code

So one of the best things of Qt, is the graphical UI designer that comes with Maya 2012 by default. This programme can be found in the same folder that Maya.exe can be, usually C:\Program Files\Autodesk\Maya2012\bin.

Other version of Qt and PyQt come with this, and its location will usually be in the root Qt folder on the C:\ drive.

UI Desinger

The UI’ve designed is exactly the same as the C# one created a year ago, to show how the code can be ported over easily.

Below is the UI in the designer, and just below that is the list of the widgets, their names, types, positions and sizes so you can exactly re-create this.

Widget Types

objectName

Position

Size

QPushButton

BrushErase_Button

10,10

101,23

QPushButton

ChangeColour_Button

10,40

101,23

QPushButton

Clear_Button

10,400

101,23

QStackedWidget

DrawingFrame

120,10

651,411

QSpinBox

Thickness_Spinner

20,80

81,22

Once this is all designed, we can save it out as a .UI file, I suggest in a common place for scripts, as you need to hardcode in the address of the file, so don't put it too far out the way!!

Ok so with the UI all complete, we need to jump into our Python IDE, with for me, is Maya 2012.

Python Side

With the UI all created and saved out, we need to start off by getting python to show display the UI. The code is fairly self-explanatory and is generic.

Running this script will create a new PyQt window with the form that we designed and saved out. Simple part done.
To support the code, we need to create a few addition classes that will hold data, and work with the held data to remove and add new points, as well as saving vital information such as colour and size of the brush strokes.

The Classes

There are 4 new classes we need to implement, the Colour3 Class; which will just hold the RGB colour values, the Point class; which will hold the X and Y coordinate, the Shape class; which holds information about that point, such as position, colour, size, and what shape this point is attached to, and the Shapes class; which will hold all the shape’s with functions to create, retrieve and remove them.

Additionally there is another class, the Painter class, but this is a widget class, so we’ll come to this later.

Both of these two classes are very simple, so I won’t explain them, they’re just there to hold small amounts of data.

The Shape class is there to hold data on that particular drawing point, such as its position, the width of the shape, the colour and the shape number so we can link all the same shape objects together.

The Shapes class is there to hold all the drawing point information, and allow quick and easy to use functions to the drawing panel regarding the drawing point data.

class Shapes:
#Stores all the shapes
__Shapes = []
def __init__(self):
self.__Shapes = []
#Returns the number of shapes being stored.
def NumberOfShapes(self):
return len(self.__Shapes)
#Add a shape to the database, recording its position,
#width, colour and shape relation information
def NewShape(self,L,W,C,S):
Sh = Shape(L,W,C,S)
self.__Shapes.append(Sh)
#returns a shape of the requested data.
def GetShape(self, Index):
return self.__Shapes[Index]
#Removes any point data within a certain threshold of a point.
def RemoveShape(self, L, threshold):
#do while so we can change the size of the list and it wont come back to bite me in the ass!!
i = 0
while True:
if(i==len(self.__Shapes)):
break
#Finds if a point is within a certain distance of the point to remove.
if((abs(L.X - self.__Shapes[i].Location.X) < threshold) and (abs(L.Y - self.__Shapes[i].Location.Y) < threshold)):
#removes all data for that number
del self.__Shapes[i]
#goes through the rest of the data and adds an extra
#1 to defined them as a seprate shape and shuffles on the effect.
for n in range(len(self.__Shapes)-i):
self.__Shapes[n+i].ShapeNumber += 1
#Go back a step so we dont miss a point.
i -= 1
i += 1

For now, let’s just create a simple dummy widget that we can integrate into the main UI and then come back later and flesh it out.

This code will create a new class which will inherit from the PyQt widget class, so we can use it within the Stacked Widget control.

Now let’s jump to setting up the main bulk of the UI interactions, so we can finish up with the creation of our own custom widget.

Just like the C# version, we need some variables to hold data such as; if we’re drawing or erasing, the currently selected colour and the current shape number among others. We can quickly declare these under the CreateUI class, before we get to the constructor.

While we’re adding that function call to the class constructor, we might as well add the code to insert the new widget class into the stacked-widget widget. Here we create an instance of the Painter widget class and save a reference to the main class so we can re-call it, as well as set this widget as the current widget on the stacked-widget widget.

That’s the code complete for the main UI, we have just to define how we can use all this date we’ve stored within our new widget to allow users to draw on the widget.

Custom Widget Class

Within our own custom widget, called Painter, we need to start off by defining a few variables which we will use, defining them in the same fashion we did with the variables within the Main UI class.

ParentLink is a reference link to the Main UI parent class so we can data from it such as the colour and size, as well as holding the point arrays.

MouseLoc is the Point of where the mouse is.

LastPos is the Point of where the mouse last was.

It would be good to note that a lot of the variables could be held within the Painter Widget class making it self-sufficient, but I did it this way to show how we can interact between widgets without the need for global variables.

Within the constructor, set MouseLoc and LastPos to default values, and grab the parent and store that under ParentLink.

All the call-backs within the widget will be done by event handlers, such as the mousePressEvent event so we know when the user has clicked on the widget, and the paintEvent event so we can override the paint event and it can draw what we want it to. Let’s start with the mouse events!

The mousePressEvent is triggered when the mouse button is clicked down. It’s a simplistic function which checks what mode is wanted, and set that mode to active as well as setting this as a new shape through the self.ParentLink.ShapeNum += 1 line.

The mouseMoveEvent is triggered when the mouse is moved whilst within the widgets area. The function checks to see if we are activity drawing or erasing, and if we drawing add a new point at mouse pointers current location or if we’re erasing, remove points at the mouse current location.

The last call-back we override in our new widget class is the paintEvent, and simply put, it’s the function which is called to re-draw the widget. We use this function to draw what we want on the widget.

To accompany this function we have a separate function called drawLines as I like to separate out my drawing commands into separate functions so it’s easier to work with.

The drawLines function simply goes through the list of points, and draws a line between conjoining points if there shape numbers m

I hope this encourages you to explore what else you can do with PyQt, and how easy it is to port a C# project over to PyQt and similarly to C++ Qt. There will be a C++ Qt version of this coming soon, which follows the same lines as this one.

Painting on a Widget - Supporting Files

Posted on 26/04/2012 at 01:08

The supporting files can be found at CodeProject here, and include all the source code as well as a final compiled exe application.