云计算百科
云计算领域专业知识百科平台

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 生命周期

// 方式一:栈上构造即 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(画笔:轮廓)

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(画刷:填充)

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(路径)

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) 圆心+起始角 仪表盘、调色环

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 文字绘制

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 图像绘制

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 向下,单位是逻辑像素。

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 轴范围固定):

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

2.10 离屏渲染与缓存

// 常用缓存载体
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+)

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 最小可运行示例:带渐变与抗锯齿的自定义控件

// 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 坐标变换实战:绘制旋转的风扇叶片

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 离屏渲染 + 缓存:拖动不闪烁的画板

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 手写图表)

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 循环 1000×1000 约需几十 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 场景图,底层都离不开它。掌握它的关键是理解三件事:

  • 状态机模型:pen/brush/font/transform/renderHint 都是"当前状态",绘制函数即时消费当前状态——所以 save/restore 是控制状态污染的唯一可靠手段;
  • 设备无关:QPainter 可以画到 QWidget/QPixmap/QImage/QPicture 任意设备,这一抽象让"离屏缓存 + 上屏 blit"成为性能优化的万能钥匙;
  • 坐标变换: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)
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Qt QPainter 2D 绘图系统深度解析:从画笔到渲染引擎适
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!