Painting and Custom Drawing
Custom drawing in Qt is the paintEvent override: the widget tells the toolkit it needs to repaint (the update, the repaint), the toolkit emits the paintEvent with a QPainter ready for the widget's surface, and the handler draws with the QPainter - the 2D graphics API Qt uses for all its rendering. The model is the deferred repaint: the widget is not a canvas you draw on once, it is a surface the toolkit repaints on demand (the update schedules, the paintEvent fires, the handler redraws from the app's state), and the double-buffering is the toolkit's (the paint goes to an off-screen buffer, the buffer is blitted - the tear-free render). This chapter covers the paintEvent and the QPainter, the drawing primitives (the paths, the fills, the text), the QPen and the QBrush, the repaint triggers (update, the repaint), and the complete example - the animated clock face.
The paintEvent and the QPainter
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor
from PyQt5.QtCore import Qt
import sys
class Canvas(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(400, 300)
def paintEvent(self, event):
# the event's painter: the QPainter on the widget's
# surface, ready to draw (the toolkit's double-buffer
# is behind it - the draw is to the buffer, the
# buffer is blitted, the tear-free)
painter = QPainter(self)
w, h = self.width(), self.height()
# the background: the fillRect (the clear)
painter.fillRect(0, 0, w, h, QColor(255, 255, 255))
# the path: the moveTo, the lineTo, the closePath
painter.setPen(Qt.NoPen)
painter.setBrush(QColor(30, 100, 200))
painter.beginPath()
painter.moveTo(w * 0.2, h * 0.7)
painter.lineTo(w * 0.5, h * 0.2)
painter.lineTo(w * 0.8, h * 0.7)
painter.closePath()
painter.drawPath(painter.path())
# the text: the drawText (the simple form)
painter.setPen(QColor(0, 0, 0))
painter.drawText(20, h - 20,
"the paintEvent, repainted from state")
painter.end() # the painter's done (the flush)
# the discipline: the paintEvent is a pure function of the
# state - it reads the app's data (the shape, the text, the
# angle) and draws it; the same state always produces the
# same picture. the repaint can happen any time (the resize,
# the occlusion, the update) and it shows the current state,
# because the state is the source and the pixels are a
# derived, disposable view of it
The paintEvent is the paint handler, and it receives the event from which the QPainter is taken (QPainter(self), the painter on the widget's surface). The drawing vocabulary is the QPainter's: the setPen/setBrush for the line and the fill, the beginPath/moveTo/lineTo/closePath for the shape, the drawPath/fillRect for the paint, the drawText for the text. The painter.end() closes the painter (the flush). The discipline is the one that makes the deferred model work: the paintEvent is a pure function of the app's state - it reads the data (the shapes, the text, the clock's angle) and renders it, and the same state always produces the same picture. The double-buffering is the toolkit's (the paint goes to the off-screen buffer, the buffer is blitted to the screen - the tear-free render the widget gets for free), and the repaint can happen any time (the first show, the resize, the occlusion, the explicit update) and always shows the current state, because the state is the source and the pixels are the disposable view.
The QPen and the QBrush
from PyQt5.QtGui import QPen, QBrush, QColor, QLinearGradient
from PyQt5.QtCore import Qt
# the QPen - the line: the color, the width, the style
pen = QPen(QColor(200, 0, 0), 3) # the red, the 3px
pen.setStyle(Qt.DashLine) # the dash
pen.setCapStyle(Qt.RoundCap) # the end cap
pen.setJoinStyle(Qt.RoundJoin) # the corner join
# the painter.setPen(pen) before the draw
# the QBrush - the fill: the solid, the pattern, the
# gradient
brush = QBrush(QColor(0, 150, 0)) # the solid
brush = QBrush(Qt.CrossPattern) # the pattern
grad = QLinearGradient(0, 0, 100, 0)
grad.setColorAt(0.0, QColor(0, 0, 255))
grad.setColorAt(1.0, QColor(255, 0, 0))
brush = QBrush(grad) # the gradient
# the painter.setBrush(brush) before the fill
# the NoPen / the NoBrush: the line / the fill off
# (the Qt.NoPen, the Qt.NoBrush) - the drawPath with the
# NoPen is the fill-only, the NoBrush is the stroke-only
# the painter state: the save / the restore (the stack)
# the painter.save(); ...change...; painter.restore() -
# the pen / the brush / the transform are the painter's
# state, and the save/restore is the scoped change (the
# draw one shape with the thick pen, the rest with the
# default) - the discipline for the multi-style paint
The QPen is the line - the color, the width, the style (the Qt.DashLine, the DotLine), the cap and the join style (the RoundCap, the RoundJoin) - and the QBrush is the fill - the solid color, the pattern (Qt.CrossPattern), or the gradient (the QLinearGradient with the setColorAt stops, the QRadialGradient the radial). The Qt.NoPen and the Qt.NoBrush are the line and the fill off (the drawPath with the NoPen is the fill-only, the NoBrush the stroke-only). The painter.save/painter.restore is the state stack: the pen, the brush, and the transform are the painter's state, and the save/restore is the scoped change (the one shape with the thick pen, the rest with the default) - the discipline for the multi-style paint, the same save/restore the other graphics APIs use. The QPainter is the API, the QPen/QBrush are the style, and the save/restore is the state control - the three that cover the drawing the custom widget needs.
The Repaint Triggers: update and repaint
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtCore import QTimer, Qt
import sys
class Animated(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(300, 200)
self.angle = 0
timer = QTimer(self)
timer.timeout.connect(self.tick)
timer.start(50) # the 20 fps
def tick(self):
self.angle += 10
# the update: the ASYNCHRONOUS repaint - it marks
# the widget dirty and the paintEvent is scheduled
# on the next loop pass; the coalescing (the burst
# of updates is ONE repaint) is the update's gift
self.update()
# the update(region): the sub-region only
# the repaint: the SYNCHRONOUS - it calls the
# paintEvent NOW (the blocking, the no-coalesce) -
# the rare case (the off-screen, the grab), not the
# animation's (the update is the animation's)
def paintEvent(self, event):
from PyQt5.QtGui import QPainter, QColor
import math
p = QPainter(self)
p.fillRect(self.rect(), Qt.white)
cx, cy = self.width() / 2, self.height() / 2
p.setPen(QColor(0, 0, 0))
p.drawLine(cx, cy,
int(cx + 80 * math.cos(math.radians(self.angle))),
int(cy - 80 * math.sin(math.radians(self.angle))))
p.end()
app = QApplication(sys.argv)
a = Animated()
a.show()
sys.exit(app.exec_())
The repaint is triggered by the update (the asynchronous) or the repaint (the synchronous). The update marks the widget (or a sub-region, the update(region)) dirty and schedules the paintEvent on the next event-loop pass - and the coalescing is the gift: a burst of update calls (the drag that updates the position ten times per frame) is ONE repaint, not ten. The repaint is the synchronous - it calls the paintEvent now (the blocking, the no-coalesce) - and it is the rare case (the off-screen render, the grab), not the animation's. The animation's pattern is the two-step: the state changes (the timer advances the angle), and the update schedules the repaint, and the paintEvent renders the new state. The example is the complete loop - the QTimer at 50 milliseconds (the 20 fps), the tick that advances the angle and calls the update, the paintEvent that draws the hand from the angle - which is the architecture for every animated widget: the state, the timer that changes it, the update that schedules the paint, and the paintEvent that is the pure function of the state.
The Working Rules
- The
paintEventis a pure function of the state. It reads the app's data and draws it; the same state produces the same picture. The state is the source, the pixels are the disposable view - the repaint can happen any time and always shows the current state. - The
updateis the asynchronous repaint, therepaintis the synchronous. The animation is theupdate(the coalesced, the next-pass); therepaintis the rare blocking case (the off-screen, the grab). Theupdate(region)is the sub-region for the partial redraw. - The
QPenis the line, theQBrushis the fill, thesave/restoreis the state. TheNoPen/NoBrushare the off; thesave/restoreis the scoped style change (the one shape with the thick pen, the rest with the default) - the discipline for the multi-style paint. - The
QPainteris on the widget's surface, theendcloses it. ThepaintEventtakes theQPainter(self), draws, and thepainter.end()flushes - the double-buffering is the toolkit's (the tear-free), the painter is the API the draw uses. - The timer drives the animation, the
updatedrives the paint. TheQTimerchanges the state on the schedule, theupdateschedules thepaintEvent, and thepaintEventrenders - the three-part loop the animated widget is, the state plus the timer plus the repaint.
Summary
Custom drawing in Qt is the paintEvent override: the widget marks itself dirty with the update (the asynchronous, the coalesced) or the repaint (the synchronous, the rare), the toolkit emits the paintEvent, and the handler draws with the QPainter on the widget's surface (the QPainter(self), the painter.end() the close), the double-buffering the toolkit's (the tear-free). The drawing vocabulary is the QPainter's - the beginPath/moveTo/lineTo/closePath for the shape, the drawPath/fillRect for the paint, the drawText for the text - and the style is the QPen (the line: the color, the width, the style, the cap, the join) and the QBrush (the fill: the solid, the pattern, the gradient), with the Qt.NoPen/NoBrush the off and the painter.save/restore the scoped state change. The discipline is the one that makes the deferred model work: the paintEvent is a pure function of the state, the state is the source, and the pixels are the disposable view repainted whenever the toolkit asks. The animation is the three-part loop - the QTimer that changes the state, the update that schedules the paint, and the paintEvent that renders it - and the complete example (the clock) is that loop in miniature.