After going through many examples in previous chapters that introduce you to the fundamentals for building GUIs, Chapter 9 finally allows for you to explore your creative and artistic side through drawing and animation in PyQt5.
Graphics in PyQt is done primarily with the QPainter API. PyQt’s painting system handles drawing for text, images, and vector graphics and can be done on a variety of surfaces, including QImage, QWidget, and QPrinter. With QPainter you can enhance the look of existing widgets or create your own.
The main components of the painting system in PyQt are the QPainter, QPaintDevice, and QPaintEngine classes. QPainter performs the drawing operations; a QPaintDevice is an abstraction of two-dimensional space which acts as the surface that QPainter can paint on; and QPaintEngine is the internal interface used by the QPainter and QPaintDevice classes for drawing.
In this chapter, we are going to be taking a look at 2D graphics, covering topics such as drawing simple lines and shapes, designing your own painting application, and animation. If you are interested in creating GUIs that work with 3D visuals, Qt also has support for OpenGL, which is software that renders both 2D and 3D graphics.
An introduction to QPainter and other classes used for drawing PyQt
Creating tool tips using QToolTip
Animating objects using QPropertyAnimation and pyqtProperty
How to set up a Graphics View and a Graphics Scene for interacting with items in a GUI window
A new widget for selecting values in a bounded range, QSlider
Handling mouse events with event handlers
PyQt’s four image handling classes
Creating your own custom widgets using PyQt
Introduction to the QPainter Class
Whenever you need to draw something in PyQt, you will more than likely need to work with the QPainter class. QPainter provides functions for drawing simple points and lines, complex shapes, text, and pixmaps. We have looked at pixmaps in previous chapters in applications where we needed to display images. QPainter also allows you to customize a variety of its settings, such as rendering quality or changing the painter’s coordinate system. Drawing can be done on a paint device, which are two-dimensional objects created from the different PyQt classes that can be painted on with QPainter.
Drawing relies on a coordinate system for specifying the position of points and shapes and is typically handled in the paint event of a widget. The default coordinate system for a paint device has the origin at the top-left corner, beginning at (0, 0). The x values increase to the right and y values increase going down. Each (x, y) coordinate defines the location of a single pixel.
This code gives examples for drawing with the QPainter class
Explanation
This program introduces quite a few new classes, a majority of them imported from the QtGui module. QtGui provides us with the tools we need for 2D graphics, imaging, and fonts. The QPoint and QRect classes imported from QtCore are used to define points and rectangles specified by coordinate values in the window’s plane.
The Drawing class inherits from QWidget, and all drawing will occur on the widget’s surface.
The paintEvent( ) Method
Drawing occurs between the begin() and end() methods on the paint device, referenced by self. The drawing is handled in between these two methods. Using begin() and end() is not required. You could construct a painter that takes as a parameter the paint device. However, begin() and end() can be used to catch any errors should the painter fail.
The QColor, QPen, and QBrush Classes
Some of the settings that can be modified include the color, width, and styles used to draw lines and shapes. The QColor class provides access to different color schemes, for example, RGB, HSV, and CMYK values. Colors can be specified by using either RGB hexadecimal strings, '#112233'; predefined color names, Qt.blue or Qt.darkBlue; or RGB values, (233, 12, 43). QColor also includes an alpha channel used for giving colors transparency, where 0 is completely transparent and 255 is completely opaque. Listing 9-1 demonstrates all three of these types.
If you wish to create multiple lines or shapes with different pens and brushes, make sure to call setPen() and/or setBrush() each time they need to be changed. Otherwise, QPainter will continue to use the pen and brush settings from the previous call.
Calling QPainter.begin() will reset all the painter settings to default values.
Drawing Points and Lines
The drawPoint() and other methods are specified using integer values. Some of the drawing methods allow you to also use floating-point values. Rather than import the QPoint and QRect classes, you should use QPointF and QRectF.
Drawing Text
Drawing Two-Dimensional Shapes
The cubicTo() method can be used to draw a parametric curve, also called a Bézier curve, from the starting position we moved to and the ending position, (30, 560). The first two points, (30, 420) and (65, 500), in cubicTo() are used to influence how the line curves between the starting and ending points. The next components of path are a line drawn with lineTo() and another curve. The abstract shape is closed with closeSubpath(), and the path is drawn using drawPath().
Drawing Gradients
Gradients can be used along with QBrush to fill the inside of shapes. There are three different types of gradient styles in PyQt – linear, radial, and conical. For this example, we will use the QLinearGradient class to interpolate colors between two start and end points. The result can be seen in Figure 9-5.
Project 9.1 – Painter GUI
There are many digital art applications out there, filled to the brim with tools for drawing, painting, editing, and creating your own art on the computer. With QPainter, you could manually code each individual line and shape one by one. However, rather than going through that painstaking process to create digital works of art, the painter GUI project lays the foundation for creating your drawing application that could pave the way for a smoother drawing process. The interface can be seen in Figure 9-6.
Painter GUI Solution
The code for creating the painter GUI
The final GUI can be seen in Figure 9-6.
Explanation
The painter GUI allows users to draw images on the canvas area. Unlike in the previous example where painting occurred on the main widget, for this example we will see how to subclass QLabel and reimplement its painting and mouse event handlers. The handling for some of the event handlers in this application was adapted from the Qt document web site.1
The program contains two classes – the Canvas class for drawing and the PainterWindow class for creating the menu and toolbar.
Creating the Canvas Class
Subclassing QLabel and reimplementing its paintEvent() method is a much easier way to manage drawing on a label object. We then create a pixmap and pass it to setPixmap(). Since QPixmap can be used as a QPaintDevice, using a pixmap makes handling the drawing and displaying of pixels much simpler. Also, using QPixmap means that we can set an initial background color using fill().
mouse_track_label – A label for displaying the mouse’s current position
eraser_selected – True if the eraser is selected
antialiasing_status – True if the user has checked the menu item for using antialiasing
last_mouse_pos – Keep track of the mouse’s last position when the left mouse button is pressed or when the mouse moves
drawing – True if the left mouse button is pressed, indicating the user might be drawing
pen_color, pen_width – Variables that hold the initial values of the pen and brush
Since the user will use the mouse to draw in the GUI window, we need to handle the events when the mouse button is pressed or released and when the mouse is moved. We can use setMouseTracking() to keep track of the mouse cursor and return its coordinates in mouseMoveEvent() . Those coordinates are displayed in the status bar.
If the user presses the left mouse button while the cursor is in the window, we set drawing equal to True and store the current value of the mouse in last_mouse_pos. Then drawOnCanvas() is called in the moveMouseEvent() .
The user has four choices in the toolbar, including a pencil, marker, eraser, and a color selector. If a user selects a tool from the toolbar, a signal triggers the selectDrawingTool() slot, updating the current tool and painter settings.
The actual drawing is handled in drawOnCanvas(). An instance of QPainter is created that draws on the pixmap. We also check whether the eraser_selected is True or False to test whether we can draw or erase. The reimplementation of the paintEvent() creates a painter for the canvas area and draws the pixmap using drawPixmap(). By first drawing on a QPixmap in the drawOnCanvas() method and then copying the QPixmap onto the screen in the paintEvent(), we can ensure that our drawing won’t be lost if the window is minimized.
The Canvas class also includes methods for clearing and saving the pixmap.
Creating the PainterWindow Class
The PainterWindow class creates the main menu, toolbar, tool tips for each of the buttons in the toolbar, and an instance of the Canvas class.
The File menu contains actions for clearing the canvas, saving the image, and quitting the application. The Tools menu contains a checkable menu item that turns antialiasing on or off.
The toolbar creates the actions and icons for the drawing tools. If a button is clicked, it triggers the Canvas class’s selectDrawingTool() slot.
The reimplemented leaveEvent() handles if the mouse cursor moves outside the main window and sets the mouse_track_label’s visibility to False .
Handling Mouse Movement Events
Mouse move events occur whenever the mouse is moved, or when a mouse button is pressed or released.
Creating Tool Tips for Widgets
Project 9.2 – Animation with QPropertyAnimation
The following project serves as an introduction to Qt’s Graphics View Framework and the QAnimationProperty class. With the framework, applications can be created that allow users to interact with the items in the window.
- 1.
A scene created from the QGraphicsScene class. The scene creates the surface for managing 2D graphical items and must be created along with a view to visualize a scene.
- 2.
QGraphicsView provides the view widget for visualizing the elements of a scene, creating a scroll area that allows user to navigate in the scene.
- 3.
Items in the scene are based on the QGraphicsItem class. Users can interact with graphical items through mouse and key events, and drag and drop. Items also support collision detection.
QAnimationProperty is used to animate the properties of widgets and items. Animations in GUIs can be used for animating widgets. For example, you could animate a button that grows, shrinks, or rotates, or text that smoothly moves around in the window, or create widgets that fade in and out or change colors. QAnimationProperty only works with objects that inherit the QObject class. QObject is the base class for all objects created in PyQt.
Qt provides a number of simple items that inherit QGraphicsItem, including basic shapes, text, and pixmaps. These items already provide support for mouse and keyboard interaction. However, QGraphicsItem does not inherit QObject. Therefore, if you want to animate a graphics item with QPropertyAnimation, you must first create a new class that inherits from QObject and define new properties for the item.
Animation Solution
Code for animating objects in Qt’s Graphics View Framework
A still image from the animation project is shown in Figure 9-8.
Explanation
Since we are going to create a Graphics Scene, we need to import QGraphicsScene, QGraphicsView, and one of the QGraphicsItem classes. For this program, we import QGraphicsPixmapItem since we will be working with pixmaps. New Qt properties can be made using pyqtProperty.
The goal of this project is to animate two items, a car and a tree, in a QGraphicsScene. Let’s first create the objects and the animations that will be placed into the scene. For this scene, the two items will move at the same time. Qt provides other classes for handling groups of animations, but for this example, the QPropertyAnimation and the animations list are used to keep track of the multiple animations.
Create the car item as an instance of the Objects class, and pass car and the position setter to QPropertyAnimation(). QPropertyAnimation will update the position’s value so that the car moves across the scene. To animate items, use setDuration() to set the amount of time the object moves in milliseconds, and specify start and end values of the property with setStartValue() and setEndValue(). The animation for the car is 6 seconds and starts off-screen on the left side and travels to the right. The tree is set up in a similar manner, but traveling in the opposite direction.
The setKeyValueAt() method allows us to create key frames at the given steps with the specified QPointF values. Using the key frames, the car and tree will appear to slow down as they pass in the scene. The start() method begins the animation.
Setting up a scene is simple. Create an instance of the scene, set the scene’s size, add objects and their animations using addItem(), and then call setScene().
Finally, a scene can be given a background using QBrush. If you want to use a background image, you will need to reimplement the QGraphicView’s drawBackground() method like we do in this example.
Project 9.3 – RGB Slider Custom Widget
For this chapter’s final project, we are going to take a look at making a custom, functional widget in PyQt. While PyQt offers a variety of widgets for building GUIs, every once in a while you might find yourself needing to create your own. One of the benefits of creating a customized widget is that you can either design a general widget that can be used by many different applications or create an application-specific widget that allows you to solve a specific problem.
Modifying the properties of PyQt’s widgets by using built-in methods, such as setAlignment(), setTextColor(), and setRange()
Creating style sheets to change a widget’s existing behavior and appearances
Subclassing widgets and reimplementing event handlers, or adding properties dynamically to QObject classes
Creating composite widgets which are made up of two more types of widgets and arranged together using a layout
Designing a completely new widget that subclasses QWidget and has its own unique properties and appearance
The RGB slider, shown in Figure 9-9, actually is created by combining a few of the preceding techniques listed. The widget uses Qt’s QSlider and QSpinBox widgets for selecting RGB values and displays the color on a QLabel. The look of the sliders is modified by using style sheets. All of the widgets are then assembled into a parent widget which we can then import into other PyQt applications.
PyQt’s Image Handling Classes
In previous examples, we have only worked with QPixmap for handling image data. Qt actually provides four different classes for working with images, each with their own special purposes.
QPixmap is the go-to choice for displaying images on the screen. Pixmaps can be used on QLabel widgets, or even on push buttons and other widgets that can display icons. QImage is optimized for reading, writing, and manipulating images, allowing direct access to an image’s pixel data. QImage can also act as paint device.
Conversion between QImage and QPixmap is also possible. One possibility for using the two classes together is to load an image file with QImage, manipulate the image data, and then convert the image to a pixmap before displaying it on the screen. The RGB slider widget shows an example of how to convert between the two classes.
Our custom widget uses two types of widgets for selecting RGB values – QSpinBox, which was introduced in Chapter 4, and a new widget, QSlider.
The QSlider Widget
The QSlider class provides a developer with a tool for selecting integer values within a bounded range. Sliders provide users with a convenient means for quickly selecting values or changing settings with only the movement of a simple handle. By default, sliders are arranged vertically, but that can be changed by passing Qt.Horizontal to the constructor.
An example of stylized slider widgets can be seen in Figure 9-9.
RGB Slider Solution
The RGB slider, which can be found in Listing 9-4, is a custom widget created by combining a few of Qt’s built-in widgets – QLabel, QSlider, and QSpinBox. The appearance of the sliders is adjusted using style sheets so that they give visual feedback to the user about which RGB value they are adjusting. The sliders and spin boxes are connected together so that their values are in sync and so that the user can see the integer value on the RGB scale. The RGB values are also converted to hexadecimal format and displayed on the widget.
Code for the RGB slider custom widget
An example of the stand-alone widget can be seen in Figure 9-9.
Explanation
We need to import quite a few classes. One worth noting, qRgb, is actually a typedef that creates an unsigned int representing the RGB value triplet (r, g, b).
The style sheet that follows the imports is used for changing the appearance of the sliders. We want to modify their appearance so that they give the user more feedback about which RGB values are being changed. Each slider is given an ID selector using the setObjectName() method . If no ID selector is used in the style sheet, then that style is applied to all of the QSlider objects. The sliders use linear gradients so that users can get a visual representation of how much of the red, green, and blue colors are being used. Refer back to Chapter 6 for a refresher about style sheets.
The contents of the label are then scaled to fit the window’s size.
Next, we create each of the red, green, and blue QSlider and QSpinBox widgets and the labels for displaying the hexadecimal value. The sliders’ maximum values are set to 255, since RGB values are in the range of 0–255. These widgets are then arranged using QGridLayout.
Updating the Sliders and Spin Boxes
These widgets are contained in the rgb_widgets QWidget. The last thing to do is to arrange the widgets in the main layout.
Updating the Colors
The new_color is then passed to updateColorInfo(). Green and blue colors are handled in a similar fashion. Next we have to create a QColor from the qRgb value and store it in current_val. The QImage color_display is updated with fill(), which is then converted to a QPixmap and displayed on the cd_label.
The last thing to do is to update the hexadecimal labels using QColor.name(). This function returns the name of the color in the format “#RRGGBB”.
Adding Methods to a Custom Widget
The options for methods that you could create for a custom widget are numerous. One option is to create methods that allow the user to modify the behavior or appearance of your custom widget. Another option is to use the event handlers to check for keyboard or mouse events that could be used to interact with your GUI.
getPixelValue() is a reimplementation of the mousePressEvent() event handler. If an image is passed into the RGBSlider constructor, then _image is not None, and the user can click points in the image to get their corresponding pixel values. QColor.pixel() gets a pixel’s RGB values. Then, we update current_val to use the selected pixel’s red, blue, and green values. These values are then passed back into the functions that will update the sliders, spin boxes, labels, and QImage.
The following example demonstrates how to implement the color selecting feature.
RGB Slider Demo
One reason for creating a custom widget is so that it can be used in other applications. The following program is a short example of how to import and set up the RGB slider shown in Project 9.3. For this example, an image is displayed in the window alongside the RGB slider. Users can click points within the image and see the RGB and hexadecimal values change in real time.
Code that shows an example for using the RGB slider widget
Explanation
In the ImageDemo class , set up the window, create an instance of the RGB slider, and load an image. For this application, we are still creating the image as an instance of QImage and then converting it to a QPixmap. QImage is used so that we can get access to the image’s pixel information.
If you only want to use the slider to get different RGB or hexadecimal values, then the application is finished. Or you could add other functionality to the RGB slider to use in your own projects.
Summary
PyQt5’s graphics and painting system is an extensive topic that could be an entire book by itself. The QPainter class is important for performing the painting on widgets and on other paint devices. QPainter works together with the QPaintEngine and QPaintDevice classes to provide the tools you need for creating two-dimensional drawing applications.
In Chapter 9, we have taken a look at some of QPainter’s functions for drawing lines, primitive and abstract shapes. Together with QPen, QBrush, and QColor, QPainter is able to create some rather beautiful digital images. To materialize this concept, we created a simple painting application. Hopefully, you use that application and add even more drawing features.
We also saw how to create properties for objects made from the QObject class and then animate those objects in Qt Graphics View Framework. It is not covered in this book, but you could use the Graphics View to create a GUI with items that are interactive.
Finally, one of PyQt’s strengths comes from being able to customize the built-in widgets or to create your own widget that can then be imported seamlessly into other applications.
In Chapter 10, we will learn about data handling using databases and PyQt.