Qt QPainter 2D 绘图系统深度解析:从画笔到渲染引擎适

1. 背景

Qt 的 2D 渲染体系自底向上大致分为三层:

层级 组件 定位
底层渲染后端 QPainter + QPaintDevice 直接绘制 2D 图元(线、矩形、椭圆、路径、文字、图像)到任意可绘表面
中间场景层 QGraphicsView / QGraphicsScene / QGraphicsItem 在 QPainter 之上封装场景-视图-图元架构,提供对象化管理
上层声明式 UI QML / Qt Quick(Scene Graph) 声明式语言描述界面,底层用 GPU 加速的场景图渲染

为什么还需要 QPainter?

  • 自定义控件(QWidget 子类)重写 paintEvent 时,官方推荐方式就是 QPainter;
  • 矢量图元、统计图、波形、画板、流程图等"自由画布"场景,QPainter 是最直接、可预测的 API;
  • QGraphicsView、QCustomPlot、Qwt 等上层框架,内部最终都落到 QPainter 的 draw 系列调用;
  • 离屏渲染(把画面画到 QImage/QPixmap 再复用)是高频性能优化手段。

2. 核心 API 说明

2.1 QPainter 生命周期

cpp 复制代码
// 方式一:栈上构造即 begin(常用)
void Widget::paintEvent(QPaintEvent*) {
    QPainter painter(this);       // 自动 begin(this)
    // ... 绘制
}                                 // 析构自动 end()

// 方式二:显式 begin/end(绘制到非 QWidget 表面或中途切换设备时)
QPainter painter;
painter.begin(&image);
// ...
painter.end();

关键成员:

API 说明
begin(QPaintDevice*) 开始绘制,返回 bool(设备不支持时 false)
end() 结束绘制;未显式调用时析构函数自动调用
isActive() 是否处于 begin 之后 end 之前
device() 当前绘制目标设备
setRenderHint(QPainter::RenderHint, bool) 设置渲染提示,如 Antialiasing(抗锯齿)

2.2 QPen(画笔:轮廓)

cpp 复制代码
QPen pen;
pen.setColor(QColor(220, 80, 80));
pen.setWidth(2);              // 像素宽度(逻辑单位,受变换影响)
pen.setWidthF(1.5);           // 浮点宽度
pen.setStyle(Qt::DashLine);   // 线型:SolidLine/DashLine/DotLine/DashDotLine...
pen.setCapStyle(Qt::RoundCap);    // 端点帽:FlatCap/SquareCap/RoundCap
pen.setJoinStyle(Qt::RoundJoin);  // 连接点:BevelJoin/MiterJoin/RoundJoin
painter.setPen(pen);

painter.setPen(QPen(Qt::blue, 0)); // 宽度 0 = cosmetic 笔(始终 1 像素,不随缩放变粗)
painter.setPen(Qt::NoPen);         // 不画轮廓

2.3 QBrush(画刷:填充)

cpp 复制代码
painter.setBrush(QBrush(QColor(100, 200, 150)));        // 纯色
painter.setBrush(Qt::BDiagPattern);                     // 预定义图案
painter.setBrush(QBrush(QPixmap("texture.png")));       // 纹理填充
painter.setBrush(Qt::NoBrush);                          // 不填充

// 渐变画刷(见 2.6)
QLinearGradient grad(0, 0, 200, 200);
grad.setColorAt(0.0, Qt::white);
grad.setColorAt(1.0, QColor(40, 120, 200));
painter.setBrush(QBrush(grad));

2.4 draw 系列图元 API

API 作用
drawPoint / drawPoints
drawLine / drawLines / drawPolyline 线 / 折线
drawRect / drawRoundedRect 矩形 / 圆角矩形
drawEllipse / drawArc / drawChord / drawPie 椭圆 / 弧 / 弓形 / 扇形
drawPolygon 多边形(自动闭合)
drawPath(QPainterPath) 任意路径(见 2.5)
drawText(QRect, int flags, QString) 文本(见 2.7)
drawPixmap / drawImage 图像(见 2.8)
fillRect(QRect, QBrush/QColor) 直接填充矩形(不走 pen)
eraseRect 擦除(不推荐,用 fillRect 替代)

所有 draw 均同时受当前 pen(轮廓)与 brush(填充)影响。

2.5 QPainterPath(路径)

cpp 复制代码
QPainterPath path;
path.moveTo(50, 50);          // 移动起点
path.lineTo(150, 50);         // 直线
path.quadTo(200, 0, 250, 80); // 二次贝塞尔
path.cubicTo(300, 160, 100, 200, 80, 120); // 三次贝塞尔
path.arcTo(QRectF(20, 20, 100, 100), 0, 180); // 圆弧
path.closeSubpath();          // 闭合当前子路径

