Qt 2D 绘制实战与性能优化深度解析

引言

上一篇文章解析了 Qt 2D 绘制系统的三层架构原理,本文聚焦实战。从绘制目标选型(QPixmap vs QImage vs QPicture)、双缓冲绘制、自定义控件 paintEvent 优化、图形基元的高效组合,到 Qt 6 硬件加速与 Vulkan 后端------每个实战问题都给出可落地的代码方案,并解释背后的性能原理。


1. QPixmap、QImage、QPicture 三者选型

这是 Qt 绘图中最容易选错的问题。三者的设计目标和使用场景截然不同。

特性 QPixmap QImage QPicture
存储位置 GPU/显存(平台后端) CPU 内存 命令记录缓冲区
线程安全 ❌ 主线程 ✅ 所有线程 ✅ 所有线程
像素访问 ❌ 通过 QPainter ✅ 直接像素操作 ❌ 不支持
适用场景 屏幕显示、缓存 图像处理、文件 IO 命令录制/回放
缩放质量 依赖平台 高质量(冷启动慢) N/A

1.1 QPixmap:屏幕显示首选

QPixmap 内部绑定平台后端(Windows GDI、macOS Core Graphics、X11),绘制到屏幕时零拷贝,是 UI 显示的标准选择:

cpp 复制代码
// 典型用法:从文件加载并显示
QPixmap pixmap(":/resources/icon.png");
painter.drawPixmap(0, 0, pixmap);

// 缩放(保持宽高比)
pixmap = pixmap.scaled(targetSize, Qt::KeepAspectRatio,
                       Qt::SmoothTransformation);
// 注意:SmoothTransformation 比 FastTransformation 质量高但慢

性能陷阱:QPixmap::scaled() 每次调用都创建新对象,在 paintEvent 中调用会严重拖慢帧率:

cpp 复制代码
// ❌ 错误:paintEvent 中每次都缩放
void MyWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    QPixmap pix = QPixmap(":/img.png").scaled(size(), Qt::KeepAspectRatio);
    painter.drawPixmap(0, 0, pix);
}

// ✅ 正确:预缩放,缓存
class MyWidget : public QWidget {
    QPixmap m_cachedPix;
    void loadImage() {
        m_cachedPix = QPixmap(":/img.png").scaled(
            size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    }
protected:
    void paintEvent(QPaintEvent *) override {
        QPainter painter(this);
        painter.drawPixmap(0, 0, m_cachedPix);
    }
};

1.2 QImage:像素级操作与跨线程

需要直接操作像素数据(如图像滤镜)时必须用 QImage:

cpp 复制代码
// 从文件加载 QImage
QImage image("photo.jpg");
if (image.format() != QImage::Format_ARGB32)
    image = image.convertToFormat(QImage::Format_ARGB32);

// 直接像素操作:亮度调整
for (int y = 0; y < image.height(); ++y) {
    QRgb *line = reinterpret_cast<QRgb *>(image.scanLine(y));
    for (int x = 0; x < image.width(); ++x) {
        int r = qRed(line[x]) + 30;
        int g = qGreen(line[x]) + 30;
        int b = qBlue(line[x]) + 30;
        line[x] = qRgb(
            qBound(0, r, 255),
            qBound(0, g, 255),
            qBound(0, b, 255)
        );
    }
}

// 或使用更快的 setPixelColor(但更慢)
// image.setPixelColor(x, y, newColor);

跨线程处理

cpp 复制代码
// Worker 线程中处理图像(QImage 是线程安全的)
class ImageProcessor : public QObject {
    Q_OBJECT
public slots:
    void process(const QString &path) {
        QImage src(path);
        QImage dst(src.size(), QImage::Format_ARGB32);

        // 在工作线程中处理,不阻塞 UI
        for (int y = 0; y < src.height(); ++y) {
            // 逐行处理...
        }

        // 处理完成后通过信号传递回主线程
        emit finished(dst);
    }
signals:
    void finished(const QImage &result);
};

1.3 QPicture:绘制命令录制与回放

QPicture 记录 QPainter 的所有绘制命令,供后续无限次回放:

cpp 复制代码
// 录制
QPicture picture;
{
    QPainter p(&picture);
    p.setPen(Qt::red);
    p.setBrush(Qt::blue);
    p.drawRect(10, 10, 100, 50);
    p.drawText(10, 80, "Recorded!");
}
// 录制完成,picture 包含绘制命令序列

// 回放(可以无数次重复调用)
void MyWidget::paintEvent(QPaintEvent *) {
    QPainter painter(this);
    painter.drawPicture(0, 0, picture); // 极快,无需重算
}

源码解析

cpp 复制代码
// qtbase/src/gui/painting/qpicture.cpp
void QPicture::play(QPainter *painter) const
{
    // 从内部缓冲区读取录制的绘制命令
    // 并逐一在目标 painter 上回放
    Q_D(const QPicture);
    QDataStream s(d->data);
    s.setByteOrder(QDataStream::LittleEndian);

    quint8 cmd;
    while (!s.atEnd()) {
        s >> cmd;
        switch (cmd) {
        case PDevCmd_drawRect:
            // 从数据流中读取参数并执行绘制
            break;
        // ... 其他命令
        }
    }
}

2. paintEvent 优化:双缓冲与最小重绘

2.1 背景:Qt 的重绘机制

Qt 的 paintEvent 由以下情况触发:

