015-Qt 的绘图系统

auther: abinng date: 2026-05-10 10:34 createDate:2026-05-10 10:34

基础了解

一个功能强大且灵活的2D图形渲染框架,它使得开发者能够高效地在屏幕、内存或设备上绘制图形、文本和图像,整个绘图系统基于QPainter, QPainterDevice和QPaintEngine三个类

  • QPainter(画家):负责执行画线、填充、变换等操作的“执行者”。
  • QPaintDevice(画布):绘图的二维空间。常见的有QWidget、QPixmap、QImage、QPrinter
  • QPaintEngine(画笔引擎):这是一个底层抽象类,负责将QPainter的指令翻译成不同设备的绘制代码。

QPainter可以配置的工具有:画笔QPen、画刷QBrush、字体QFont

  • QPen:线宽、颜色、线型,含有设置接口和读取接口
  • QBrush:定义了绘制时的填充特性,包括颜色、填充样式、材质填充时的图片等,也支持渐变样式的填充

Qt 的绘图操作大部分发生在 paintEvent 中,什么时候会触发 paintEvent 呢? 系统自动触发:

  • 首次显示:窗口第一次调用 show() 时。
  • 遮挡恢复:原本被其他窗口遮挡的部分重新露出来时。
  • 窗口调整:用户手动改变窗口大小(Resize)时。
  • 最小化后还原:窗口从任务栏被重新激活时。 人为触发:
  • update():并不会立即调用 paintEvent,而是向事件队列发送一个绘制请求。Qt 会进行“合并优化”。如果你连续调用了 10 次 update(),Qt 会在下一次事件循环时将它们合并成一次 paintEvent 调用,避免过度渲染造成的卡顿
  • repaint():会立即强制执行 paintEvent。不进行合并优化,容易造成界面闪烁或性能浪费。只有在需要极其即时的重绘(如某些动画)时才考虑。

但是一些标准控件(例如:QPushButton, QLCDNumber),当调用 btn->setText("xxx") 或者 lcdNum->display(1) 的时候,不是也重绘了吗?

这些内部也会调用 update() 发送绘制请求到事件队列,所以本质还是 update()

常用基本图形接口

点、线(0、1维)

  • drawPoint(), drawPoints()QPointQPointF
  • drawLine(), drawLines():起点(x1, y1)、终点(x2,y2)
  • drawPolyline():折线图(不自动闭合)参数是QPointF[]点数组

几何面(2维)

  • drawRect()drawEllipse()drawRoundedRect()
  • 圆的核心是矩形,矩形的内接圆/椭圆
  • drawPolygon():多边形(自动闭合)参数是QPointF[] 点数组

弧面

  • drawArc(), drawPie(), drawChord()
  • 这里注意传参时候的角度要乘上16,因为Qt使用“1/16度”作为角度的基本度量单位

下面是部分示例代码:

点击展开
1
2
3
4
5
6
7
8
9
10
#include <QApplication>
#include "BasePainter.h"