path.addRect(...);            // 添加子路径
path.addEllipse(...);
path.addRoundedRect(...);
path.addText(QPointF, font, "Qt");

painter.drawPath(path);

// 常用谓词
path.isEmpty(); path.contains(QPointF); path.boundingRect();
// 布尔运算(Qt 5.10+ 需要 QtGui 附加模块路径支持)
QPainterPath united = path1.united(path2);   // 并集
QPainterPath inter  = path1.intersected(path2); // 交集
QPainterPath diff   = path1.subtracted(path2);   // 差集

2.6 QGradient(渐变)

三种内置渐变 + 一个通用基类:

类型 构造要点 示例
QLinearGradient(x1,y1,x2,y2) 起点/终点定义方向 高光、进度条
QRadialGradient(cx,cy,r) 圆心+半径 光晕、球体
QConicalGradient(cx,cy,angle) 圆心+起始角 仪表盘、调色环
cpp 复制代码
QLinearGradient g(0, 0, width(), 0);   // 水平渐变
g.setColorAt(0.0, QColor(255, 0, 0));
g.setColorAt(0.5, QColor(0, 255, 0));
g.setColorAt(1.0, QColor(0, 0, 255));
g.setSpread(QGradient::RepeatSpread);  // 超出范围后的扩散方式
painter.setBrush(g);

2.7 文字绘制

cpp 复制代码
QFont font("Microsoft YaHei", 12);   // 注意字体名要存在于系统
font.setBold(true);
painter.setFont(font);
painter.setPen(Qt::black);

painter.drawText(QPoint(10, 30), "左上角基线文本");

painter.drawText(rect, Qt::AlignCenter | Qt::TextWordWrap, "居中换行文本");
// 返回实际绘制到的矩形(可用来测文本尺寸)
QRectF br;
painter.drawText(rect, Qt::AlignCenter, "文本", &br);

QFontMetrics fm(painter.font());     // 字体度量
int w = fm.horizontalAdvance("文本"); // 文本像素宽度(Qt5.11+;旧版用 width())
int h = fm.height();                  // 行高

2.8 图像绘制

cpp 复制代码
painter.drawPixmap(x, y, pixmap);                    // 原尺寸
painter.drawPixmap(QRect(x,y,w,h), pixmap);          // 拉伸到目标矩形
painter.drawPixmap(QRect(x,y,w,h), pixmap, QRectF(src)); // 带源区域截取
painter.drawImage(QRect(x,y,w,h), image);            // QImage 同理

// 平滑缩放
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);

2.9 坐标系统与变换

QPainter 默认坐标系:原点在设备左上角,x 向右,y 向下,单位是逻辑像素。

cpp 复制代码
painter.translate(100, 50);   // 平移
painter.rotate(45);           // 旋转(度,顺时针)
painter.scale(2.0, 2.0);      // 缩放
painter.shear(0.1, 0);        // 错切

// 必须成对使用,否则变换叠加污染后续绘制
painter.save();
painter.translate(cx, cy);
painter.rotate(angle);
painter.drawText(QPointF(0, -10), "标签");
painter.restore();

// 世界变换矩阵(Qt 5.14+ 提供更精确的 QTransform 重载)
QTransform t;
t.translate(50, 50).rotate(30).scale(1.5, 1.5);
painter.setWorldTransform(t);
painter.resetTransform();     // 重置为单位矩阵

视图变换(Qt 5.14+):setViewport / setWindow 允许把逻辑窗口映射到设备视口,适合固定逻辑坐标系的绘图(如曲线图 y 轴范围固定):

cpp 复制代码
painter.setWindow(QRect(0, 0, 1000, 500));   // 逻辑窗口
painter.setViewport(QRect(0, 0, width(), height())); // 物理区域
// 此后所有坐标按 1000x500 逻辑空间书写,自动映射

2.10 离屏渲染与缓存

cpp 复制代码
// 常用缓存载体
QPixmap  pm(size);            // 屏幕优化,绘制在屏幕关联设备上最快(依赖平台)
QImage   img(size, QImage::Format_ARGB32_Premultiplied); // 像素可直读,适合 CPU 处理/保存
QPicture pic;                 // 记录绘制指令(矢量),可重放

// 离屏绘制标准姿势
QPixmap pm(size);
pm.fill(Qt::transparent);     // 先填充透明,否则初始内容未定义
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing);
// ... 绘制
p.end();

// 之后在 paintEvent 中 blit 上屏
painter.drawPixmap(0, 0, pm);

