引言
上一篇文章解析了 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 ¢er,
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 场景中游刃有余。
注:若有发现问题欢迎大家提出来纠正