简介这份 Qt 环形进度条示例面向已有一定 C 与 Qt 基础、希望在页面中补全自定义控件能力的开发者针对 Qt 自带控件仅有垂直和水平进度条、缺少环形展示形式的问题给出可参考的自定义实现适用于文件下载进度、任务完成度等需要圆形可视化的场景。压缩包共 7 个文件以 3 个 cpp 源文件和 2 个 h 头文件为主体另含 1 个 ui 界面文件与 1 个 pro 工程配置整体约 8KB规模轻量便于快速阅读和移植。目前已有 493 人学习下载。读者可从中了解环形进度条的绘制思路、进度值更新方式以及界面集成方法并据此替换颜色、线宽和尺寸较快搭建符合项目风格的圆形进度展示也能为 Qt 自定义绘制类控件积累可复用的实践参考。1. 一排横向进度条占满的界面环形进度条真正要解决什么某公司的设备监控上位机一块 10 寸工控屏上要同时显示六路通道的实时负载。第一版用 QProgressBar 横向排开占满整行宽度不说数值和条子还互相挤操作员离屏幕一米多就看不清楚。改版思路很直接同样面积换成六个圆形仪表盘进度用一圈弧线表示数字摆在圆心。麻烦在于 Qt Widgets 自带的控件库里没有这个形状QML 那边的圆形仪表盘又意味着整个工程得迁到 Quick 技术栈代价太大。落到实处的做法是在 QWidget 上用 QPainter 自绘一个环形进度条圆形进度条对外接口和 QProgressBar 保持同一套用法代码量两百多行。适合正在做 qt 上位机、设备面板、课设毕设里需要把进度展示做得像样一点的人也适合想补上自绘控件这块短板的开发者。2. 视觉规格与接口设计先把参数表和状态机定死自绘控件最容易犯的错是边写边调颜色和宽度写到一半发现「换个尺寸全乱了」再回头重构。我一般会先花半小时把视觉参数和对外接口用文字和表格固化下来再动键盘。2.1 一张参数表锁死所有可变项环形进度条看起来简单实际上可调项比想象的多。下面这张表是工控类项目里比较通用的一套默认值可以直接抄也可以按产品规范改参数类型默认值建议范围说明ringWidthRatioqreal0.100.06 ~ 0.18环宽占控件短边的比例不是绝对像素startAngleint90固定 90起始角度90 度 时钟 12 点方向sweepAngleint360固定 360整圈跨度半圆仪表盘可改 180directionenumClockwise顺/逆时针影响 spanAngle 的正负baseColorQColor#2A2F3A任意底环颜色通常比背景亮一档startColorQColor#3AA0FF任意进度起始色endColorQColor#7B61FF任意进度结束色capStyleQt::PenCapStyleRoundCapRoundCap/FlatCap圆角端点更柔和但会外溢半个环宽textVisiblebooltrue-圆心是否显示百分比animationDurationint400200 ~ 800数值变化时的过渡时长单位毫秒关键的一条是 ringWidthRatio 用比例而不是像素。工控屏可能是 1024×600笔记本可能是 1920×1080同一套界面代码要跑在两种分辨率上写死setRingWidth(12)的做法在高分屏上细得像根头发。用比例存绘制时再乘以短边尺寸自适应这件事就自动成立了。圆角端点这一项要单独提醒RoundCap 会让弧线两端各多出半个环宽的圆头如果你按控件矩形边缘去算绘制区域圆头会被裁掉看起来像被切了一刀。这一点在设计阶段就要预留余量别等到验收时才发现。2.2 对外接口暴露语义不暴露绘制细节一个控件好不好用看头文件就知道。我给这个环形进度条定的原则是外部只关心「当前值、范围、颜色、要不要动画」不关心你用的是 drawArc 还是 QPainterPath。// ringprogressbar.h class RingProgressBar : public QWidget { Q_OBJECT Q_PROPERTY(qreal currentValue READ currentValue WRITE setCurrentValue) public: explicit RingProgressBar(QWidget *parent nullptr); // 数值相关与 QProgressBar 保持一致的语义 void setRange(qreal min, qreal max); void setValue(qreal value); // 外部入口内部走动画 qreal value() const; // 视觉相关 void setRingWidthRatio(qreal ratio); void setColors(const QColor start, const QColor end, const QColor base); void setShowText(bool show); void setAnimationDuration(int ms); signals: void valueChanged(qreal value); protected: void paintEvent(QPaintEvent *event) override; private slots: void onAnimationTick(const QVariant v); private: void setCurrentValue(qreal v); // 动画属性写入点走属性系统 qreal currentValue() const; qreal m_min 0.0; qreal m_max 100.0; qreal m_currentValue 0.0; // 绘制用的插值不是目标值 qreal m_targetValue 0.0; // 外部设定的目标值 qreal m_ringWidthRatio 0.10; QColor m_baseColor{0x2A, 0x2F, 0x3A}; QColor m_startColor{0x3A, 0xA0, 0xFF}; QColor m_endColor{0x7B, 0x61, 0xFF}; bool m_showText true; int m_duration 400; QPropertyAnimation *m_anim nullptr; };这里有个刻意的设计m_currentValue和m_targetValue是两个变量。外部调用 setValue(80) 只改目标值并启动动画真正被 paintEvent 读取的是插值出来的当前值。很多人图省事只留一个成员变量结果动画一跑起来画面就跳帧——因为动画每一帧都在改同一个变量而外部如果按定时器高频推送数据两边打架值会来回抽搐。分开放之后谁改目标、谁改插值职责是分得清的。Q_PROPERTY那一行也别省。它的作用是让 QPropertyAnimation 能通过 Qt 的元对象系统直接驱动这个属性不用手写 connect 去接每一帧。这一点在第 4 章会展开。2.3 尺寸基准所有几何量从短边推导绘制之前的几何计算决定了控件在任意尺寸下是否都长得对。我的做法是只认短边// 在 paintEvent 里先算三个基准量 const qreal side qMin(width(), height()); // 短边 const qreal ringWidth side * m_ringWidthRatio; // 环宽 const qreal margin ringWidth / 2.0 1.0; // 端点与抗锯齿余量 QRectF arcRect(margin, margin, side - margin * 2, side - margin * 2); arcRect.moveCenter(QRectF(rect()).center()); // 非正方形时在控件里居中之所以要用moveCenter是因为控件不一定是正方形。放在布局里时QLayout 给的可能是 120×80 的长方形如果直接拿rect()当绘制区域画出来是个椭圆。用短边算出一个正方形再把它挪到控件中心无论外部怎么拉伸圆环始终是正圆并且居中。margin里那个 1.0不是玄学是给抗锯齿像素留的余量。抗锯齿会在边缘画出半透明的过渡像素如果绘制区域紧贴控件边界这一圈过渡像素就落在控件外面被裁掉圆环边缘看起来会有一圈硬边。多留一个逻辑像素问题就没了。还有一个容易忽略的点尺寸极小时side - margin * 2可能变成负数QRectF 宽高为负会导致绘制结果不可预测有些平台直接什么都不画。这属于第 5 章的排查范围但设计阶段就应该用qBound兜住。3. 绘制内核paintEvent 里三段弧线加一个锥形渐变绘制部分是这个控件的核心代码不长但每一行的顺序和参数都有讲究。拆开看paintEvent 里干的事就四件算几何、画底环、画进度环、写文字。3.1 绘制顺序决定视觉层次底环、进度环、中心文字这个顺序不能乱。先画底环是因为它永远在最下层进度环盖在底环上但只覆盖实际角度那一段文字最后画保证不会被任何东西遮挡。如果反过来先画进度环再画底环进度就被整圈灰环吃掉了看起来像根本不动。void RingProgressBar::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); // 抗锯齿必须开否则弧线是锯齿状 p.setRenderHint(QPainter::TextAntialiasing, true); const qreal side qMin(width(), height()); const qreal ringWidth side * m_ringWidthRatio; const qreal margin ringWidth / 2.0 1.0; QRectF arcRect(margin, margin, side - margin * 2, side - margin * 2); arcRect.moveCenter(QRectF(rect()).center()); if (arcRect.width() 0 || ringWidth 0) // 极小尺寸保护别让它算出负数 return; // ---- 第一层底环整圈 ---- QPen basePen(m_baseColor, ringWidth, Qt::SolidLine, Qt::RoundCap); p.setPen(basePen); p.drawArc(arcRect, 0, 360 * 16); // ---- 第二层进度环按比例 ---- const qreal span (m_max - m_min) 0 ? (m_currentValue - m_min) / (m_max - m_min) : 0.0; if (span 0.0) { const int startAngle 90 * 16; // 12 点方向 const int spanAngle -qRound(span * 360.0 * 16); // 负号 顺时针 QConicalGradient grad(arcRect.center(), 90.0); // 渐变起点也在 12 点 grad.setColorAt(1.0, m_startColor); // 12 点处是起始色 grad.setColorAt(0.0, m_endColor); // 绕回 12 点时是结束色 QPen progPen(QBrush(grad), ringWidth, Qt::SolidLine, Qt::RoundCap); p.setPen(progPen); p.drawArc(arcRect, startAngle, spanAngle); } // ---- 第三层圆心文字 ---- if (m_showText) { // 见 3.4 } }参数说明集中在几个地方。90 * 16里的 16 是 Qt 的角度单位约定drawArc 的角度以 1/16 度为单位90 度要写 1440。spanAngle取负值表示从起始角顺时针方向扫过正角度在屏幕上表现为逆时针。这两个约定如果混了进度条会从 12 点开始倒着往回走看起来极其别扭。span 0.0这个判断不能省。spanAngle 为 0 时RoundCap 的笔帽会让它在 12 点位置画出一个圆点进度为 0 的控件上莫名其妙多一颗珠子。用判断直接跳过绘制干净。3.2 角度换算1/16 度单位和方向是最大陷阱Qt 的 drawArc 有两个反直觉的地方几乎每个第一次写环形进度条的人都会踩。第一个是角度单位。Qt 不用弧度也不用度用 1/16 度这是历史遗留早期为了用整数表示角度。360 * 16 5760才是一整圈。很多人在网上抄代码时看到drawArc(rect, 90, -360 * 16)能跑自己改成drawArc(rect, 90, -360)就画不出东西原因就在这里。第二个是方向。Qt 的坐标系 y 轴向下但 drawArc 的角度正方向在屏幕上是逆时针的。所以要从 12 点顺时针画起始角写 90 度12 点位置跨度写负数。反过来写startAngle 90, spanAngle 也能画出弧但方向就是逆时针跟多数仪表的读数习惯相反。第三个隐式陷阱是百分比到角度的映射。如果范围是 30 到 90比如温度当前值 60直接拿 60/360 去算角度是错的必须先归一化(value - min) / (max - min)。这个除法还要防除零max min时直接返回 0。实际项目里我还会处理一种情况数值超界。外部推送的数据偶尔会因为协议解析出错跑出范围比如负载率变成 103%。这时span会大于 1画出来的弧会超过一整圈并盖住起点视觉上像环变粗了一截。所以取比例之后要qBound(0.0, span, 1.0)夹一下脏数据不至于把界面搞花。3.3 锥形渐变为什么不用 QLinearGradient进度环要好看颜色就得沿圆弧方向渐变而不是从左到右。QLinearGradient 是线性渐变在一个斜向矩形上铺色画到圆环上会出现明显的方向性断裂——同一段弧上颜色不均匀看起来像脏了。正确的工具是 QConicalGradient它以某个中心点为轴沿角度方向铺色天然贴合圆环。// 指定渐变中心为圆心起始角度 90 表示从 12 点方向开始铺色 QConicalGradient grad(arcRect.center(), 90.0); grad.setColorAt(1.0, m_startColor); // t1 与 t0 都在 12 点这里放进度起点色 grad.setColorAt(0.0, m_endColor); // 绕一整圈回到 12 点时是结束色为什么起始色放在 1.0 而不是 0.0因为 QConicalGradient 是沿逆时针方向铺色的t 从 0 增加到 1 表示角度逆时针转一圈而进度是顺时针增长的正好反着。把起始色放在 t1.0、结束色放在 t0.0进度从 12 点顺时针走过 25%到 3 点位置时实际落在渐变的 t≈0.75 处颜色刚好走了四分之一。视觉上就是「头亮尾暗」的渐变符合直觉。代价是 t0 和 t1 在 12 点位置重合但颜色不同会形成一条硬边。不过那条边正好在进度的起点被 RoundCap 的圆头盖住实际看不出来。这是这个方案能用两点色搞定渐变的原因。如果产品要求「进度头部永远是同一个亮色不管进度多少」那就不能靠固定渐变得在每次绘制时按当前比例重新计算 setColorAt 的位置把渐变随进度旋转。这种动态渐变计算量略大但对单控件来说完全不是瓶颈。3.4 圆心文字字号随尺寸走别写死圆心放百分比是环形进度条的标配但文字大小写死成QFont font; font.setPointSize(24);就是给未来的自己挖坑。控件从 200×200 缩到 80×80 时24 号字会撑破圆环。正确做法是按短边比例算字号再用 QFontMetrics 做精确居中QFont f font(); f.setPixelSize(qMax(8, int(side * 0.22))); // 字号 短边的 22% f.setBold(true); p.setFont(f); const QString text QString::number(qRound(m_currentValue)); QFontMetrics fm(f); QRect textRect rect(); textRect.moveTop(arcRect.center().y() - fm.height() / 2); p.setPen(QColor(#E8ECF3)); p.drawText(textRect, Qt::AlignHCenter | Qt::AlignVCenter, text);0.22 这个系数不是随便定的。环形进度条的圆心区域可用直径大约是side - ringWidth * 2取环宽为短边的 10%可用直径就是 0.8×side。三位数字要横下放字号超过短边的 25% 就会顶到内圈。0.22 留了一点呼吸空间视觉上也更稳。moveTop那一步是为了修正垂直居中。QFontMetrics 的 height 包含了字体的上下行距ascent descent leading直接把文字矩形对齐到圆心视觉上会偏下几个像素。把矩形顶部挪到「圆心 y 减去字高一半」文字才是真的在中间。这个几像素的偏差人在看单个控件时未必察觉但六个环形并排放在一起错位就很明显。数值取整用qRound而不是直接强转 int。强转是截断99.6 会显示成 99用户看到「明明已经满了怎么还是 99」会疑惑。四舍五入到 100 更符合直觉。4. 动画驱动把 value 做成属性交给 QPropertyAnimation进度条只画静态弧线也能交差但真正让界面「活」起来的是数值变化时那段过渡。用户看到指针从 40% 平滑走到 65%会下意识觉得数据是连续的瞬间跳过去反而像出了故障。4.1 用 Q_PROPERTY 让动画引擎直接驱动绘制Qt 的动画框架有两种用法。一种是 QVariantAnimation connect自己处理每一帧另一种是 QPropertyAnimation直接绑定到 Q_PROPERTY 上。后者的代码量少一半而且自带状态管理start/stop/pause我更倾向这种。RingProgressBar::RingProgressBar(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent, false); // 允许背景透出便于放在卡片控件里 m_anim new QPropertyAnimation(this, currentValue, this); m_anim-setDuration(m_duration); m_anim-setEasingCurve(QEasingCurve::OutCubic); } // 外部调用入口只设目标值不直接改绘制值 void RingProgressBar::setValue(qreal v) { v qBound(m_min, v, m_max); if (qFuzzyCompare(v 1.0, m_targetValue 1.0)) return; // 目标没变就别重启动画否则会一直抖 m_targetValue v; m_anim-stop(); m_anim-setStartValue(m_currentValue); // 从当前已画到的位置接着走 m_anim-setEndValue(m_targetValue); m_anim-start(); } // 属性写入点动画引擎每帧调一次 void RingProgressBar::setCurrentValue(qreal v) { if (qFuzzyCompare(v 1.0, m_currentValue 1.0)) return; m_currentValue v; update(); // 只触发重绘别在这里写业务逻辑 }setStartValue(m_currentValue)这一行是平滑的关键。如果不设动画每次都从 0 开始第一个数值从 0 涨到 40 是好看了第二个数值从 0 涨到 65 就会出现「指针先回零再往上冲」的鬼畜画面。用当前插值当起点无论外部怎么高频改目标视觉上永远是连续地朝新目标走。qFuzzyCompare前面加个 1.0是因为这个函数对 0 附近的值会误判内部做除法。这是 Qt 的老坑浮点数比较写成qFuzzyCompare(a 1.0, b 1.0)就能规避。4.2 缓动曲线不同业务场景选不同手感QEasingCurve 有几十种曲线环形进度条常用的就那么几个曲线手感适用场景Linear匀速机械感持续计时、下载进度这类线性增长的数据OutCubic起步快、收尾缓绝大多数仪表盘默认选择OutQuint更快起步、更长收尾数值跳变幅度大如从 10 到 90时更顺InOutQuad两头慢中间快双向变化涨落都频繁的场景OutBack末尾轻微过冲再回弹展示型大屏娱乐化界面工业场景慎用OutCubic 是最保险的默认值。它在 70% 的时间里走完 90% 的路程最后 30% 慢慢收尾人的眼睛会自动把这段收尾读成「稳定下来了」跟仪表指针停住的感觉高度吻合。OutBack 那种回弹效果要慎用。在一个显示电机转速的工控界面上指针冲过头再弹回来操作员第一反应是「设备出问题了」。缓动曲线是情绪表达工业场景里情绪要稳。时长也不是越长越好。400ms 是一个经验值低于 200ms 人眼几乎感知不到过渡等于白做超过 800ms连续刷新的数据会让动画永远追不上目标值最终显示的是个逐渐落后的滞后值。数据刷新频率高的时候时长要跟着压到 250ms 以内。4.3 数据源解耦外部推得比动画快怎么办真实项目里环形进度条的值往往来自串口、Modbus 或者 WebSocket推送频率可能 10Hz 甚至更高。而动画的刷新率跟着屏幕走通常 60Hz。两边频率不一致直接调 setValue 会出现两种问题一是动画被反复 stop/start永远走不完第一段二是每帧都在 updateCPU 占用飙升。我的处理分两层。第一层在控件内部做节流距离上次 setValue 不足 16ms 的变化只更新 m_targetValue 不重启动画等下一帧再统一处理。void RingProgressBar::setValueThrottled(qreal v) { m_pendingValue qBound(m_min, v, m_max); if (!m_throttleTimer-isActive()) { applyPendingValue(); // 立即响应第一次变化 m_throttleTimer-start(16); // 16ms 内的后续变化合并 } } void RingProgressBar::applyPendingValue() { setValue(m_pendingValue); // 走 4.1 的动画入口 }第二层在调用方如果数据源本身是 1Hz 的传感器读数那就别做 400ms 的动画了直接 setCurrentValue 瞬间到位反而更真实。环形进度条是表现层动画时长应该跟数据的时间尺度匹配不是所有场景都需要过渡。还有一种极端情况数据源推送的值本身就是抖动的比如负载在 58% 到 62% 之间高频跳动。这种时候平滑动画会把抖动放大成「呼吸灯」效果看着心烦。可以用一个简单的滑动平均把最近 5 次的均值作为目标值视觉上稳定很多。这个逻辑不该塞进绘制控件里放在数据接收层更合适。5. 避坑与排查环形进度条上线前必须过的五道关前面几章的代码跑起来能出画面但从「能看」到「能交付」中间隔着一堆细节。下面这五条是我在不同项目里真实撞过的按现象、原因、解决拆开写。5.1 开了抗锯齿边缘还是有毛边现象setRenderHint(QPainter::Antialiasing, true)明明设了圆环外沿还是能看到一圈生硬的锯齿尤其是斜向的弧线段。原因不在抗锯齿本身而在绘制区域被裁。前面算 arcRect 时如果 margin 只留了环宽的 1/2RoundCap 圆头加上抗锯齿产生的半透明像素正好落在控件矩形边界外被 QWidget 的裁剪区切掉剩下的就是一条硬直边。另外如果控件本身放在了带圆角或者透明背景的父控件里父级裁剪也会造成同样效果。解决办法是把余量从ringWidth / 2加到ringWidth / 2 1并且确认控件没有设置WA_TranslucentBackground之外的异常裁剪属性。还有一种可能控件尺寸是奇数中心点落在半像素上抗锯齿会把它均匀分成两个半透明像素视觉上反而发虚。遇到这种情况可以在 resizeEvent 里把控件尺寸向上取偶或者干脆接受这点差异——在 1080p 屏上肉眼几乎看不出。5.2 进度从 12 点开始逆时针走越走越回去现象数值增大时弧线不是顺时针变长而是从 12 点往 9 点方向长最终跟仪表盘的习惯完全相反。原因是 spanAngle 的符号写反了。Qt 的角度正方向在屏幕上是逆时针要从 12 点顺时针扫跨度必须取负。很多人按数学直觉写正数结果就是反向。把-qRound(span * 360.0 * 16)的负号补上就好。还有一种是渐变和弧线的方向对不上进度顺时针走但QConicalGradient是逆时针铺色如果 setColorAt 的位置没按第 3.3 节那样对调会出现「弧线头部的颜色是渐变的尾部色」这种别扭效果。判断方法是把进度设成 50%看 3 点位置和 9 点位置的颜色是不是跟设计的起止色对应。不对就调换 setColorAt 的 t 值。5.3 界面卡顿、CPU 占用异常高现象一个页面上放了六个环形进度条刷新时风扇开始转拖动窗口有明显的掉帧。先用排查工具定位Qt Creator 的 Analyzer 里看 CPU 火焰图或者简单粗暴地在 paintEvent 里打点计时。多数时候罪魁祸首是绘制频率。如果外部数据源是 50Hz 推送每次都走完整动画 update那绘制频率就跟着上去了六个控件加起来每秒三百次重绘扛不住很正常。解法按优先级排第一控件的 setValue 加节流第 4.3 节的 16ms 合并第二在不可见时停止动画重写 showEvent/hideEvent隐藏状态下m_anim-stop()避免后台空转第三检查有没有在 paintEvent 里构造 QFont、QPen、QColor 这类对象它们应该做成成员变量每帧新建对象会造成不必要的分配第四如果只是数字在变设置Qt::WA_OpaquePaintEvent减少背景重绘。还有一条容易被忽略QPainter 的 Antialiasing 在高频绘制下开销不小。如果控件尺寸很小比如 60×60 的状态指示可以关掉抗锯齿换成 2 倍超采样绘制再缩放性能反而更好。5.4 尺寸变成 0 或者极小时控件崩了现象把环形进度条放进 QSplitter 或者可折叠面板拖到最小的时候程序崩溃或者画面上出现一团乱七八糟的色块。原因是几何计算出负数。side - margin * 2在 side 小于 2 倍 margin 时变负QRectF 宽高为负drawArc 的行为是未定义的某些平台上直接触发断言。同理ringWidth 算出 0 时QPen 的宽度为 0 是「cosmetic pen」会画成 1 像素宽的细线跟预期完全不符。修复就是在 paintEvent 开头加保护if (arcRect.width() 0 || ringWidth 0) return;。更稳妥的做法是用 qMax 兜底把 ringWidth 限制在[1.0, side * 0.3]之间让极小尺寸下控件退化成一个小圆点而不是崩溃。这个保护必须写别指望布局系统永远不给零尺寸。5.5 setStyleSheet 一加自绘效果全没了现象为了统一界面风格给父窗口设了全局样式表结果环形进度条的颜色不生效了或者背景变成一块白底。原因是 QWidget 的样式表机制和自绘会互相干扰。一旦控件被 QStyleSheetStyle 接管paintEvent 的调用时机和背景擦除逻辑都会变。具体表现为控件默认的autoFillBackground被样式表打开paintEvent 之前先刷了一层背景你的绘制被盖住或者样式表里的color属性影响了p()-setPen之外的绘制路径。解决办法有两个方向。一是彻底隔离给控件设setAttribute(Qt::WA_StyledBackground, false)并在 paintEvent 里显式p.fillRect(rect(), Qt::transparent)或者按需填充背景色把绘制权完全拿回来。二是顺应样式表把颜色配置成 Q_PROPERTY在qproperty-xxx里从样式表读这样主题切换时能跟着走。工控项目里我一般选第一种简单可控多主题的消费级产品选第二种可维护性更好。判断到底是哪种冲突最快的办法是临时把全局样式表注释掉看控件是否恢复正常。恢复了就是样式表的问题没恢复就回去查绘制代码。6. 验收与答辩一套能当场自证的检查表环形进度条这种控件验收时最怕遇到「看着挺好」四个字。评审的人说不出具体哪里不行但你自己心里也没底。我的做法是准备一张能当场跑、当场看结果的检查表把主观判断变成可复现的验证动作。验收项验证方法通过标准尺寸自适应把窗口从 400×400 拖到 80×80 再拉满圆环始终正圆、居中、文字不溢出、不崩溃方向与起点设 value 为 25%截图对照时钟弧线从 12 点出发顺时针到 3 点范围映射设 range(30, 90)value 为 60显示半圈文字为 60超界保护直接 setValue(150)画满整圈后停止不越界不重叠动画连续性1 秒内连续调 5 次 setValue指针连续前进无回零、无抽搐数值精度value 设 99.6 与 99.4分别显示 100 与 99主题颜色改 startColor/endColor 后立即生效无需重启下次重绘即变隐藏状态把控件 hide 后持续推送数据CPU 占用回落无后台动画答辩环节评委最常问的三个问题基本可以预判。第一个是「为什么不用 QML 现成的组件」回答要落在技术栈代价上现有工程是 Widgets引入 Quick 需要额外维护一套渲染后端和打包依赖收益不足以覆盖迁移成本。第二个是「性能怎么样」直接给出量化数据比形容词有说服力——六个控件同时以 20Hz 刷新CPU 占用多少有没有做节流。第三个是「怎么保证不同分辨率下都对」把短边比例那套几何推导讲清楚比说「测过了」可信得多。我自己在这上面栽过一次。第一版交付时只测了开发机的 1920×1080到现场工控屏 1024×600 上一跑圆环压扁成椭圆文字顶到了环外面。原因就一句话几何计算里有一处写死了像素值。从那以后我每次做自绘控件都会强制在 80、200、1080 三档尺寸下各截一张图摆在一起对比着看。这十秒钟的动作省掉的是往返现场的时间。这套环形进度条我已经整理成一份可直接编译的 Qt Widgets 工程包含完整的头文件、源文件、一个演示窗口和上面那张检查表对应的测试用例。需要的话按标题找过去就行希望能帮到你。本文还有配套的精品资源点击获取