// QImage 逐像素处理示例
QImage img(400, 300, QImage::Format_ARGB32_Premultiplied);
img.fill(Qt::white);
for (int y = 0; y < img.height(); ++y)
    for (int x = 0; x < img.width(); ++x)
        img.setPixelColor(x, y, QColor(x % 256, y % 256, 128));
img.save("out.png");          // 支持 png/jpg/bmp 等(按后缀选格式)

2.11 组合模式(Composition Mode,Qt 4.6+)

cpp 复制代码
painter.setCompositionMode(QPainter::CompositionMode_SourceOver); // 默认
painter.setCompositionMode(QPainter::CompositionMode_Source);     // 直接覆盖
painter.setCompositionMode(QPainter::CompositionMode_Plus);       // 叠加增亮
painter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
// ... 共 30 种模式,适合实现橡皮擦、发光、遮罩等效果
// 橡皮擦常用:先设置 Source 模式,再用透明色绘制
painter.setCompositionMode(QPainter::CompositionMode_Clear);
painter.fillRect(rect, Qt::transparent);
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);

3. 详细使用说明

3.1 最小可运行示例:带渐变与抗锯齿的自定义控件

cpp 复制代码
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget {
    Q_OBJECT
public:
    explicit Widget(QWidget *parent = nullptr);
protected:
    void paintEvent(QPaintEvent *event) override;
};
#endif

// widget.cpp
#include "widget.h"
#include <QPainter>
#include <QPainterPath>
#include <QLinearGradient>

Widget::Widget(QWidget *parent) : QWidget(parent) {
    setMinimumSize(400, 300);
}

void Widget::paintEvent(QPaintEvent *) {
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing, true); // 抗锯齿,画曲线必备

    // 1) 渐变圆角矩形
    QLinearGradient grad(0, 0, width(), height());
    grad.setColorAt(0.0, QColor(70, 130, 220));
    grad.setColorAt(1.0, QColor(30, 60, 130));
    p.setBrush(grad);
    p.setPen(QPen(Qt::white, 2));
    p.drawRoundedRect(rect().adjusted(10, 10, -10, -10), 12, 12);

    // 2) 贝塞尔曲线路径
    QPainterPath curve;
    curve.moveTo(20, 200);
    curve.cubicTo(120, 40, 280, 260, 380, 120);
    p.setPen(QPen(QColor(255, 220, 100), 3));
    p.setBrush(Qt::NoBrush);
    p.drawPath(curve);

    // 3) 居中文案
    p.setPen(Qt::white);
    p.setFont(QFont("Microsoft YaHei", 14, QFont::Bold));
    p.drawText(rect(), Qt::AlignCenter, "Hello QPainter");
}

3.2 坐标变换实战:绘制旋转的风扇叶片

cpp 复制代码
void FanWidget::paintEvent(QPaintEvent *) {
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);
    const QPointF c = rect().center();
    const int n = 3;
    for (int i = 0; i < n; ++i) {
        p.save();                     // 保存未变换状态
        p.translate(c);               // 平移到中心
        p.rotate(angle_ + i * 360.0 / n); // 旋转
        p.setBrush(QColor(80, 160, 255, 180));
        p.setPen(Qt::NoPen);
        p.drawEllipse(QRectF(-90, -20, 180, 40)); // 以原点为中心的叶片
        p.restore();                  // 恢复,避免旋转叠加
    }
}

要点:每次变换前 save、结束后 restore,否则第二次循环的 rotate 会在第一次基础上叠加,叶片会越转越歪。

3.3 离屏渲染 + 缓存:拖动不闪烁的画板