int main(int argc, char* argv[])
{
QApplication a(argc, argv);
BasePainter win;
win.show();
return QApplication::exec();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef EX21_PAINTERBASE_BASEPAINTER_H
#define EX21_PAINTERBASE_BASEPAINTER_H
#include <QWidget>

class BasePainter : public QWidget {
public:
explicit BasePainter(QWidget *parent = nullptr);
~BasePainter() override = default;
protected:
void paintEvent(QPaintEvent *event) override;
private:
void base01();
void base02();
};


#endif //EX21_PAINTERBASE_BASEPAINTER_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <QPainter>
#include "BasePainter.h"

BasePainter::BasePainter(QWidget *parent) : QWidget(parent)
{
resize(500, 500);
}

void BasePainter::paintEvent(QPaintEvent *event)
{
base02();
}

void BasePainter::base01()
{
int W = width();
int H = height();
// 画家的画布画布设置为当前QWidget
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿
painter.setRenderHint(QPainter::TextAntialiasing); // 文本抗锯齿

// 方式1
QPen pen(Qt::red, 3, Qt::SolidLine, Qt::FlatCap, Qt::RoundJoin);
#if 0
// 方式2
QPen pen;
pen.setWidth(3);
pen.setColor(Qt::red);
pen.setCapStyle(Qt::FlatCap);
pen.setJoinStyle(Qt::RoundJoin);
pen.setStyle(Qt::DashDotLine);
#endif

// QBrush brush(Qt::darkCyan); // 普通刷子
QLinearGradient gradient(QPoint(W/4, H/4), QPoint(W/4 + W/2, H/4 + H/2)); // 线性渐变刷子
// 第一个参数是位置,百分之多少处
gradient.setColorAt(0, Qt::red);
gradient.setColorAt(0.5, Qt::green);
gradient.setColorAt(1, Qt::blue);

painter.setPen(pen);
// painter.setBrush(brush);
painter.setBrush(gradient);
// 在整个画布的正中间画矩形
painter.drawRect(W/4, H/4, W/2, H/2);
}

void BasePainter::base02()
{
int W = width();
int H = height();
// 画家的画布画布设置为当前QWidget
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿
painter.setRenderHint(QPainter::TextAntialiasing); // 文本抗锯齿

// 闭合曲线的基础都是矩形
QRect rect(W/4, H/4, W/2, H/2);
// int radius = qMin(rect.width(), rect.height()) / 2;
// painter.drawRect(rect); // 矩形
// painter.drawRoundedRect(rect, radius, radius); // 圆角矩形
// painter.drawEllipse(rect); // 圆/椭圆
// painter.drawArc(rect, 0, 30 * 16); // 弧
// painter.drawChord(rect, 0, 190 * 16); // 弧+两端相连
// painter.drawPie(rect, 0, 190 * 16); // 扇形

}

调用 drawxxx() 函数时,并不是发送绘制请求到事件队列,因为当前就是在 paintEvent 中处理绘制请求,这些 drawxxx() 的作用是:

  • 填充缓冲区:将绘图指令(或者像素数据)写入到 Qt 的双缓冲区(Back Buffer)中
  • 不立即上屏:这些指令并不会立刻出现在你的显示器上
  • 提交指令:只有当 paintEvent 执行完毕,QPainter 对象被销毁(析构)时,Qt 才会将缓冲区的内容一次性“贴”到屏幕上

也就是说当我们要在设备上绘图时,必须将 drawxxx() 写在(或被包含在)paintEvent 及其调用的子函数中。

为什么不能写在外面?

paintEvent 触发之前,操作系统的窗口系统并没有给这个 QWidget 分配绘图上下文(Context)。此时的画板是“锁定”状态,QPainter 无法打开绘图引擎

即便你在某个瞬间强行画上去了,只要窗口被遮挡一下再露出来,或者窗口缩放一下,之前的画面就会被擦除。只有 paintEvent 是会被重复触发的,它能保证你的画面“持久存在”。

“写在 paintEvent 中”并不意味着代码必须全部堆在那个函数里。 为了保持代码整洁,你可以这样写:

点击展开
1
2
3
4
5
6
7
8
9
10
void MainWin::paintEvent(QPaintEvent *event) {
QPainter painter(this);
drawBackground(&painter);
drawDataCurve(&painter);
drawIcons(&painter);
}

void MainWin::drawBackground(QPainter *painter) {
painter->fillRect(rect(), Qt::black);
}

当然,特殊地,我们也可以先画在图片(QPixmap/QImage)上,然后调用 update() 如下:

点击展开
1
2
3
4
5
6
7
8
9
10
11
12
13
// 可以在任何函数里写
void MainWin::drawOnBuffer() {
m_pixmap = QPixmap(size());
QPainter painter(&m_pixmap); // 往图片上画,不需要在 paintEvent 里
painter.drawEllipse(0, 0, 100, 100);
update(); // 提醒界面重绘
}

// 在 paintEvent 里只需要把这张图贴上去
void MainWin::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.drawPixmap(0, 0, m_pixmap);
}

重绘机制

上面其实已经讲了一些重绘机制了,即 update(), repaint()

  • 利用定时器实现动态刷新
  • 延x轴左右移动的小球,增加y轴的加速度效果
点击展开
1
2
3
4
5
6
7
8
9
10
#include <QApplication>
#include "Ball.h"

int main(int argc, char* argv[])
{
QApplication a(argc, argv);
Ball win;
win.show();
return QApplication::exec();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef EX21_PAINTERBASE_BALL_H
#define EX21_PAINTERBASE_BALL_H
#include <QWidget>
#include <QTimer>

class Ball : public QWidget {
Q_OBJECT
public:
explicit Ball(QWidget *parent = nullptr);
~Ball() override = default;
public slots:
void onTimerOutV1();
void onTimerOutV2();
protected:
void paintEvent(QPaintEvent *event) override;
private:
QTimer *m_timer;
int m_radius;
double m_posX;
double m_speedX;
double m_posY;
double m_speedY;
};


#endif //EX21_PAINTERBASE_BALL_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "Ball.h"
#include <QPainter>

Ball::Ball(QWidget *parent) : QWidget(parent)
{
m_radius = 30;
m_posX = 50;
m_speedX = 3;
m_posY = 0;
m_speedY = 0;
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &Ball::onTimerOutV1);
connect(m_timer, &QTimer::timeout, this, &Ball::onTimerOutV2);
// 每秒60帧,每16ms触发一次
m_timer->start(16);
setGeometry(150, 150, 600, 400);
}

void Ball::onTimerOutV1()
{
// 更新数据,数据驱动
m_posX += m_speedX;
// 碰到边缘,反弹
if (m_posX + m_radius > width() || m_posX - m_radius < 0) {
m_speedX = -m_speedX;
}
update();
}