  • 显式调用 update() / repaint()
  • 窗口显示/隐藏/尺寸变化
  • 遮挡区域恢复可见
  • 顶层窗口移动

最小重绘原则 :Qt 默认只重绘需要更新的区域(通过 QRegion 计算脏区域):

cpp 复制代码
// qtbase/src/widgets/kernel/qwidget.cpp
void QWidget::repaint()
{
    // 立即重绘(同步)
    // 等价于 update() + 事件循环立即处理
}

void QWidget::update()
{
    // 异步:将 widget 加入待重绘队列
    // 多个 update() 调用会被合并为一次 paintEvent
    // 合并策略通过 QWidgetPrivate::updateOnScreenTimer 实现
}

2.2 双缓冲:消除闪烁

双缓冲是最经典的 UI 绘制优化模式------先画到离屏图像,再一次性拷贝到屏幕:

cpp 复制代码
class DoubleBufferWidget : public QWidget {
    QPixmap m_backBuffer;
    QSize m_lastSize;

protected:
    void resizeEvent(QResizeEvent *event) override {
        // 窗口大小变化时重建离屏缓冲区
        if (size() != m_lastSize) {
            m_backBuffer = QPixmap(size());
            m_backBuffer.fill(Qt::white); // 可选:预填充背景
            m_lastSize = size();
        }
        QWidget::resizeEvent(event);
    }

    void paintEvent(QPaintEvent *) override {
        // === 离屏绘制(Back Buffer)===
        QPainter bufferPainter(&m_backBuffer);
        drawContent(bufferPainter); // 自定义绘制逻辑

        // === 一次性推送到屏幕 ===
        QPainter screenPainter(this);
        screenPainter.drawPixmap(0, 0, m_backBuffer);
    }

    void drawContent(QPainter &painter) {
        // 复杂的绘制逻辑...
        painter.setRenderHint(QPainter::Antialiasing);
        painter.fillRect(rect(), Qt::white);
        // ... 大量绘制操作
    }
};

双缓冲的核心原理:减少屏幕上绘制操作的次数,避免逐个图形元素绘制时的闪烁。

2.3 QBackingStore:Qt 官方双缓冲

Qt Widgets 框架内置了双缓冲支持,通过 QBackingStore 实现:

cpp 复制代码
// 自定义 widget 启用 Qt 官方双缓冲
void MyWidget::paintEvent(QPaintEvent *) {
    // QWidget 的 backingStore 默认已经做了离屏缓冲
    // 只需要关注绘制内容本身
    QPainter painter(this);

    // 如果需要强制离屏缓冲:
    // QWidget::setAttribute(Qt::WA_PaintOnScreen, false);
    // QWidget::setAttribute(Qt::WA_OpaquePaintEvent, true);
}

3. 高效绘制:批量操作与路径合并

3.1 批量绘制减少调用开销

QPainter 的每次绘制调用都有固定开销(状态检查、引擎调度)。将多个同类图形合并可以显著提升性能:

cpp 复制代码
// ❌ 错误:逐个绘制 1000 个矩形,1000 次调用
for (int i = 0; i < 1000; ++i) {
    painter.drawRect(QRectF(i * 10, 0, 8, 100));
}

// ✅ 正确:一次性绘制多个矩形
QPainterPath path;
for (int i = 0; i < 1000; ++i) {
    path.addRect(QRectF(i * 10, 0, 8, 100));
}
painter.fillPath(path, Qt::blue); // 一次调用完成 1000 个矩形

3.2 QPainterPath 的布尔运算

利用路径的布尔运算合并区域:

cpp 复制代码
// 合并多个独立区域为一个路径
QPainterPath combined;
combined.addRect(rect1);
combined.addRect(rect2);
combined.addEllipse(ellipseRect);
painter.fillPath(combined, gradient); // 一次填充替代多次

3.3 脏区域更新:只重画必要的部分

cpp 复制代码
// 在数据变化时只更新变化区域
void ChartWidget::updateData(const QVector<QPointF> &newData)
{
    // 计算新的数据范围
    QRectF newBounds = boundingRect(newData);
    QRectF dirty = oldBounds.united(newBounds);

    // 只更新包含数据的区域
    update(dirty.toRect());
}

void ChartWidget::paintEvent(QPaintEvent *event)
{
    // 只绘制事件中的脏区域
    QPainter painter(this);
    painter.setClipRegion(event->region()); // 关键:裁剪到脏区域

    // 绘制逻辑...
}

4. 图形基元绘制实战

4.1 绘制正弦波形(高频更新场景)

cpp 复制代码
class SineWaveWidget : public QWidget {
    QVector<QPointF> m_points;
    int m_phase = 0;

public:
    SineWaveWidget(QWidget *parent = nullptr) : QWidget(parent) {
        setAttribute(Qt::WA_OpaquePaintEvent);
        setAttribute(Qt::WA_NoSystemBackground);
        // 60 FPS 定时器
        QTimer *timer = new QTimer(this);
        connect(timer, &QTimer::timeout, this, [this] {
            m_phase = (m_phase + 5) % 360;
            update(); // 触发重绘
        });
        timer->start(16); // ~60 FPS
    }

protected:
    void paintEvent(QPaintEvent *) override {
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);
        painter.setPen(QPen(Qt::cyan, 2));