cpp 复制代码
class BoardWidget : public QWidget {
    QPixmap canvas_;
    QPoint last_;
protected:
    void paintEvent(QPaintEvent *) override {
        QPainter p(this);
        p.drawPixmap(0, 0, canvas_);      // 每次重绘只需 blit 缓存
    }
    void mousePressEvent(QMouseEvent *e) override {
        last_ = e->pos();
    }
    void mouseMoveEvent(QMouseEvent *e) override {
        QPainter p(&canvas_);              // 直接在缓存上绘制
        p.setPen(QPen(Qt::black, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
        p.drawLine(last_, e->pos());
        last_ = e->pos();
        update();                          // 触发重绘,paintEvent 中 blit
    }
    void resizeEvent(QResizeEvent *e) override {
        QPixmap next(size());
        next.fill(Qt::white);
        QPainter p(&next);
        p.drawPixmap(0, 0, canvas_);       // 保留旧内容
        canvas_ = next;
        QWidget::resizeEvent(e);
    }
};

这是白板/涂鸦类应用的标准架构:交互绘制到离屏缓存,paintEvent 只做 blit,天然防闪烁、性能高。

3.4 简单柱状图(QPainter 手写图表)

cpp 复制代码
void ChartWidget::paintEvent(QPaintEvent *) {
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);
    const QVector<double> data{42, 78, 55, 91, 66};
    const int n = data.size();
    const double bw = width() / double(n) * 0.6; // 柱宽
    const double maxV = 100.0;

    // 坐标轴
    p.setPen(QPen(Qt::gray, 1));
    p.drawLine(QPointF(20, height() - 20), QPointF(width() - 10, height() - 20));
    p.drawLine(QPointF(20, height() - 20), QPointF(20, 10));

    // 柱体(顶部渐变)
    for (int i = 0; i < n; ++i) {
        double h = data[i] / maxV * (height() - 60);
        double x = 30 + i * (width() - 40) / n + (width() - 40) / n * 0.2;
        QRectF bar(x, height() - 20 - h, bw, h);
        QLinearGradient g(bar.topLeft(), bar.bottomLeft());
        g.setColorAt(0, QColor(90, 170, 255));
        g.setColorAt(1, QColor(40, 90, 200));
        p.setBrush(g);
        p.setPen(Qt::NoPen);
        p.drawRoundedRect(bar, 3, 3);
        // 数值标签
        p.setPen(Qt::black);
        p.drawText(bar.adjusted(0, -22, 0, -6), Qt::AlignCenter, QString::number(data[i]));
    }
}

4. 常错点 / 坑(高发清单)

4.1 重复 begin 导致警告或崩溃

用 QPainter p(this) 后又在函数里调用 p.begin(...),或对已 begin 的 painter 再次 begin,会输出 QPainter::begin: Painter already active 且返回 false。规则:栈上构造即 begin,之后只准 end 一次(可省略);需要切换设备时用无参构造 + 显式 begin。

4.2 在 paintEvent 之外绘制 QWidget 不生效

QWidget 的内容绘制只能发生在 paintEvent 中(系统随时会擦除重画)。想主动重画调用 update()(合并请求)或 repaint()(立即重画,阻塞,慎用)。非 paintEvent 中画 widget 的正确做法:画到离屏 QPixmap/QImage,再在 paintEvent 里 blit。

4.3 变换未 save/restore 导致污染

translate/rotate/scale 是有状态的 ,会影响其后的所有绘制。忘记 restore 的典型症状:第二次刷新画面位置越偏越远、图形越来越小/大。每个变换块前后必须 save()/restore() 配对。

4.4 高 DPI / 缩放模糊

  • 未启用高 DPI 支持时,高分屏上 QPainter 画的文字/线条发虚:程序入口加 QApplication::setHighDpiScaleFactorRoundingPolicy(...),并在 main 最前面 QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling)(Qt 5;Qt 6 默认开启)。
  • 图片拉伸模糊:需要 SmoothPixmapTransform 渲染提示。
  • 线条随缩放变粗:想要恒 1 像素用宽度 0(cosmetic pen)。

4.5 中文乱码 / 字体缺失

  • 明确设置中文字体:QFont("Microsoft YaHei") 或 "SimHei",不要依赖默认字体;
  • 系统无该字体时 Qt 会回退,可能显示豆腐块;
  • 画布控件设置字体后记得 painter.setFont(font),painter 不会自动继承 widget 字体。

4.6 性能陷阱

  • 在 paintEvent 中创建渐变/字体/大对象:每次重绘都重建。应在构造函数缓存为成员变量。
  • 抗锯齿全开:圆角+渐变+阴影叠加时 CPU 开销显著,只有曲线/斜线需要时开启,矩形/文字可不开。
  • 频繁 update() 造成连续重绘:合并为定时器节流(如 30ms 一次)。
  • 大 QPixmap 反复绘制:用 drawTiledPixmap 平铺或缩小目标区域。
  • QImage 逐像素 setPixelColor 很慢:批量用 setPixelColor 循环 1000x1000 约需几十 ms 级;需要高性能图像处理优先操作 bits()/scanLine() 指针或改用 GPU 方案。

4.7 QPixmap 未填充就绘制

QPixmap pm(size) 初始内容未定义(取决于平台,可能是垃圾数据)。离屏绘制前必须先 pm.fill(Qt::transparent) 或填充底色,否则出现花屏/黑边。

4.8 QImage 格式选择错误

  • Format_ARGB32 是不预乘 alpha,Format_ARGB32_Premultiplied 是预乘,二者混用会导致颜色发暗/发亮;
  • 默认 Format_ARGB32(Qt 5 中 QImage 默认就是它)在部分绘制操作上比预乘慢;需要 alpha 合成建议用 Premultiplied;
  • Format_RGB32 无 alpha,pixelColor 读 alpha 恒为 255。

