auther: abinng date: 2026-05-18 09:07
createDate:2026-05-18 09:06
复习路线
这篇笔记是一个综合实战——用 Qt
的绘图系统和动画框架,从零实现一个可交互的拟物化时钟控件。
涉及的知识点:
1 2 3 4 5 QPainter 坐标系变换 → 绘制 60 个刻度 + 三根针(时/分/秒) → Q_PROPERTY + QPropertyAnimation 实现平滑扫秒 → QTimer 驱动时间推进 → 信号/槽连接控件与 LCD 面板 → 日间/夜间主题切换、手动设置时间
建议先读完 015-Qt 的绘图系统 和
016-小项目-仿汽车仪表盘
之后再来看这篇,它们分别覆盖了基础绘图和 Q_PROPERTY
动画模式。
1. 效果描述
实现一个拟物化时钟控件,具备以下功能:
圆形表盘,60 个刻度(每 5 个加粗),时针/分针/秒针
两种走针模式:平滑扫秒 (秒针连续转动)和机械跳动 (秒针每秒跳一格)
日间/夜间主题切换
右侧控制面板:数字时间显示(LCD)、走针模式、主题、手动设置时间
2. 核心架构
项目分为两层:
ABClockWidget —
时钟控件本体。负责绘制表盘、推进时间、执行动画。是本文核心。
ABMainWin —
上层窗口。左右布局,左侧放大钟,右侧放控制面板。
ABMainWin 通过信号槽连接各个按钮到
ABClockWidget 的接口:
1 2 3 4 5 6 7 ABMainWin (窗口层) ├── ABClockWidget (时钟控件) ← setSmoothMode / setDarkMode / setManualTime ├── QLCDNumber ← 接收 timeUpdated 信号更新数字显示 └── 控制面板 (RadioButton/PushButton/QTimeEdit) - 走针模式 → setSmoothMode(bool) - 主题切换 → setDarkMode(bool) - 手动设时 → setManualTime(QTime)
3. 时间推进机制
时钟需要每秒走一格。核心是用 QTimer 每 1000ms
触发一次,在槽函数里推进时间:
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 m_timer = new QTimer (this ); connect (m_timer, &QTimer::timeout, this , &ABClockWidget::updateTime);m_timer->start (1000 ); void ABClockWidget::updateTime () { m_currentTime = m_currentTime.addSecs (1 ); m_hour = m_currentTime.hour (); m_minute = m_currentTime.minute (); int nextSecond = m_currentTime.second (); if (m_isSmooth) { m_anim->stop (); qreal startSecond = fmod (m_second, 60.0 ); qreal endSecond = nextSecond; if (endSecond == 0.0 ) { endSecond = 60.0 ; } m_anim->setStartValue (startSecond); m_anim->setEndValue (endSecond); m_anim->start (); } else { setSecond (nextSecond); } emit timeUpdated (m_currentTime.toString("hh:mm:ss" )) ; }
这里有一个容易忽略的细节:当秒针从 59 走向 0 时,如果
endSecond = 0,动画会让秒针逆时针 倒转一圈回到
0(因为从 59 补间到 0 是数值下降)。解决办法很简单——当目标值为 0
时,把动画终点设为 60.0。绘制时通过 fmod(second, 60.0) 把
60° 映射回 0° 的位置,视觉上就是顺时针平滑跨过 12 点方向。
4. 绘制表盘
绘制在 paintEvent()
中完成。首先把坐标系变换到中心,缩放到 [-100, 100]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void ABClockWidget::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 , H / 2 ); painter.scale (side / 200.0 , side / 200.0 ); QColor bgColor = m_isDark ? QColor (40 , 44 , 52 ) : QColor (245 , 245 , 240 ); QColor tickColor = m_isDark ? Qt::white : Qt::black; painter.setPen (QPen (QColor (60 , 65 , 70 ), 4 )); painter.setBrush (bgColor); painter.drawEllipse (-98 , -98 , 196 , 196 );
4.1 绘制刻度
60 个刻度均匀分布在圆周上,每步旋转 6°。每 5 个刻度加粗:
1 2 3 4 5 6 7 8 9 10 11 painter.setPen (tickColor); for (int i = 0 ; i < 60 ; ++i) { if (i % 5 == 0 ) { painter.setPen (QPen (tickColor, 2 )); painter.drawLine (86 , 0 , 95 , 0 ); } else { painter.setPen (QPen (tickColor, 1 )); painter.drawLine (91 , 0 , 95 , 0 ); } painter.rotate (6.0 ); }
这里利用了 painter.rotate(6.0)
不重置 的特性——每次 rotate
是累加在之前的角度上的,60 次刚好回到起点。
4.2 绘制三根针
关键认知:QPainter 的 0° 方向是 3
点钟方向(水平向右) 。而时钟的 12 点在正上方(相当于
270°)。所以每根针的最终角度 = 270° + 该针对应的角度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 qreal offset = 270.0 ; painter.save (); painter.rotate (offset + (m_hour % 12 ) * 30.0 + m_minute / 2.0 ); painter.setPen (QPen (tickColor, 6 , Qt::SolidLine, Qt::RoundCap)); painter.drawLine (0 , 0 , 50 , 0 ); painter.restore (); painter.save (); painter.rotate (offset + m_minute * 6.0 + fmod (m_second, 60.0 ) * 0.1 ); painter.setPen (QPen (Qt::gray, 4 , Qt::SolidLine, Qt::RoundCap)); painter.drawLine (0 , 0 , 75 , 0 ); painter.restore (); painter.save (); painter.rotate (offset + fmod (m_second, 60.0 ) * 6.0 ); painter.setPen (QPen (QColor (230 , 50 , 50 ), 2 , Qt::SolidLine, Qt::RoundCap)); painter.drawLine (-15 , 0 , 85 , 0 ); painter.setBrush (QColor (230 , 50 , 50 )); painter.drawEllipse (-4 , -4 , 8 , 8 ); painter.restore ();
为什么
fmod(m_second, 60.0)?因为平滑扫秒模式下,m_second
在 59→60 过渡时会变成 60.0,用 fmod 取模后映射回 6°
的位置,指针不会跳回 0°。
5. 手动设置时间
用户通过 QTimeEdit 选择一个时间后,点击按钮调用
setManualTime():
1 2 3 4 5 6 7 8 9 10 void ABClockWidget::setManualTime (const QTime &time) { m_anim->stop (); m_currentTime = time; m_hour = time.hour (); m_minute = time.minute (); m_second = time.second (); update (); emit timeUpdated (m_currentTime.toString("hh:mm:ss" )) ; }
停止动画是为了避免旧动画继续修改
m_second,和新设的值产生冲突。
6. 主题切换
日间/夜间模式只需要切换两个颜色变量,然后 update()
重绘:
1 2 3 4 5 void ABClockWidget::setDarkMode (bool enable) { m_isDark = enable; update (); }
paintEvent() 中根据 m_isDark
选择配色——深色底板 + 白色刻度和指针,或者浅色底板 + 黑色刻度指针。
7. 完整代码
点击展开
ABClockWidget.h ABClockWidget.cpp ABMainWin.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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #ifndef ABCLOCKWIDGET_H #define ABCLOCKWIDGET_H #include <QTime> #include <QTimer> #include <QWidget> #include <QPropertyAnimation> class ABClockWidget : public QWidget { Q_OBJECT Q_PROPERTY (qreal second READ getSecond WRITE setSecond) public : explicit ABClockWidget(QWidget *parent = nullptr ); ~ABClockWidget () override = default ; qreal getSecond () const ; void setSecond (qreal second) ; void setSmoothMode (bool enable) ; void setDarkMode (bool enable) ; void setManualTime (const QTime &time) ; signals: void timeUpdated (const QString &timeStr) ; protected : void paintEvent (QPaintEvent *event) override ; private slots: void updateTime () ; private : qreal m_second; int m_minute, m_hour; QTimer *m_timer; QPropertyAnimation *m_anim; bool m_isDark, m_isSmooth; QTime m_currentTime; }; #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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 #include "ABClockWidget.h" #include <QPainter> #include <cmath> ABClockWidget::ABClockWidget (QWidget *parent) : QWidget (parent) { m_isDark = false ; m_isSmooth = true ; m_currentTime = QTime::currentTime (); m_hour = m_currentTime.hour (); m_minute = m_currentTime.minute (); m_second = m_currentTime.second (); m_anim = new QPropertyAnimation (this , "second" ); m_anim->setDuration (1000 ); m_anim->setEasingCurve (QEasingCurve::Linear); m_timer = new QTimer (this ); connect (m_timer, &QTimer::timeout, this , &ABClockWidget::updateTime); m_timer->start (1000 ); } qreal ABClockWidget::getSecond () const { return m_second; }void ABClockWidget::setSecond (qreal second) { m_second = second; update (); } void ABClockWidget::setSmoothMode (bool enable) { m_isSmooth = enable; if (!m_isSmooth) { m_anim->stop (); setSecond (m_currentTime.second ()); } } void ABClockWidget::setDarkMode (bool enable) { m_isDark = enable; update (); } void ABClockWidget::setManualTime (const QTime &time) { m_anim->stop (); m_currentTime = time; m_hour = time.hour (); m_minute = time.minute (); m_second = time.second (); update (); emit timeUpdated (m_currentTime.toString("hh:mm:ss" )) ; } void ABClockWidget::paintEvent (QPaintEvent *) { QPainter painter (this ) ; painter.setRenderHints (QPainter::Antialiasing); qreal W = width (), H = height (); qreal side = qMin (W, H); painter.translate (W / 2 , H / 2 ); painter.scale (side / 200.0 , side / 200.0 ); QColor bgColor = m_isDark ? QColor (40 , 44 , 52 ) : QColor (245 , 245 , 240 ); QColor tickColor = m_isDark ? Qt::white : Qt::black; painter.setPen (QPen (QColor (60 , 65 , 70 ), 4 )); painter.setBrush (bgColor); painter.drawEllipse (-98 , -98 , 196 , 196 ); painter.setPen (tickColor); for (int i = 0 ; i < 60 ; ++i) { if (i % 5 == 0 ) { painter.setPen (QPen (tickColor, 2 )); painter.drawLine (86 , 0 , 95 , 0 ); } else { painter.setPen (QPen (tickColor, 1 )); painter.drawLine (91 , 0 , 95 , 0 ); } painter.rotate (6.0 ); } qreal offset = 270.0 ; painter.save (); painter.rotate (offset + (m_hour % 12 ) * 30.0 + m_minute / 2.0 ); painter.setPen (QPen (tickColor, 6 , Qt::SolidLine, Qt::RoundCap)); painter.drawLine (0 , 0 , 50 , 0 ); painter.restore (); painter.save (); painter.rotate (offset + m_minute * 6.0 + std::fmod (m_second, 60.0 ) * 0.1 ); painter.setPen (QPen (Qt::gray, 4 , Qt::SolidLine, Qt::RoundCap)); painter.drawLine (0 , 0 , 75 , 0 ); painter.restore (); painter.save (); painter.rotate (offset + std::fmod (m_second, 60.0 ) * 6.0 ); painter.setPen (QPen (QColor (230 , 50 , 50 ), 2 , Qt::SolidLine, Qt::RoundCap)); painter.drawLine (-15 , 0 , 85 , 0 ); painter.setBrush (QColor (230 , 50 , 50 )); painter.drawEllipse (-4 , -4 , 8 , 8 ); painter.restore (); } void ABClockWidget::updateTime () { m_currentTime = m_currentTime.addSecs (1 ); m_hour = m_currentTime.hour (); m_minute = m_currentTime.minute (); int nextSecond = m_currentTime.second (); if (m_isSmooth) { m_anim->stop (); qreal startSecond = std::fmod (m_second, 60.0 ); qreal endSecond = nextSecond; if (endSecond == 0.0 ) { endSecond = 60.0 ; } m_anim->setStartValue (startSecond); m_anim->setEndValue (endSecond); m_anim->start (); } else { setSecond (nextSecond); } emit timeUpdated (m_currentTime.toString("hh:mm:ss" )) ; }
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 #include "ABMainWin.h" #include "ABClockWidget.h" #include <QGroupBox> #include <QHBoxLayout> #include <QLCDNumber> #include <QPushButton> #include <QRadioButton> #include <QTimeEdit> #include <QVBoxLayout> ABMainWin::ABMainWin (QWidget *parent) : QWidget (parent) { auto main_layout = new QHBoxLayout (this ); m_clock = new ABClockWidget (this ); m_clock->setMinimumSize (300 , 300 ); auto ctl_layout = new QVBoxLayout (); auto group_lcd = new QGroupBox ("数字时间" ); auto layout_lcd = new QHBoxLayout (group_lcd); m_lcd = new QLCDNumber (group_lcd); m_lcd->setDigitCount (8 ); m_lcd->setSegmentStyle (QLCDNumber::Flat); m_lcd->display (QTime::currentTime ().toString ("hh:mm:ss" )); layout_lcd->addWidget (m_lcd); auto group_mode = new QGroupBox ("走针模式" ); auto layout_mode = new QHBoxLayout (group_mode); auto btn_mech = new QRadioButton ("机械跳动" ); auto btn_smooth = new QRadioButton ("平滑扫秒" ); btn_smooth->setChecked (true ); layout_mode->addWidget (btn_mech); layout_mode->addWidget (btn_smooth); auto group_theme = new QGroupBox ("主题" ); auto layout_theme = new QHBoxLayout (group_theme); auto btn_day = new QPushButton ("日间模式" ); auto btn_night = new QPushButton ("夜间模式" ); layout_theme->addWidget (btn_day); layout_theme->addWidget (btn_night); auto group_time = new QGroupBox ("设置时间" ); auto layout_time = new QHBoxLayout (group_time); auto time_edit = new QTimeEdit (QTime::currentTime ()); time_edit->setDisplayFormat ("hh:mm:ss" ); auto btn_go = new QPushButton ("跳转" ); layout_time->addWidget (time_edit); layout_time->addWidget (btn_go); ctl_layout->addWidget (group_lcd); ctl_layout->addWidget (group_mode); ctl_layout->addWidget (group_theme); ctl_layout->addWidget (group_time); ctl_layout->addStretch (); main_layout->addWidget (m_clock, 2 ); main_layout->addLayout (ctl_layout, 1 ); connect (m_clock, &ABClockWidget::timeUpdated, this , [this ](const QString &timeStr) { m_lcd->display (timeStr); }); connect (btn_mech, &QRadioButton::toggled, this , [this ](bool c) { if (c) m_clock->setSmoothMode (false ); }); connect (btn_smooth, &QRadioButton::toggled, this , [this ](bool c) { if (c) m_clock->setSmoothMode (true ); }); connect (btn_day, &QPushButton::clicked, this , [this ]() { m_clock->setDarkMode (false ); }); connect (btn_night, &QPushButton::clicked, this , [this ]() { m_clock->setDarkMode (true ); }); connect (btn_go, &QPushButton::clicked, this , [this , time_edit]() { m_clock->setManualTime (time_edit->time ()); }); }
8. 两个版本对比
项目里其实有两套实现——ClockWidget 和
ABClockWidget(“AB” 意为 “A
Better”,即改进版)。它们功能相同,区别在动画的边界处理:
对比点
ClockWidget(初版)
ABClockWidget(改进版)
59→0 处理
检测 m_second > 45 && target < 15 后
endSec += 60
直接判断 endSecond == 0.0 则设为 60.0
时针微调
m_minute / 2.0
同上
分针微调
m_second / 10.0(等价于 * 0.1)
fmod(m_second, 60.0) * 0.1(对平滑模式更安全)
改进版的逻辑更简洁直白,推荐以改进版为准。