        QPainterPath path;
        int w = width(), h = height();
        int amp = h / 3;

        path.moveTo(0, h / 2);
        for (int x = 0; x < w; ++x) {
            double rad = (m_phase + x * 2) * M_PI / 180.0;
            double y = h / 2 - amp * sin(rad);
            path.lineTo(x, y);
        }

        painter.drawPath(path);
    }
};

4.2 绘制雷达图(多边形与渐变)

cpp 复制代码
void drawRadarChart(QPainter &painter, const QPointF &center,
                    double radius, int axes, const QVector<double> &values)
{
    // 绘制轴线
    painter.setPen(QPen(Qt::gray, 1));
    for (int i = 0; i < axes; ++i) {
        double angle = i * 2 * M_PI / axes - M_PI / 2;
        painter.drawLine(center,
            QPointF(center.x() + radius * cos(angle),
                    center.y() + radius * sin(angle)));
    }

    // 绘制网格圆
    for (int r = 1; r <= 4; ++r) {
        painter.drawEllipse(center,
            radius * r / 4, radius * r / 4);
    }

    // 绘制数据多边形
    QPainterPath dataPath;
    for (int i = 0; i < axes; ++i) {
        double angle = i * 2 * M_PI / axes - M_PI / 2;
        double value = values[i];
        QPointF pt(center.x() + radius * value * cos(angle),
                   center.y() + radius * value * sin(angle));
        if (i == 0) dataPath.moveTo(pt);
        else dataPath.lineTo(pt);
    }
    dataPath.closeSubpath();

    // 渐变填充
    QRadialGradient gradient(center, radius);
    gradient.setColorAt(0, QColor(255, 100, 100, 180));
    gradient.setColorAt(1, QColor(255, 100, 100, 30));
    painter.fillPath(dataPath, gradient);
    painter.drawPath(dataPath);
}

5. 内存优化:缓存策略

5.1 层次化缓存

不同数据的缓存策略不同:

cpp 复制代码
class ChartRenderer : public QObject {
    // L1 缓存:QPixmap(GPU 缓存,最快)
    QPixmap m_staticBgCache; // 静态背景,如网格线
    bool m_bgDirty = true;

    // L2 缓存:QPicture(命令缓存,中等)
    QPicture m_chartPicture; // 图表命令录制
    bool m_chartDirty = true;

    // 动态数据:直接绘制
    QVector<QPointF> m_liveData; // 实时数据,直接绘制

    void renderStaticBackground() {
        if (!m_bgDirty) return;
        m_staticBgCache = QPixmap(size());
        m_staticBgCache.fill(Qt::transparent);
        QPainter p(&m_staticBgCache);
        drawGrid(p);
        m_bgDirty = false;
    }

    void paintEvent(QPaintEvent *) {
        QPainter painter(this);

        // L1: 静态背景(缓存的 QPixmap)
        renderStaticBackground();
        painter.drawPixmap(0, 0, m_staticBgCache);

        // L2: 图表(缓存的 QPicture)
        if (m_chartDirty) {
            m_chartPicture = QPicture();
            QPainter p(&m_chartPicture);
            drawChart(p);
            m_chartDirty = false;
        }
        painter.drawPicture(0, 0, m_chartPicture);

        // L3: 动态数据(直接绘制)
        painter.setPen(QPen(Qt::red, 2));
        painter.drawPolyline(m_liveData.data(), m_liveData.size());
    }
};

5.2 缓存失效策略

缓存最大的问题是"何时失效":