void Ball::onTimerOutV2()
{
double g = 0.6; // 重力加速度
double bounce = 0.8; // 反弹系数

m_speedY += g;
m_posY += m_speedY;

// 碰撞检测
if (m_posY + m_radius > height()) {
m_posY = height() - m_radius; // 修正位置,避免陷入地面
m_speedY = -m_speedY * bounce;
}
update();
}

void Ball::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
// 绘制背景
painter.setBrush(Qt::black);
painter.drawRect(rect());
// 绘制小球
painter.setBrush(Qt::yellow);
painter.setPen(Qt::NoPen);
painter.drawEllipse(m_posX - m_radius, m_posY - m_radius, m_radius * 2, m_radius * 2);

}

复杂图形绘制

QPainterPath:

  • 支持布尔运算,处理图形交叠时的填充规则
  • 交(intersected)、并(united)、补(subtracted)
  • 定义一次路径,然后在不同的位置、以不同的旋转角度重复绘制

坐标变换:

  • 核心思想:我不动图,我动“纸”
  • translate(dx,dy)(平移):把坐标原点(0,0)从左上角挪到某个位置
  • rotate(angle)(旋转):把整张“纸”顺时针旋转一定角度
  • scale(sx,sy)(缩放):把“纸”上的格子放大或缩小

利用 save()/restore() 保存和恢复画家状态 由于变换是永久生效的(直到 QPainter 销毁),如果你只想临时转一下坐标系,画完后还想回到原来的状态,可以使用这两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
void MyWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);

painter.save(); // 保存当前干净的状态(原点在左上角)

painter.translate(100, 100);
painter.rotate(45);
painter.drawRect(0, 0, 50, 50); // 画一个旋转的矩形

painter.restore(); // 恢复状态!原点回到左上角,旋转也取消了

painter.drawText(10, 10, "我是正常的文字");
}

示例代码如下(代码中有部分讲解):

点击展开
1
2
3
4
5
6
7
8
9
10
#include <QApplication>
#include "seniorPainter.h"

int main(int argc, char* argv[])
{
QApplication a(argc, argv);
seniorPainter win;
win.show();
return QApplication::exec();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef EX21_PAINTERBASE_SENIORPAINTER_H
#define EX21_PAINTERBASE_SENIORPAINTER_H
#include <QWidget>

class seniorPainter : public QWidget
{
public:
explicit seniorPainter(QWidget *parent = nullptr);
~seniorPainter() override = default;
protected:
void paintEvent(QPaintEvent *event) override;
private:
void base01(); // 实现布尔运算,交并补
void base02(); // 画一个月亮
void base03(); // 画一个手表的表盘,12等分
};

#endif //EX21_PAINTERBASE_SENIORPAINTER_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include "seniorPainter.h"
#include <QPainter>
#include <QPainterPath>

seniorPainter::seniorPainter(QWidget *parent)
{
resize(500, 400);
}

void seniorPainter::paintEvent(QPaintEvent *event)
{
// base01();
// base02();
base03();
}

void seniorPainter::base01()
{
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); // 抗锯齿

QPainterPath path1;
path1.addEllipse(0, 0, 100, 100);
QPainterPath path2;
path2.addRect(50, 50, 100, 100);

// 求并集
// QPainterPath result = path1.united(path2);
// 求差集
// QPainterPath result = path1.subtracted(path2);
// 求交集
QPainterPath result = path1.intersected(path2);

// painter.drawPath(path1);
// painter.drawPath(path2);
painter.drawPath(result);
}

void seniorPainter::base02()
{
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); // 抗锯齿

QPainterPath path1;
path1.addEllipse(0, 0, 100, 100); // 画一个大圆
QPainterPath path2;
path2.addEllipse(20, 0, 100, 100); // 右移一点

QPainterPath moon = path1.subtracted(path2);

// painter.translate(50, 100); // 平移,图形右下移动,或者说画布左上移动
// painter.rotate(30); // 旋转,图形顺时针转了30度,或者说画布逆时针转了30度
painter.scale(2, 2); // 缩放,图形长宽都放大两倍
// painter.drawPath(moon);
painter.fillPath(moon, Qt::darkYellow);
}

void seniorPainter::base03()
{
QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing); // 抗锯齿
qreal W = width();
qreal H = height();
qreal side = qMin(W, H);

// 1. 平移 坐标原点从左上角移动到窗口中心点
painter.translate(W/2, H/2);
// 2. 缩放 图形随着窗口大小进行自适应,假设逻辑坐标范围[-100, 100]
painter.scale(side / 200.0, side / 200.0);
painter.setPen(Qt::black);
for (int i = 0; i < 12; ++i) {
if (i % 3 == 0) {
painter.save();
QPen pen = painter.pen();
pen.setWidth(3);
painter.setPen(pen);
painter.drawLine(88, 0, 96, 0);
painter.restore();
} else {
painter.drawLine(88, 0, 96, 0);
}
painter.rotate(30);

}
}