auther: abinng date: 2026-05-10 18:21
createDate:2026-05-10 18:21
复习路线
这篇笔记是一个综合实战——用 Qt
的绘图系统和动画框架,从零实现一个仿汽车仪表盘的自定义控件。
涉及的知识点:
1 2 3 4 QPainter 坐标系变换(translate/rotate/scale) → 绘制环形刻度盘 + 刻度文字 → QPainterPath 布尔运算(subtract 镂空指针) → Q_PROPERTY + QPropertyAnimation(平滑动画)
建议先读完 015-Qt
的绘图系统 ,掌握 QPainter 的
translate/rotate/scale 和 drawArc
之后再来看这篇。
1. 效果描述
实现一个汽车速度仪表盘控件,具备以下功能:
速度范围 0 ~ 240 km/h
环形刻度盘,短刻度和长刻度交替,长刻度旁显示速度值
指针采用中间镂空样式
支持设置目标速度,指针以动画平滑滑到对应位置
2. 核心架构
整个项目分为两层:
SpeedWidget —
自定义绘图控件,负责仪表盘的绘制和动画。是本文的核心。
MainWin — 上层窗口,放一个
SpeedWidget + 输入框 +
按钮,负责接收用户输入的速度值。
SpeedWidget 的关键设计是用 Qt
的属性动画系统驱动重绘:
1 2 3 4 用户设置速度 → animateToSpeed(target) → QPropertyAnimation 逐帧修改 speed 属性 → set_speed() 被调用 → update() 触发 paintEvent() → paintEvent() 根据当前 speed 值重绘指针位置
3.
动画机制:Q_PROPERTY + QPropertyAnimation
这是本篇最核心的技巧。我们希望指针不是瞬间跳到目标位置,而是平滑滑动过去。Qt
提供了
QPropertyAnimation,它可以对某个对象的属性值做补间动画。
关键代码在头文件中:
1 2 3 4 5 6 7 8 9 10 11 class SpeedWidget : public QWidget { Q_OBJECT Q_PROPERTY (qreal speed READ get_speed WRITE set_speed) ; public : qreal get_speed () const ; void set_speed (qreal speed) ; void animateToSpeed (qreal target) ; private : QPropertyAnimation *m_anim; qreal m_speed; };
Q_PROPERTY 宏向 Qt 元对象系统注册了一个名为
speed 的属性。QPropertyAnimation
通过这个名字来读写属性值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void SpeedWidget::animateToSpeed (qreal target) { if (target < 0 ) target = 0 ; else if (target > 240 ) target = 240 ; if (m_anim == nullptr ) { m_anim = new QPropertyAnimation (this , "speed" , this ); m_anim->setEasingCurve (QEasingCurve::OutQuad); } m_anim->stop (); m_anim->setStartValue (m_speed); m_anim->setEndValue (target); m_anim->setDuration (1500 ); m_anim->start (); } void SpeedWidget::set_speed (qreal speed) { m_speed = speed; update (); }
动画期间,Qt 以高帧率不断调用 set_speed()
写入中间值,每次写入都 update()
一次,指针就看起来在平滑滑动了。QEasingCurve::OutQuad
让动画有减速收尾的效果。
4. 绘制刻度盘
绘制在 paintEvent() 中完成,整体流程是:坐标系变换 →
画外壳圆 → 画刻度线 → 画指针 → 画单位。
首先把坐标系变换到画布中心,并缩放到 [-100, 100]
的逻辑坐标范围:
1 2 3 4 5 6 7 8 9 10 void SpeedWidget::paintEvent (QPaintEvent *event) { QPainter painter (this ) ; painter.setRenderHints (QPainter::Antialiasing | QPainter::TextAntialiasing); qreal W = width (), H = height (); qreal side = qMin (W, H); painter.translate (W / 2.0 , H / 2.0 ); painter.scale (side / 210.0 , side / 210.0 );
为什么要做这个变换?控件大小可能变化,但在逻辑坐标系里绘制的图形会自动等比缩放,省去手动计算像素坐标的麻烦。
刻度盘共 25 条刻度线(0 到 240,每隔 10 km/h 一条),分布在从 150° 到
390°(即 150° + 240°)的弧上:
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 qreal startAngle = 150 ; qreal rangeAngle = 240 ; qreal max_speed = 240 ; for (int i = 0 ; i <= 24 ; ++i) { painter.save (); qreal currentAngle = startAngle + (i * rangeAngle / 24.0 ); painter.rotate (currentAngle); if (i % 2 == 0 ) { painter.setPen (QPen (Qt::black, 2 )); painter.drawLine (80 , 0 , 95 , 0 ); painter.translate (70 , 0 ); painter.rotate (-currentAngle); QRect textRect (-15 , -10 , 30 , 20 ) ; QFont font = painter.font (); font.setPointSize (6 ); painter.setFont (font); painter.drawText (textRect, Qt::AlignCenter, QString::number (i * 10 )); } else { painter.setPen (QPen (Qt::black, 1 )); painter.drawLine (80 , 0 , 95 , 0 ); } painter.restore (); }
这里使用 save()/restore() 是关键——restore()
后坐标系回到画布中心,下一轮循环从干净的起点开始。
5. 绘制镂空指针
指针需要“中间镂空“的效果,用的是 QPainterPath
的布尔运算——从大三角形里减掉一个小三角形 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 qreal speedAngle = startAngle + (m_speed / max_speed) * rangeAngle; painter.rotate (speedAngle); QPainterPath bigNeedle; bigNeedle.addPolygon (QPolygonF ({QPointF (0 , 4 ), QPointF (80 , 0 ), QPointF (0 , -4 )})); QPainterPath smallNeedle; smallNeedle.addPolygon (QPolygonF ({QPointF (4 , 2 ), QPointF (60 , 0 ), QPointF (4 , -2 )})); QPainterPath finalNeedle = bigNeedle.subtracted (smallNeedle); painter.setBrush (QColor (255 , 60 , 60 )); painter.setPen (QPen (Qt::darkRed, 1 )); painter.drawPath (finalNeedle);
QPainterPath 的布尔运算和 015-绘图系统中讲过的
intersected/subtracted/united
完全一样,这里只是应用到了指针绘制上。
6. 完整代码
点击展开
SpeedWidget.h SpeedWidget.cpp MainWin.cpp 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 SPEEDWIDGET_H #define SPEEDWIDGET_H #include <QWidget> #include <QPropertyAnimation> class SpeedWidget : public QWidget { Q_OBJECT Q_PROPERTY (qreal speed READ get_speed WRITE set_speed) ;public : explicit SpeedWidget (QWidget *parent = nullptr ) ; ~SpeedWidget () override = default ; qreal get_speed () const ; void set_speed (qreal speed) ; void animateToSpeed (qreal target) ; protected : void paintEvent (QPaintEvent *event) override ; private : QPropertyAnimation *m_anim; qreal m_speed; }; #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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 #include "SpeedWidget.h" #include <QPainter> #include <QPainterPath> SpeedWidget::SpeedWidget (QWidget *parent) : QWidget (parent) { m_speed = 0.0 ; m_anim = nullptr ; } qreal SpeedWidget::get_speed () const { return m_speed; }void SpeedWidget::animateToSpeed (qreal target) { constexpr qreal max_speed = 240.0 ; if (target < 0 ) target = 0 ; else if (target > max_speed) target = max_speed; if (m_anim == nullptr ) { m_anim = new QPropertyAnimation (this , "speed" , this ); m_anim->setEasingCurve (QEasingCurve::OutQuad); } m_anim->stop (); m_anim->setStartValue (m_speed); m_anim->setEndValue (target); m_anim->setDuration (1500 ); m_anim->start (); } void SpeedWidget::set_speed (qreal speed) { m_speed = speed; update (); } void SpeedWidget::paintEvent (QPaintEvent *) { QPainter painter (this ) ; painter.setRenderHints (QPainter::Antialiasing | QPainter::TextAntialiasing); qreal W = width (), H = height (); qreal side = qMin (W, H); painter.translate (W / 2.0 , H / 2.0 ); painter.scale (side / 210.0 , side / 210.0 ); painter.save (); painter.setPen (QPen (Qt::blue, 2 )); painter.drawEllipse (-100 , -100 , 200 , 200 ); painter.restore (); qreal startAngle = 150 , rangeAngle = 240 , max_speed = 240 ; for (int i = 0 ; i <= 24 ; ++i) { painter.save (); qreal currentAngle = startAngle + (i * rangeAngle / 24.0 ); painter.rotate (currentAngle); if (i % 2 == 0 ) { painter.setPen (QPen (Qt::black, 2 )); painter.drawLine (80 , 0 , 95 , 0 ); painter.translate (70 , 0 ); painter.rotate (-currentAngle); QRect textRect (-15 , -10 , 30 , 20 ) ; QFont font = painter.font (); font.setPointSize (6 ); painter.setFont (font); painter.drawText (textRect, Qt::AlignCenter, QString::number (i * 10 )); } else { painter.setPen (QPen (Qt::black, 1 )); painter.drawLine (80 , 0 , 95 , 0 ); } painter.restore (); } painter.save (); qreal speedAngle = startAngle + (m_speed / max_speed) * rangeAngle; painter.rotate (speedAngle); QPainterPath bigNeedle; bigNeedle.addPolygon (QPolygonF ({QPointF (0 , 4 ), QPointF (80 , 0 ), QPointF (0 , -4 )})); QPainterPath smallNeedle; smallNeedle.addPolygon (QPolygonF ({QPointF (4 , 2 ), QPointF (60 , 0 ), QPointF (4 , -2 )})); QPainterPath finalNeedle = bigNeedle.subtracted (smallNeedle); painter.setBrush (QColor (255 , 60 , 60 )); painter.setPen (QPen (Qt::darkRed, 1 )); painter.drawPath (finalNeedle); painter.restore (); painter.save (); QRect textRect (-20 , 60 , 40 , 20 ) ; QFont font = painter.font (); font.setPointSize (12 ); painter.setFont (font); painter.drawText (textRect, Qt::AlignCenter, "Km/h" ); painter.restore (); }
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 #include <QHBoxLayout> #include <QPushButton> #include <QLineEdit> #include "MainWin.h" #include "SpeedWidget.h" MainWin::MainWin (QWidget *parent) : QWidget (parent) { auto main_layout = new QVBoxLayout (this ); auto ctl_widget = new QWidget (this ); auto ctl_layout = new QHBoxLayout (ctl_widget); auto btn_start = new QPushButton ("速度设置" ); auto edt_value = new QLineEdit; ctl_layout->addWidget (edt_value); ctl_layout->addWidget (btn_start); auto m_speed_meter = new SpeedWidget (); main_layout->addWidget (m_speed_meter); main_layout->addWidget (ctl_widget); main_layout->setStretch (0 , 9 ); main_layout->setStretch (1 , 1 ); resize (400 , 400 ); connect (btn_start, &QPushButton::clicked, this , [m_speed_meter, edt_value]() { bool ok = false ; double v = edt_value->text ().toDouble (&ok); if (ok) { m_speed_meter->animateToSpeed (v); } }); }