☰
Qt自绘电池进度条:paintEvent实战与避坑指南
2026/10/15 11:06:59 网站建设 项目流程

简介:一套面向Qt初学者的自定义控件示例,聚焦重写paintEvent实现电池进度条,覆盖QPainter基本绘制、电量比例计算、状态颜色切换与QPropertyAnimation动画驱动,适合需要掌握Qt绘图和控件封装的开发者。压缩包共8个文件,含3个cpp源文件、2个h头文件、1个pro工程文件、1个ui界面文件及1个工程配置user文件,从源码实现、类声明到可视化布局和构建配置均有体现,仅7KB体积结构清晰,便于逐文件拆解学习。已有498人学习使用。示例逐层绘制电池外壳与电量内芯,并以80%、20%为阈值切换正常色、警示色与过渡色,直观呈现电量状态;同时演示QPropertyAnimation驱动电量变化触发动态更新,以及避免在paintEvent中重复计算、分离绘制逻辑与动画逻辑等优化手法。读者可借此快速熟悉Qt自定义控件从绘图到动画的完整实现链路,是入门Qt图形界面设计的实用参考。

1. 用paintEvent自绘电池:别用三个QLabel拼出一个假电池

要做一个带百分比数字的电池进度条,很多人的第一反应是拼控件:一个外框 QLabel、一个填充 QProgressBar、再加一个文字 QLabel。这个方案在小窗口里看着还行,一旦放到状态栏、DPI 变化或者窗口拉伸,三个控件的对齐就开始互相打架——圆角的弧度对不上、文字偏移两三个像素、电量填充的边距忽宽忽窄。用 paintEvent 自绘电池,就是让外框、电量填充、刻度、文字共用同一套坐标,在一次重绘里全部画完。这篇笔记围绕 Qt 绘制电池进度条(paintEvent)实现里最难拿捏的几个点展开:重绘触发机制、坐标系规划、绘制顺序、动画刷新,以及我踩过的几个真实坑。适合会用 QWidget、但还没系统梳理过自绘控件的开发者,照着代码走一遍就能落地使用。

2. 电池控件的底座:paintEvent 的触发机制与坐标规划

自绘控件的第一步不是画,而是搞清楚 paintEvent 什么时候会被调用、每次调用要干什么。很多新手把绘制逻辑写在业务槽函数里,数据一变就调 repaint(),结果界面又卡又闪。这一章先把「谁在叫 paintEvent」讲清楚,再给出一套坐标计算方案,这两件事理顺了,后续画出来的东西才是稳的。

2.1 重绘从哪来:update()、repaint() 与系统事件

paintEvent 由事件系统驱动,不是想调就能调的。最常见的触发源有三类:窗口系统事件(窗口被遮挡、移动、resize)、显式调用 update()、显式调用 repaint()。窗口系统事件不受我们控制,但后两个可以控制。

一个典型的电池控件骨架长这样:

class BatteryWidget : public QWidget { Q_OBJECT public: explicit BatteryWidget(QWidget *parent = nullptr); void setValue(int percent); int value() const { return m_percent; } protected: void paintEvent(QPaintEvent *event) override; private: int m_percent = 62; // 默认显示 62% 电量 };

setValue 的写法是整个控件的关键入口:

void BatteryWidget::setValue(int percent) { percent = qBound(0, percent, 100); // 把电量强制限制在 0~100 之间 if (m_percent == percent) return; // 值没变,直接返回,避免无谓重绘 m_percent = percent; update(); // 异步请求一次重绘 }

这里的逻辑有必要拆开说。qBound 保证了数据合法性,外界传入 120 或 -5 都不会把控件画崩;提前 return 处理了「连续 setValue 同一个值」的重复重绘问题;最后的 update() 才是重绘的实际入口。update() 是异步的,它只标记当前控件需要重绘,然后回到事件循环里统一派发 paintEvent,所以连续两次 setValue 只会触发一次重绘。这一点对动画场景尤其重要:充电动画里每秒要刷新 30 次,如果用 repaint() 同步重绘,界面会明显掉帧。

repaint() 与 update() 的区别在于它是同步的:调用 repaint() 会立即执行 paintEvent,不等事件循环。常规 UI 场景我几乎不用 repaint(),只有一种情况例外——必须在某一帧里立刻把最新状态画出来,比如视频播放器直接把帧数据贴到控件上。画一个电池进度条,用 update() 就够了。

还有一点值得注意:paintEvent 每次被调用时,整个控件区域都会被重绘。事件对象里带了需要更新的 region 信息,但绝大多数场景直接用 event->rect() 或者干脆不理会事件参数,整块重绘。局部重绘优化只在控件特别复杂时才需要考虑,电池控件只有几行绘制,整块重绘的成本可以忽略。

写自定义控件时,推荐在构造函数里设置两个属性:

BatteryWidget::BatteryWidget(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent, true); setAttribute(Qt::WA_NoSystemBackground, false); }

WA_OpaquePaintEvent 告诉 Qt 这个控件会画满整个区域,不用先做背景清除,直接省掉一次清屏操作;WA_NoSystemBackground 也是类似的语义。如果控件有透明区域,这两个属性就不要开,否则会出现拖影。

2.2 先算坐标再动笔:外壳、壳体、内胆的尺寸分层

画电池最忌在 paintEvent 里写一堆魔法数字,比如 drawRect(10, 10, 80, 20)。控件一旦 resize,这些数字就全废了。我的习惯是先把电池拆成三层:外壳、壳体、内胆,每个层次单独用一个函数计算坐标,绘制代码只消费计算结果。

QRectF BatteryWidget::shellRect() const { const qreal tipWidth = 12; // 正极凸起占用的宽度 const qreal margin = 2; // 与外边界的间距 return QRectF(margin, margin, width() - tipWidth - margin, height() - 2 * margin); } QRectF BatteryWidget::innerRect() const { QRectF shell = shellRect(); return shell.adjusted(4, 4, -4, -4); // 壳体内边距 4px }

这里把正极凸起放进了坐标规划,而不是画的时候临时加偏移。原因是后续的刻度线、文字、电量填充都要以外壳左边界为参照,正极凸起的宽度在 shellRect 里被提前扣除后,所有子元素都能天然对齐。

两个函数返回的都是 QRectF 而不是 QRect,是有意为之。QRect 用整数坐标,在 DPI 缩放或圆角矩形的浮点边界下容易产生 1px 的误差累积;QRectF 在计算阶段保留小数,哪怕最终绘制时像素会取整,误差也被均匀分摊到边缘,视觉上更干净。

为什么要给电量填充留 4px 内边距?一是视觉上让电量「泡在」壳体里,更贴近真实电池;二是当电量百分比特别低时,填充矩形变窄,如果紧贴外壳,圆角矩形在窄条状态下会画出怪异的形状,留出内边距后仍然能保持对称。刻度线则画在外壳和内胆之间的夹层区域,这个细节到了第 3 章再展开。

坐标规划做完后,绘制顺序也必须钉死。我固定用五步:先画外框,再画内胆背景,然后画电量填充,接着画高光与刻度,最后写文字。顺序不能乱——先写文字再画填充,文字会被盖住;把高光放在最后,高光会把填充边缘盖掉,看起来像电池缺了一块。颜色方案上我踩过坑,后面避坑章会有详细记录。

3. 绘制电池外观:圆角外框、渐变高光与分段电量

到了真正动笔的环节。这一章的代码量是全文最多的部分,建议直接照抄后改参数跑一遍,再根据视觉效果微调颜色。我给出的是一套比较通用的灰白配色,深浅两套主题下都能看清。

3.1 外框与正极凸起:圆角半径怎么定

电池外壳用圆角矩形画,半径和尺寸比例直接相关。高度 30px 左右的控件,圆角半径 6px 是合适范围;半径再大,电池就会变成胶囊形状,不像电池。正极凸起是一个小长条矩形,高约 8px,圆角半径 2px,与外壳中心线对齐。

void BatteryWidget::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); QRectF shell = shellRect(); QRectF tip(shell.right() + 3, shell.center().y() - 4, 8, 8); // 正极凸起,宽 8 高 8 QPen framePen(QColor(0x88, 0x8e, 0x99)); framePen.setWidthF(1.5); painter.setPen(framePen); painter.setBrush(Qt::NoBrush); painter.drawRoundedRect(shell, 6, 6); painter.drawRoundedRect(tip, 2, 2); }

代码逻辑:先用 shellRect 拿到外壳矩形,tip 的 x 坐标接在外壳右边界之后,y 方向手动偏移半高来实现垂直居中。画笔宽度 1.5px 比默认的 1px 略粗,外框轮廓更清晰,又不会像 2px 那样在小尺寸下吃掉太多内部空间。setBrush(Qt::NoBrush) 是这一小段里最容易漏掉的语句,漏掉的后果第 5 章会单独讲。

外框画完后,内部还是透明的。要做真实感,需要在壳体内画一层浅灰背景,模拟空电池的暗腔:

painter.setPen(Qt::NoPen); painter.setBrush(QColor(0xee, 0xf0, 0xf3)); painter.drawRoundedRect(shell.adjusted(1, 1, -1, -1), 5, 5);

这层底色的意义在于,无论电量多低,视觉上都能看出电池内部不是空的。adjusted(1,1,-1,-1) 让底色比外壳小一圈,避免灰色背景在圆角处溢出外壳边缘。

接下来是外壳顶部的高光。真实电池的外壳是光滑塑料,上边缘会形成一条白色反光带。这个效果用 QLinearGradient 就能实现,成本极低但观感提升明显:

QLinearGradient gloss(shell.topLeft(), shell.bottomLeft()); gloss.setColorAt(0.0, QColor(255, 255, 255, 180)); gloss.setColorAt(0.12, QColor(255, 255, 255, 40)); gloss.setColorAt(1.0, QColor(255, 255, 255, 0)); painter.setPen(Qt::NoPen); painter.setBrush(gloss); painter.drawRoundedRect(shell, 6, 6);

渐变沿垂直方向展开,顶部是半透明白色,到 12% 高度处迅速衰减到接近透明,整条高光只占外壳高度很少的一部分,不喧宾夺主。如果不需要高光,把这段删掉不影响功能。

3.2 电量填充:纯色、渐变与低电量分色策略

电量填充是控件的灵魂。最简单的实现是纯色矩形,但真实设备在满电和低电量时视觉反馈差异很大,所以把颜色按电量分段更实用:

QColor BatteryWidget::fillColor() const { if (m_percent >= 60) return QColor(0x2e, 0xcc, 0x71); // 健康段:绿色 if (m_percent >= 20) return QColor(0xf3, 0x9c, 0x12); // 警戒段:橙色 return QColor(0xe7, 0x4c, 0x3c); // 危险段:红色 }

60/20 是移动设备常见的电量分割点。如果你的场景是工业设备,要求 40% 就进入警戒,直接改阈值和颜色即可。注意不要在 paintEvent 里临时计算三个颜色再比较,把颜色决策抽成单独函数,后期调色只改一个地方。

填充的绘制核心在于宽度计算和圆角处理:

QRectF inner = innerRect(); qreal fillWidth = inner.width() * m_percent / 100.0; QPainterPath innerClip; innerClip.addRoundedRect(inner, 4, 4); painter.save(); painter.setClipPath(innerClip); // 限定绘制范围到内胆圆角 QColor fill = fillColor(); painter.setPen(Qt::NoPen); painter.setBrush(fill); painter.drawRect(QRectF(inner.left(), inner.top(), fillWidth, inner.height())); painter.restore();

fillWidth 把百分比线性映射到内胆宽度,这个公式保证控件 resize 后填充比例依旧正确。为什么不用 drawRoundedRect 画填充?因为当 fillWidth 只有 5px 而圆角半径写 4px 时,Qt 会把矩形强行画成接近胶囊的形状,中间直线几乎消失。用 clipPath 限定绘制范围后,填充矩形即使越界也会被圆角剪裁得整整齐齐,这是替代「窄条圆角矩形」最稳的方案。clip 路径的半径要与内胆圆角一致,否则左边上下角会露直角。

如果追求更精致的液面观感,可以加垂直渐变:

QLinearGradient liquid(fillRect.topLeft(), fillRect.bottomLeft()); liquid.setColorAt(0.0, fill.lighter(120)); liquid.setColorAt(1.0, fill.darker(115)); painter.setBrush(liquid); painter.drawRect(fillRect);

lighter(120) 把当前颜色提亮 20%,darker(115) 压暗 15%,渐变方向自上而下模拟光从液体上层进入的立体感。个人经验:渐变只适合宽条填充,fillWidth 很窄时亮暗过渡会显得杂乱,此时直接退回纯色更好。

3.3 刻度线与充电状态边框

最后一步是给电池加刻度线,增加精细感。刻度线画在壳体底部,像真实电池的凹槽标注,每段长约 4px 就够,位置在内胆下边缘与外壳之间:

painter.setPen(QPen(QColor(0xbb, 0xc0, 0xc8), 1)); for (int i = 1; i < 5; ++i) { qreal x = inner.left() + inner.width() * i / 5.0; painter.drawLine(QPointF(x, shell.bottom() - 6), QPointF(x, shell.bottom() - 3)); }

循环从 1 到 4,把内胆宽度等分成五段,四条刻度线均匀分布。x 坐标完全基于内胆计算,控件拉伸成任何比例,刻度都跟随壳体走。颜色选比外框浅一个层次的灰,不抢戏。

充电状态的外框变色逻辑可以放在这里一并处理,通过一个 bool 标志位切换边框颜色,让外框从灰色变为绿色,视觉上立刻识别出「正在充电」。静态电池绘制到这里闭环了。把 BatteryWidget 放进窗口 setFixedSize(100, 36),编译运行,你会看到一个灰白外壳、绿色填充、底部带四条刻度的小电池。如果没有达到预期,多半是坐标计算或绘制顺序的问题,对着代码逐段比对即可。

4. 让电池会说话:文字基线、充电动画与呼吸灯

稳定的电池只能算半成品,工程落地还需要百分比文字、电量变化的动画过渡、充电状态的视觉提示。这一章的内容来自我在设备状态栏项目里的实际实现,算是把电池控件从「能看」变成「会用」的关键。

4.1 百分比居中:把 QFontMetrics 的基线算对

很多开发者会在 paintEvent 里这样写文字:

painter.drawText(innerRect(), Qt::AlignCenter, QString::number(m_percent) + "%");

这段代码表面看没问题,实际运行时会发现文字整体偏上或偏下 1~2px,换一种字体偏移量又不一致。原因在于 drawText 的矩形对齐以「行盒」为基准,而视觉中心其实是字体的 x-height 中心,两者有偏移。想让百分比在内胆里真正视觉居中,必须手动算基线位置:

QString text = QString::number(m_percent) + "%"; QFont font = painter.font(); font.setPixelSize(14); font.setBold(true); painter.setFont(font); QFontMetricsF fm(font); QRectF textRect = innerRect(); qreal textWidth = fm.horizontalAdvance(text); qreal baseline = textRect.center().y() + (fm.ascent() - fm.descent()) / 2.0; painter.setPen(QColor(0x33, 0x33, 0x33)); painter.drawText(QPointF(textRect.center().x() - textWidth / 2.0, baseline), text);

baseline 的公式逻辑:字体绘制时文字主体的真实高度是 ascent + descent,center 位置应该在 baseline 上方 descent 一半处。所以从矩形中心向下挪 (ascent - descent) / 2,文字主体就会真正居中。这个公式可以原样套到任何文字自绘控件,建议存成工具函数复用。

文字颜色也要斟酌。电量 35% 时绿色背景配白字没问题;电量 90% 时背景是亮绿色,白字就几乎看不清了。所以我在电量高于 80% 时切换到深色文字:

if (m_percent > 80) painter.setPen(QColor(0x1f, 0x2d, 0x3d)); else painter.setPen(QColor(0xff, 0xff, 0xff));

这个细节看起来不值一提,但满电状态时深灰字和低电量时白字,可读性差距非常明显。我第一次只用了白字,结果满电时数字糊成一团,后来才补上这个分支判断。

4.2 QPropertyAnimation 驱动的电量过渡动画

电量从 20% 跳到 85%,界面直接刷新会显得很生硬。真实设备充电时电量是渐进增长的,用 QPropertyAnimation 插值一个 m_visualPercent 是最贴近真实观感的做法。

先让控件暴露可动画属性:

class BatteryWidget : public QWidget { Q_OBJECT Q_PROPERTY(int visualPercent READ visualPercent WRITE setVisualPercent) public: int visualPercent() const { return m_visualPercent; } void setVisualPercent(int value) { m_visualPercent = value; update(); } void animateSetValue(int targetPercent); private: int m_visualPercent = 62; QPropertyAnimation *m_animation = nullptr; };

animateSetValue 创建并启动动画:

void BatteryWidget::animateSetValue(int targetPercent) { if (m_animation) { m_animation->stop(); m_animation->deleteLater(); } m_animation = new QPropertyAnimation(this, "visualPercent", this); m_animation->setDuration(500); m_animation->setStartValue(m_visualPercent); m_animation->setEndValue(targetPercent); m_animation->setEasingCurve(QEasingCurve::OutCubic); m_animation->start(); }

Q_PROPERTY 这一行是整个动画能跑起来的前提。QPropertyAnimation 通过属性名查找 setter,如果类没有声明 Q_PROPERTY,动画会直接报错停止。动画时长 500ms 比较平衡:太短像瞬移,太长让用户觉得电量刷新迟钝;OutCubic 让动画先快后慢,接近真实电量的爬升手感。

还有一个容易忽略的点:动画插值过程中 setVisualPercent 被调用多次,每次都触发 update(),500ms 的动画大概会触发 30~60 次重绘。单个控件完全没问题,但如果同时有 20 个电池控件做动画,就得考虑让所有电池共享一个 100ms 的公共 QTimer 统一刷新。我后来的仪表盘项目就是让所有电池共享刷新定时器,动画只更新数值,重绘频率由外部控制。

4.3 充电状态:闪电符号与呼吸效果

充电中的电池应该有区别于普通状态的外观。我的做法是外框改色加呼吸效果。呼吸效果需要一个随时间变化的 alpha 值,用 QTimer 累加相位后触发重绘:

QTimer *breathTimer = new QTimer(this); connect(breathTimer, &QTimer::timeout, this, [this]() { m_breathePhase += 0.15; if (m_breathePhase > 2 * M_PI) m_breathePhase = 0; update(); }); breathTimer->start(60);

绘制时把外框颜色按正弦做透明度变化:

qreal breatheAlpha = 0.55 * (1 + sin(m_breathePhase)); QColor breathColor = QColor(0x26, 0x9a, 0x5c, int(120 + 130 * breatheAlpha)); QPen breathPen(breathColor); breathPen.setWidthF(2.5); painter.setPen(breathPen); painter.drawRoundedRect(shell, 6, 6);

呼吸的本质是 alpha 在 120 到 250 之间平滑摆动,绿色边框像「透出」又「隐入」。定时器 60ms 一帧配上正弦波形,观感足够流畅。呼吸只改变描边颜色,不重新计算坐标,绘制开销和静态时几乎一样。

闪电符号用 QPainterPath 画折线,放在内胆中央:

QPainterPath bolt; qreal cx = inner.center().x(); qreal cy = inner.center().y(); bolt.moveTo(cx + 5, inner.top() + 2); bolt.lineTo(cx - 4, cy); bolt.lineTo(cx + 1, cy); bolt.lineTo(cx - 5, inner.bottom() - 2); bolt.lineTo(cx + 4, cy - 1); painter.setPen(QPen(QColor(0xff, 0xff, 0xff), 2)); painter.drawPath(bolt);

路径把闪电画成上下两折,白色描边配合绿色边框,视觉上像是真有一个小闪电嵌在电池里。路径坐标全部基于 inner 中心偏移,无论控件尺寸如何变化,闪电始终居中。

5. 电池自绘的避坑指南:五条血泪排查记录

这一章整理的是我在实际项目里踩过、以及帮同事排查过的典型问题,每条按「现象 → 原因 → 解决」的结构写。问题单独看都不大,但排查起来往往要花掉一个下午。

5.1 现象:拖动窗口时电池区域疯狂闪烁、CPU 占用飙升

原因:绝大多数是 paintEvent 里做了耗时操作,比如每次重绘都读取并缩放图片、创建字体或 QPixmap 对象。闪烁的根源是重绘频率过高且每次重绘都要重新分配资源,Qt 的自动擦除背景和绘制叠加,就成了肉眼可见的撕裂。

解决:把所有耗时资源创建移出 paintEvent,在构造函数里预创建,paintEvent 里只做绘制。如果控件有半透明叠加需求,启用 Qt::WA_NoSystemBackground 并自行负责背景清除。再检查代码里是否误用了 repaint()——改成 update(),让 Qt 合并重绘请求,闪烁通常立刻缓解大半。

5.2 现象:圆角矩形边缘出现一圈锯齿白边,放大看还有缺口

原因:绘制时没开抗锯齿,或者内胆填充与外框圆角用了不同半径的圆角矩形叠加,导致局部像素覆盖不均。白边是外壳底色与填充色之间的缝隙在设备像素上取整产生的。

解决:paintEvent 开头先执行 painter.setRenderHint(QPainter::Antialiasing, true);内胆 clip 路径的圆角半径与内胆保持一致;再检查内胆与外框间距是否落在整数像素上。1.5px 画笔在高 DPI 下会产生半像素偏移,必要时将画笔宽度调整为整数。

5.3 现象:百分比文字总是偏上两三个像素,换字体后偏移量还变

原因:用 drawText(rect, Qt::AlignCenter) 时 Qt 按行盒垂直居中,不同字体的 ascent/descent 比例不同,行盒中心与视觉中心不重合。

解决:弃用矩形对齐,改用 4.1 节的基线公式。先取字体度量,算 baseline,再用 drawText(QPointF, text) 定点绘制。这个方法对任何文字自绘控件通用,抽成工具函数后到处复用。

5.4 现象:想画空心外框,结果画成了实心圆角块

原因:画笔与画刷混淆。上一段代码里给 painter 设置了内部背景画刷,画外框时忘了恢复为 Qt::NoBrush,drawRoundedRect 就同时用了画刷填充整个圆角矩形。

解决:养成习惯,每次绘制前统一设置画笔和画刷:

painter.setPen(framePen); painter.setBrush(Qt::NoBrush); painter.drawRoundedRect(shell, 6, 6);

同一段代码里既有填充又有描边时,用 painter.save() 与 painter.restore() 把状态包起来,避免后续绘制污染。

5.5 现象:调了 setValue 界面却长时间不更新,甚至完全不更新

原因:常见三种——setValue 里忘了调用 update();百分比值没变被早退拦截;用了 QPropertyAnimation 但类没声明 Q_PROPERTY,动画直接失败,数值停在旧值。第三种最隐蔽,代码不报错,只有运行时才发现数值纹丝不动。

解决:先确认 setValue 末尾有 update();再在 setValue 里 qDebug 打印当前值,确认没走进「值没变就 return」分支;最后检查 Q_PROPERTY 声明是否完整,READ 和 WRITE 两个方法名拼写一致。三处排查完,更新问题基本能定位。

提示:排坑时开着 Qt 调试输出,很多自绘问题在 debug 版会打印 QPainter 警告,这些警告往往直接指向问题根因。

6. 把电池控件嵌进状态栏:尺寸策略、高DPI适配与一次性能实测

自绘控件最终要放回真实界面。我自己的常态场景是把电池放在状态栏右侧,偶尔也塞进标题栏。这两类场景对控件的要求完全不同:状态栏高度固定,不允许控件自行撑大;标题栏则要求电池跟随窗口缩放。

6.1 状态栏尺寸策略:高度跟随而非写死

状态栏高度通常在 30px 左右,直接 setFixedSize(100, 30) 在大多数系统下能用,但状态栏高度并非完全一致,写死高度会在某些主题下溢出 1~2px。稳妥做法是宽度固定、高度留弹性:

battery->setFixedWidth(100); battery->setMinimumHeight(statusBar()->height() - 6); battery->setMaximumHeight(statusBar()->height() - 2);

宽度固定 100px 保证电池长宽比不变,高度跟随状态栏实际高度收缩 2~6px。电池既不会顶到状态栏上下边界,也不破坏整体视觉比例。高度变化时 paintEvent 里的坐标函数基于 height() 实时计算,电池外形自动适配,无需额外代码。

6.2 高DPI适配:别手动乘 devicePixelRatio

高 DPI 下有个隐藏很深的坑:不少开发者以为 QPainter 不会自动处理缩放,于是手动乘 devicePixelRatio 再绘制。结果在 150% 缩放下电池变成原来一倍半大,所有坐标错位。

真相是——Qt6 启动时检测到系统缩放比例后,QWidget 的 width() 返回的已经是逻辑像素值,QPainter 在 paintEvent 内默认把逻辑坐标映射到设备像素。代码里不需要乘任何东西,也不需要调用 painter.scale()。只要满足两点:坐标全部基于控件的 width()/height() 计算,绘制时开启抗锯齿。本章之前的代码全部满足,同样的代码放到 4K 屏下,电池依然清晰且比例正确。

最后做一次性能实测。我用 QElapsedTimer 测过单个电池控件的 paintEvent 耗时:绘制外框、内胆、填充、文字和刻度约 0.15ms;加上呼吸动画的 alpha 计算约 0.2ms。状态栏同时放 10 个电池控件总耗时约 2ms,完全不用做额外缓冲优化。如果仪表盘要同时画 50 个电池,建议把所有电池的呼吸定时器合并成一个公共定时器,统一驱动 alpha 相位,这样能把重绘次数从 50 次降到 1 次。

把电池控件接到大项目时,建议把坐标规划、绘制、动画三层拆成独立文件,避免一个类越写越大。我后来接自绘控件项目都固定走这套流程:先独立窗口调好静态绘制,再接进状态栏验证尺寸,最后才加动画和充电逻辑。每次改动都强制走一遍「改代码 → 编译 → 看效果」的最小循环,省掉了大量反复调整尺寸和颜色的时间。这套带完整源码的电池控件方案打包好了,下载后直接编译就能跑,剩下的微调交给你自己的配色偏好。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询