Qt Painting and Graphics View


  • 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);                            // construct on a QPaintDevice
    p.setRenderHint(QPainter::Antialiasing);
    p.setPen(QPen(Qt::red, 2));
    p.setBrush(Qt::yellow);
    p.drawRect(10, 10, 100, 60);
}                                                // destructor calls end() — releases the device
  • 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);                        // initialize alpha
{
    QPainter p(&pm);
    p.setRenderHint(QPainter::Antialiasing);
    p.drawEllipse(50, 50, 100, 100);
}                                                // p ends here, pm is now drawn
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));

// Gradient brush
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);              // angles in 1/16th degrees
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 degree360 * 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.).

5. Coordinate Transforms

  • QPainter keeps a transformation matrix you can stack with save/restore.
p.save();
p.translate(width() / 2.0, height() / 2.0);     // origin → center
p.rotate(angle);                                 // degrees
p.scale(zoom, zoom);
p.drawRect(-50, -50, 100, 100);                  // centered square
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);   // default
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

// Construct from file (extension drives the format)
QImage img("photo.jpg");
QPixmap pm("icon.png");

// Or via reader for finer control
QImageReader r("big.png");
r.setScaledSize(QSize(800, 600));    // decode at target size — faster than load + scale
r.setClipRect(QRect(0, 0, 500, 500));
QImage scaled = r.read();
qDebug() << r.errorString();         // check on failure

// Write
img.save("out.png");                                 // format from extension
img.save("out.jpg", "JPEG", /*quality=*/85);         // explicit format + quality

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);            // optional GPU acceleration
  • 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
  • Mapping helpers:
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);    // <-- forward to scene/items
}

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().

11. OpenGL with QOpenGLWidget

  • 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);
        // compile shaders, upload geometry
    }
    void resizeGL(int w, int h) override {
        glViewport(0, 0, w, h);
    }
    void paintGL() override {
        glClear(GL_COLOR_BUFFER_BIT);
        // draw
    }
};
  • 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