- Description: 2D drawing with
QPainter on QPaintDevice (QWidget/QImage/QPixmap/QPicture), coordinate transforms, composition modes, update vs repaint, the QImage/QPixmap/QBitmap family, the Graphics View Framework (QGraphicsScene/QGraphicsView/QGraphicsItem), and OpenGL integration via QOpenGLWidget
- My Notion Note ID: K2A-B3-6
- Created: 2018-03-04
- Updated: 2026-05-18
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
1. Painting Pipeline
- A widget paints itself in response to a paint event. You override
paintEvent(QPaintEvent *), construct a QPainter on this, issue draw calls. Qt double-buffers and composites the result.
QPainter uses a QPaintEngine under the hood to talk to the concrete QPaintDevice:
QPainter → QPaintEngine → QPaintDevice
├─ QWidget (screen)
├─ QPixmap (pixel buffer, GPU-friendly)
├─ QImage (pixel buffer, CPU-friendly)
├─ QPicture (records calls, replays later)
├─ QPrinter (PostScript / PDF)
└─ QOpenGLPaintDevice
- You never instantiate
QPaintEngine directly; QPainter's constructor picks the right one for the device.
2. QPainter Basics
void MyWidget::paintEvent(QPaintEvent *event) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.setPen(QPen(Qt::red, 2));
p.setBrush(Qt::yellow);
p.drawRect(10, 10, 100, 60);
}
- Lifetime:
QPainter must end before the widget paint event returns. Stack-allocating it inside paintEvent is the idiom.
- Painting on
QPixmap / QImage (offscreen):
QPixmap pm(200, 200);
pm.fill(Qt::transparent);
{
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing);
p.drawEllipse(50, 50, 100, 100);
}
label->setPixmap(pm);
- You can only have one
QPainter active on a device at a time. Constructing a second one before the first ended logs "QPainter::begin: A paint device can only be painted by one painter at a time". Use painter.save()/restore() to stack state, not multiple painters.
3. Pens, Brushes, Colors
QPen draws outlines (with width, color, style, cap, join). QBrush fills regions (with color, gradient, pattern, or pixmap).
p.setPen(QPen(Qt::black, 2, Qt::DashLine, Qt::RoundCap, Qt::MiterJoin));
p.setBrush(QBrush(Qt::lightGray, Qt::DiagCrossPattern));
QLinearGradient grad(0, 0, 200, 0);
grad.setColorAt(0.0, Qt::white);
grad.setColorAt(1.0, Qt::darkBlue);
p.setBrush(grad);
- Colors:
Qt::GlobalColor enum — Qt::red, Qt::darkGreen, Qt::transparent, plus 17 named globals.
QColor for arbitrary RGB / RGBA / HSV / HSL / named CSS strings: QColor("#336699"), QColor::fromHslF(0.5, 0.8, 0.5), QColor(255, 0, 0, 128) (50% red).
setRenderHint(QPainter::Antialiasing) is opt-in — without it, lines and ellipses are blocky. Pair with TextAntialiasing and SmoothPixmapTransform for high-quality 2D.
4. Drawing Primitives
p.drawLine(QPointF(0, 0), QPointF(100, 100));
p.drawRect(10, 10, 50, 50);
p.drawRoundedRect(QRectF(10, 10, 100, 60), 8, 8);
p.drawEllipse(QPointF(80, 80), 40, 30);
p.drawArc(rect, 30 * 16, 120 * 16);
p.drawPolygon(QPolygonF{{0, 0}, {50, 0}, {25, 40}});
p.drawText(QRectF(0, 0, 200, 30), Qt::AlignCenter, tr("Hello"));
QPainterPath path;
path.moveTo(0, 0);
path.cubicTo(50, 0, 50, 100, 100, 100);
p.drawPath(path);
p.drawPixmap(QPointF(0, 0), pm);
p.drawImage(QPointF(0, 0), img);
QPainterPath builds a complex path (lines, curves, sub-paths) once and reuses it cheaply — much faster than re-issuing individual primitives every frame.
- Arc angles use the unusual unit of 1/16th of a degree —
360 * 16 == full circle. Easy to forget.
drawText(QRectF, int flags, QString) is the right overload for centered/aligned text inside a bounding box. The int-version is the alignment flag set (Qt::AlignCenter, Qt::AlignVCenter | Qt::AlignLeft, etc.).
QPainter keeps a transformation matrix you can stack with save/restore.
p.save();
p.translate(width() / 2.0, height() / 2.0);
p.rotate(angle);
p.scale(zoom, zoom);
p.drawRect(-50, -50, 100, 100);
p.restore();
worldTransform() / setWorldTransform() set the full QTransform directly. transform() is an alias.
- Logical vs device coordinates:
setWindow(QRect) — logical coordinate range. "What coordinates do my drawing commands use?"
setViewport(QRect) — physical pixel range on the device. "Where on the screen does the logical window map to?"
- Together they let you draw in millimeters, normalized (-1, +1) coords, etc., independent of widget size.
6. Composition Modes
- Controls how source pixels combine with destination pixels — standard Porter–Duff operators + a few extras.
QImage layer(size, QImage::Format_ARGB32_Premultiplied);
layer.fill(Qt::transparent);
QPainter p(&layer);
p.drawImage(0, 0, background);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
p.drawImage(0, 0, foreground);
| Mode |
Effect |
SourceOver (default) |
Standard alpha blend — src over dst. |
DestinationOver |
dst over src. |
Source |
Replace dst with src (ignore dst alpha). |
Clear |
Erase to fully transparent. |
Multiply / Screen / Overlay / Lighten / Darken |
Photoshop-style blend modes. |
- For alpha-blending to work right, the destination
QImage must use a premultiplied format: Format_ARGB32_Premultiplied. Plain Format_ARGB32 works for SourceOver but is slower; non-trivial modes need premultiplied.
7. QImage, QPixmap, QBitmap, QPicture
| Type |
Storage |
Use when |
QImage |
CPU-side pixel buffer (raw bytes accessible via bits(), scanLine()) |
Pixel-level access, image processing, I/O, threaded work |
QPixmap |
Implementation-defined; on raster backends same as QImage, on GPU backends a texture |
Repeated display on screen — fast to blit, slower to inspect |
QBitmap |
QPixmap with depth 1 |
Masks, cursors, monochrome icons |
QPicture |
Records QPainter commands for replay |
Resolution-independent printing, drawing macros |
- Heuristic: if you're going to draw it on screen many times, use
QPixmap. If you need to read or modify pixels (filters, computer vision, file I/O), use QImage. Convert with QPixmap::fromImage(img) and pixmap.toImage().
QImage formats matter: Format_RGB32, Format_ARGB32, Format_ARGB32_Premultiplied, Format_Grayscale8, Format_RGBA8888 (matches OpenGL ordering). Mismatched formats trigger slow per-pixel conversions.
QPicture is niche — used for SVG-like recording/replay before QSvgRenderer was good. Read existing code that uses it; don't write new code with it.
8. update vs repaint
- Both ask the widget to repaint, but they're different:
| Call |
What it does |
update() |
Schedules a paint event; coalesces multiple update() calls in the same event loop iteration into one. |
update(QRect) |
Same, but only the given rect (visible via event->region() / event->rect()). |
repaint() |
Synchronously calls paintEvent immediately — bypasses the event loop. |
- Prefer
update everywhere. repaint is for animations where you must block until the screen has the new frame (rare) or for printing test pages — and it can re-enter handlers in unexpected ways.
- Never call
update() or repaint() from inside paintEvent() — guaranteed infinite paint loop.
- For partial repaints (most common case), inspect
event->region() and only redraw the dirty area:
void Canvas::paintEvent(QPaintEvent *event) {
QPainter p(this);
for (const QRect &r : event->region()) {
p.fillRect(r, background_);
drawItemsInRect(p, r);
}
}
- Background filling:
setBackgroundRole(QPalette::Base) + setAutoFillBackground(true) lets Qt clear the dirty region with the palette color before paintEvent runs.
9. Reading and Writing Images
QImage img("photo.jpg");
QPixmap pm("icon.png");
QImageReader r("big.png");
r.setScaledSize(QSize(800, 600));
r.setClipRect(QRect(0, 0, 500, 500));
QImage scaled = r.read();
qDebug() << r.errorString();
img.save("out.png");
img.save("out.jpg", "JPEG", 85);
QImageWriter w("out.tiff", "TIFF");
w.setCompression(1);
w.write(img);
- Supported formats depend on Qt's image plugins (
imageformats/qjpeg.dll etc.). QImageReader::supportedImageFormats() lists what's available at runtime.
- For loading large images,
QImageReader::setScaledSize is usually cheaper than QImage::scaled — format handlers that support scaled decoding (JPEG most notably) skip pixels at decode time. For formats without native scaled decode, the reader scales after decoding, but you still avoid the round-trip through a full-size QImage.
10. Graphics View Framework
10.1 Scene, View, Item
- For applications that need many interactive 2D shapes — diagram editors, level designers, CAD-lite tools. Three layers:
QGraphicsItem (each shape — square, sprite, custom QObject)
↑
| (lives in)
↓
QGraphicsScene (the world — owns items, dispatches events, indexes by position)
↑
| (rendered by)
↓
QGraphicsView (a QWidget — viewport with scrollbars, applies transforms)
auto *scene = new QGraphicsScene(this);
scene->setSceneRect(-500, -500, 1000, 1000);
scene->addRect(QRectF(-50, -50, 100, 100), QPen(Qt::black), QBrush(Qt::yellow));
scene->addEllipse(QRectF(60, 60, 80, 80), QPen(Qt::black), QBrush(Qt::cyan));
auto *view = new QGraphicsView(scene);
view->setRenderHint(QPainter::Antialiasing);
view->setDragMode(QGraphicsView::ScrollHandDrag);
view->setViewport(new QOpenGLWidget);
- Common scene operations:
addItem(item), removeItem(item), items(), itemAt(pos, transform), selectedItems(), clear().
10.2 Built-in Item Classes
- 8 ready-to-use
QGraphicsItem subclasses:
| Class |
Shape |
QGraphicsRectItem |
Rectangle |
QGraphicsEllipseItem |
Ellipse / circle |
QGraphicsLineItem |
Line segment |
QGraphicsPolygonItem |
Polygon |
QGraphicsPathItem |
Arbitrary QPainterPath |
QGraphicsSimpleTextItem |
Plain text |
QGraphicsTextItem |
Rich text + editable |
QGraphicsPixmapItem |
Image sprite |
- For custom shapes, subclass
QGraphicsItem and override boundingRect() + paint(QPainter*, ...). For signals/slots support, subclass QGraphicsObject (inherits QObject too).
10.3 Three Coordinate Systems
- The thing every Graphics View tutorial trips on:
| System |
Origin |
When you use it |
| Item |
Item's local origin (typically center or top-left) |
paint(), hit testing inside the item, child items |
| Scene |
World origin |
Item placement (setPos), scene queries, persistence |
| View |
Top-left of the viewport widget |
Mouse events, custom view rendering, picking from raw input |
QPointF inScene = view->mapToScene(mouseEvent->position().toPoint());
QPointF inItem = item->mapFromScene(inScene);
QPointF inView = view->mapFromScene(item->scenePos());
scenePos() returns the item's position in scene coords; pos() returns it relative to its parent item (or scene origin if no parent).
setSceneRect(QRect) defines the scrollable region of the world; otherwise the scene grows to enclose all added items automatically.
centerOn(item) / centerOn(QPointF) scrolls the view so the target is centered.
10.4 Item Flags and Events
auto *r = scene->addRect(QRectF(0, 0, 100, 100));
r->setFlag(QGraphicsItem::ItemIsMovable);
r->setFlag(QGraphicsItem::ItemIsSelectable);
r->setFlag(QGraphicsItem::ItemIsFocusable);
r->setAcceptHoverEvents(true);
- Common flags:
ItemIsMovable, ItemIsSelectable, ItemIsFocusable, ItemSendsGeometryChanges (calls itemChange(ItemPositionChange, ...)).
- Items receive event handlers that mirror widget ones:
mousePressEvent(QGraphicsSceneMouseEvent*), hoverEnterEvent, keyPressEvent, wheelEvent, etc. Inside an overridden view handler, always call the base class for unhandled events — otherwise the scene/items never see input:
void MyView::keyPressEvent(QKeyEvent *event) {
if (handleMyShortcut(event)) return;
QGraphicsView::keyPressEvent(event);
}
10.5 Graphics Effects
QGraphicsEffect post-processes an item's pixels.
auto *shadow = new QGraphicsDropShadowEffect;
shadow->setBlurRadius(8);
shadow->setOffset(2, 2);
item->setGraphicsEffect(shadow);
| Effect |
Use |
QGraphicsBlurEffect |
Gaussian blur. |
QGraphicsColorizeEffect |
Tint with a color. |
QGraphicsDropShadowEffect |
Drop shadow. |
QGraphicsOpacityEffect |
Adjustable transparency (cheaper than setOpacity for repeated changes). |
- Effects can be expensive on large items because the framework rasterizes the item into an offscreen pixmap before applying the effect. For very dynamic content, prefer painting the effect yourself in
paint().
QOpenGLWidget (Qt 5.4+) replaced the older QGLWidget. Override initializeGL / resizeGL / paintGL:
class MyGL : public QOpenGLWidget, protected QOpenGLFunctions {
protected:
void initializeGL() override {
initializeOpenGLFunctions();
glClearColor(0, 0, 0, 1);
}
void resizeGL(int w, int h) override {
glViewport(0, 0, w, h);
}
void paintGL() override {
glClear(GL_COLOR_BUFFER_BIT);
}
};
- CMake:
find_package(Qt6 COMPONENTS OpenGL OpenGLWidgets) and link Qt6::OpenGLWidgets. qmake: QT += opengl openglwidgets.
- Mixing
QPainter and raw GL inside the same widget: wrap GL calls between beginNativePainting() / endNativePainting() so Qt knows to flush its painter state.
- For modern Qt projects that need 3D, evaluate Qt Quick 3D / scene-graph-based APIs first — direct
QOpenGLWidget is fine for visualization but more work than declarative options.
12. References