4.9 线程绘制安全

QPainter 绘制 QImage 是线程安全的(只要该 QImage 不被其它线程同时读写),但绘制 QPixmap / QWidget 不是。后台线程画图请用 QImage,完成后通过信号槽把结果交给 GUI 线程再转 QPixmap 上屏。

4.10 drawText 对不齐 / 测量误差

  • drawText(QPoint, ...) 的 y 是基线(baseline)坐标,不是文字顶部,第一眼觉得"偏上了"是正常的;
  • 想要精确居中用 drawText(QRect, Qt::AlignCenter, ...);
  • 宽度测量用 QFontMetrics::horizontalAdvance(不要用废弃的 width(),多字节字符会偏小)。

4.11 组合模式状态泄漏

设置了 CompositionMode_Clear 或 Source 后忘记恢复 SourceOver,后续所有绘制都会变成覆盖/擦除效果。改组合模式同样建议 save/restore 包裹。

4.12 resizeEvent 中未保留画板内容

画板类控件 resize 后 QPixmap 尺寸变化,直接 new 一块空的会把旧内容清掉;应按 3.3 示例把旧画布内容复制到新画布。


5. 总结

QPainter 是 Qt 2D 绘制的:自定义控件、离屏渲染、图表、画板、甚至上层 Graphics View / QCustomPlot / QML 场景图,底层都离不开它。掌握它的关键是理解三件事:

  1. 状态机模型:pen/brush/font/transform/renderHint 都是"当前状态",绘制函数即时消费当前状态------所以 save/restore 是控制状态污染的唯一可靠手段;
  2. 设备无关:QPainter 可以画到 QWidget/QPixmap/QImage/QPicture 任意设备,这一抽象让"离屏缓存 + 上屏 blit"成为性能优化的万能钥匙;
  3. 坐标变换:translate/rotate/scale 是绘制复杂图形(仪表、扇形、旋转元素)的根基,配合 QPainterPath 可以表达任意矢量形状。

日常使用记住 8 条避坑铁律:begin 只一次、绘制只在 paintEvent、变换必配对 save/restore、QPixmap 先 fill、抗锯齿按需开、中文显式设字体、QImage 预乘格式、线程画 QImage 不画 QPixmap。

FAQ 速查表

问题 一句话答案
画笔宽度为 0 是什么意思? cosmetic 笔,恒为 1 像素,不随 scale 变粗
想画透明背景的缓存图? pm.fill(Qt::transparent) 且使用带 alpha 的格式
画出来的圆有锯齿? setRenderHint(QPainter::Antialiasing, true)
重绘闪烁怎么办? 离屏 QPixmap 绘制 + paintEvent 只 blit
如何清空画板? canvas.fill(Qt::white) 后 update()
后台线程能绘制吗? 只能画 QImage,画完转 QPixmap 交 GUI 线程
文字垂直不对齐? drawText(QPoint) 的 y 是基线;居中用 QRect 重载
图片放大模糊? SmoothPixmapTransform 渲染提示
高 DPI 下模糊? Qt6 默认开启;Qt5 需 AA_EnableHighDpiScaling
如何保存绘制结果? QImage::save(path);矢量保存用 QPicture 或导出 SVG(QSvgGenerator)
相关推荐
wabs6661 小时前
关于二叉树【力扣100.相同的树的思考】
数据结构·c++·算法·leetcode·二叉树·递归法
小小龙学IT1 小时前
Boost.PFR 开源结构体字段反射库深度解析
c++·开源
程序猿编码1 小时前
零依赖纯手写:C++ 实现完整神经网络,张量反向传播全打通
c++·神经网络·transformer·大模型推理
码匠许师傅1 小时前
【C++三方组件】utfcpp:UTF-8 字符串处理避坑
c++
蒸蒸yyyyzwd1 小时前
八股学习 MVCC 慢查询优化和 read 过程
c++·八股
APEX真好玩2 小时前
把高频传感器(IMU)数据封装进 MP4:FFmpeg 私有数据轨实践
c++
晴天的雨.9922 小时前
[C++算法]盛最多水的容器(双指针算法)
开发语言·c++·算法
深兰科技2 小时前
深兰科技受邀参与第二届中国(南宁)—东盟人工智能场景应用对接会,深化AI国际合作
人工智能·qt·r语言·scala·symfony·智能机器人·深兰科技
布莱克6052 小时前
理解内存泄漏:成因、发现方法与解决策略(C++ 举例)
c语言·c++·内存泄漏