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():QPoint、QPointF
drawLine(), drawLines():起点(x1,
y1)、终点(x2,y2)
drawPolyline():折线图(不自动闭合)参数是QPointF[]点数组
几何面(2维)
drawRect()、drawEllipse()、drawRoundedRect()
圆的核心是矩形,矩形的内接圆/椭圆
drawPolygon():多边形(自动闭合)参数是QPointF[]
点数组
弧面
drawArc(), drawPie(),
drawChord()
这里注意传参时候的角度要乘上16,因为Qt使用“1/16度”作为角度的基本度量单位
下面是部分示例代码:
点击展开
main.cpp BasePainter.h BasePainter.cpp 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
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 (); QPainter painter (this ) ; painter.setRenderHint (QPainter::Antialiasing); painter.setRenderHint (QPainter::TextAntialiasing); QPen pen (Qt::red, 3 , Qt::SolidLine, Qt::FlatCap, Qt::RoundJoin) ; #if 0 QPen pen; pen.setWidth (3 ); pen.setColor (Qt::red); pen.setCapStyle (Qt::FlatCap); pen.setJoinStyle (Qt::RoundJoin); pen.setStyle (Qt::DashDotLine); #endif 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 (gradient); painter.drawRect (W/4 , H/4 , W/2 , H/2 ); } void BasePainter::base02 () { int W = width (); int H = height (); QPainter painter (this ) ; painter.setRenderHint (QPainter::Antialiasing); painter.setRenderHint (QPainter::TextAntialiasing); QRect rect (W/4 , H/4 , W/2 , H/2 ) ; }
调用 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) ; painter.drawEllipse (0 , 0 , 100 , 100 ); update (); } 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
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); 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 , "我是正常的文字" ); }
示例代码如下(代码中有部分讲解):
点击展开
main.cpp seniorPainter.h seniorPainter.cpp 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 () ; }; #endif
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) { 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. intersected (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.scale (2 , 2 ); 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); painter.translate (W/2 , H/2 ); 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 ); } }