cpp 复制代码
void ChartRenderer::setData(const QVector<QPointF> &data)
{
    // 数据变化:只影响动态层,静态背景不变
    m_liveData = data;
    // m_staticBgCache 保持有效,不需要重建
}

void ChartRenderer::setGridVisible(bool visible)
{
    // 网格参数变化:重建静态背景
    m_bgDirty = true;
    // 图表也可能受影响
    m_chartDirty = true;
}

void ChartRenderer::resizeEvent(QResizeEvent *event)
{
    // 尺寸变化:所有层都要重建
    m_bgDirty = true;
    m_chartDirty = true;
}

6. Qt 6 硬件加速与 Vulkan 后端

6.1 启用硬件加速

Qt 6 默认启用硬件加速,在 Windows 上使用 Direct2D,在 macOS 上使用 Core Graphics。可以通过以下方式验证:

cpp 复制代码
// 检查当前使用的后端
QPaintDevice *device = this;
QPaintEngine *engine = device->paintEngine();
qDebug() << "Paint engine:" << engine->type();
// QPaintEngine::Raster     --- 软件光栅化
// QPaintEngine::OpenGL     --- OpenGL 加速
// QPaintEngine::Direct2D   --- Windows Direct2D

6.2 QOpenGLWidget 中的 2D 绘制

对于需要极致性能的 2D 绘制场景,可以借助 OpenGL 加速:

cpp 复制代码
class OpenGL2DWidget : public QOpenGLWidget {
protected:
    void initializeGL() override {
        // 设置 OpenGL 属性
        QSurfaceFormat format = this->format();
        format.setProfile(QSurfaceFormat::CoreProfile);
        format.setSamples(4); // MSAA 抗锯齿
        this->setFormat(format);
    }

    void paintGL() override {
        // 清屏
        glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 使用 OpenGL 命令绘制 2D 图形(超高性能)
        // 顶点数组、VBO、纹理等
        drawTriangles();
    }

    void resizeGL(int w, int h) override {
        glViewport(0, 0, w, h);
    }
};

6.3 Vulkan 后端(Qt 6.3+)

Qt 6.3 引入了实验性的 Vulkan 绘制后端:

cpp 复制代码
// 启用 Vulkan 加速(需要显卡和驱动支持)
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
// 或
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);

// 在支持 Vulkan 的系统上,Qt 会自动选择 Vulkan 作为后端

7. 性能优化总结清单

优化项 方法 效果
静态内容缓存 QPixmap 缓存不变背景 减少 90%+ 绘制开销
批量绘制 QPainterPath 合并多图形 减少 N 倍调用开销
离屏预渲染 QImage::fill / QPicture 录制 复杂场景 3-10x 提升
脏区域更新 update(QRect) 只重画必要的区域
像素操作 QImage + 指针算术 比 QPainter 逐像素快 10x
避免透明叠加 setAttribute(WA_OpaquePaintEvent) 减少合成开销
抗锯齿控制 按需开启 Antialiasing 高频场景关闭可提速 5x
颜色格式 统一用 Format_ARGB32 避免每帧格式转换
定时器优化 60FPS 场景用 16ms 定时器 减少不必要重绘
OpenGL 加速 QOpenGLWidget 超高频绘制(游戏、实时图表)

结语

Qt 2D 绘制实战的核心是"选对工具、用对策略"。QPixmap 用于屏幕显示、QImage 用于像素处理、QPicture 用于命令缓存;双缓冲消除闪烁、脏区域更新减少浪费、层次化缓存分离静态与动态内容。在 Qt 6 时代,硬件加速让 2D 绘制性能进一步突破,理解底层原理才能在高频 UI 场景中游刃有余。

注:若有发现问题欢迎大家提出来纠正

相关推荐
FeBaby2 小时前
ReentrantLock 与 synchronized 底层实现对比图解
开发语言·c#
jaycyj2 小时前
pytest
开发语言·python
A_aspectJ2 小时前
【Java基础开发】基于 Java Swing +MySQL + JDBC 版实现图书管理系统
java·开发语言·mysql
Gary Studio2 小时前
安卓HAL编译流程
开发语言·python
我是无敌小恐龙2 小时前
Java SE 零基础入门Day06 方法重载+Debug调试+String字符串全套API详解(超全干货)
java·开发语言·人工智能·python·transformer·无人机·量子计算
AI+程序员在路上2 小时前
Qt6读取rtsp视频流的几种方法
开发语言·qt6.3
码农飞哥2 小时前
从Java后端到AI应用开发,我这两年做了什么
java·开发语言·人工智能
胡童嘉2 小时前
C语言考研《谭浩强C语言》教材第一章理论+实践汇总
c语言·开发语言·考研
初心未改HD3 小时前
Go语言Slice切片底层原理深度解析
开发语言·golang