ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Qt自定义控件:用paintEvent绘制电池进度条全流程

Qt自定义控件:用paintEvent绘制电池进度条全流程 简介一套基于 Qt 的电池进度条自定义控件源码通过重写 paintEvent 并结合 QPainter 完成绘制适合学习 Qt 图形界面编程、自定义控件与 QPropertyAnimation 动画机制的初学者。项目自带完整的 Qt Widget 工程文件共8个文件包含3个 C 源文件、2个头文件以及 .pro 工程文件和 .ui 界面文件整体压缩包仅7KB结构简洁便于快速导入 Qt Creator 阅读运行。已有498人学习下载。资源不仅展示了电池外壳、电量填充与颜色分级警示如低电量红色、高电量绿色的绘制思路还提供了动画驱动电量变化的实现示例读者可从中掌握 paintEvent 触发机制、绘图性能优化以及 UI 布局集成方法适合作为 Qt 入门阶段的练习参考。 电池进度条这个控件我在好几个项目里都用过。最早是给一个嵌入式设备做电量显示后来又在桌面软件的关于窗口里加了它。老实说第一次拿到这个需求我的第一反应是用QProgressBar改改样式算了但实际做下来发现手绘一个电池进度条并用paintEvent实现其实也就几十行代码的量而且可控性完全不在一个档次。这篇文章就把我实际写电池进度条的过程完整拆开讲一遍。它是什么一个继承自QWidget的自绘控件通过重写paintEvent逐像素画出电池外壳、正极帽、内部电量和百分比文字。它能做什么显示0~100%的电量、充电状态闪烁、低电量变色甚至可以复用同一套代码改出音量条、温度计这类同类控件。适合谁看刚接触Qt自定义绘制的朋友或者项目里需要把状态显示做得更灵活、不想被QProgressBar样式表折腾的开发。1. 先想明白为什么非要用paintEvent去画一个电池1.1 QProgressBar的样式调整空间有限QProgressBar不是不能做电池效果我见过有人用样式表把进度条背景改成圆角、把chunk改成绿色远看确实像个电池条但一旦遇到要加个正极金属头、电量文字要嵌在电池内部、低电量要用红色闪烁这几个需求样式表的写法就会变得非常别扭。样式表能控制的只有QProgressBar本身的背景、边框、chunk区域你没法在进度条右侧额外画一个凸起的小耳朵也不方便只针对圆角路径内部做裁剪。更麻烦的是QProgressBar的chunk只有水平或垂直两种模式想做电池那种“左右留出内边距、正极帽紧贴外壳”的视觉结构你得不断用margin和border-radius去凑最后还是一堆魔法值换个尺寸就崩。相比之下paintEvent给的是绝对控制权。你定义坐标定义绘制顺序定义颜色Qt只负责把QPainter画出来的东西渲染到屏幕上。这个自由度的差距就是自绘和样式表之间的本质区别。1.2 电池进度条的真实应用场景有人可能会问做项目图省事直接用背景图替换不是更快背景图方案确实快但遇到动态状态就很尴尬充电闪烁要切不同图片、不同电量等级要换颜色、文字里的百分比还得单独盖一层Label上去对齐一个控件拆成三四个部件维护反而更累。自绘控件则天然适合这种动态场景。我实际用到电池进度条的场合有这么几类设备管理面板显示远程传感器节点的剩余电量数值是通过串口或网络实时上报的。嵌入式设备的上位机模拟器要在PC端还原设备屏幕上的电池图标要求像素级一致。教学Demo演示QPainter的路径绘制、渐变色、抗锯齿这些能力电池刚好是个视觉反馈明显又不复杂的载体。个人工具软件的状态栏比如显示笔记本电量需要支持Windows下通过系统接口读取真实电量再喂给控件。这些场景有个共同点显示状态是动态变化的而且对视觉效果有具体预期。用paintEvent把这些状态一次画出来比维护一堆素材图要优雅得多。2. 动手前的设计电池进度条的结构和坐标计算2.1 控件结构拆解外壳、正极帽、内部填充与文字画电池之前先在脑子里把控件拆成四层。第一层是正极帽就是电池右侧那个凸起的小金属头没有它别人看不出这是个电池更像一个进度条。第二层是电池外壳一个圆角矩形通常用深灰色描边、浅灰或白色填充。第三层是电量填充区域位于外壳内部四周留出内边距根据百分比决定填充宽度。第四层是文字层百分比数字居中显示颜色要与底图有足够对比度。这四个层级不是随便定的。外壳和正极帽是静态的只有电量填充和文字是动态的。把静态部分和动态部分分开绘制代码就清晰了以后改颜色、改尺寸也不容易互相影响。我习惯这样拆正极帽在最底层画接着画外壳背景然后画电量填充最后画文字。这样即使填充颜色和外壳背景重叠也不会盖到不该盖的地方。2.2 关键尺寸参数怎么定直接写死坐标是最省事的但控件一旦resize就露馅。我建议把所有尺寸都相对于控件宽高推导只保留一个最小尺寸约束。部件推荐参数说明控件最小尺寸120x60太小了外壳和正极帽会挤在一起文字也放不下正极帽宽10高为控件高度的0.3右侧留出12px模拟电池头部凸起高度约为外壳高度的三分之一外壳外框(3, 3, w-18, h-6)左侧、上下各留3px边距右侧留出12px给正极帽外壳圆角6px圆角太大电池显得圆润太小不够协调内边距4px电量填充区和外壳边框之间的间距电量填充区域外壳内边距内的矩形宽度 可填充最大宽度 * 电量百分比外层控件尺寸假设是120x60那么外壳矩形是(3, 3, 102, 54)正极帽是(108, 21, 10, 18)正好在右侧居中。注意正极帽左侧和外壳右侧要重叠1~2个像素否则缩放后会出现一条窄缝视觉上非常影响整体感。2.3 绘制顺序决定最终效果绘制顺序我列个清单后面代码也是按这个顺序来的画正极帽直接用圆角矩形路径填充不需要描边。画外壳底色填充浅灰色圆角矩形再描边深灰色。画电量填充先计算填充宽度再用路径裁剪圆角颜色根据电量等级变化。画文字用QFont设置字号居中对齐绘制在电量区域上方。为什么先画正极帽因为它在外壳右侧即使外壳描边和它重叠也可以先把重叠区域覆盖掉。这个顺序在缩放时最稳定。3. 核心代码实现paintEvent一套带走3.1 基础框架与抗锯齿配置类定义不算复杂继承QWidget重写paintEvent对外提供setPower和setCharging两个接口。关键点在于setPower内部必须调用update()否则界面上不会刷新。#include QWidget #include QPainter #include QPainterPath #include QTimer class BatteryWidget : public QWidget { Q_OBJECT public: explicit BatteryWidget(QWidget *parent nullptr) : QWidget(parent) , m_power(66) , m_charging(false) , m_chargeDirection(true) { setMinimumSize(120, 60); } int power() const { return m_power; } void setPower(int value) { m_power qBound(0, value, 100); update(); } bool charging() const { return m_charging; } void setCharging(bool charging) { m_charging charging; if (m_charging) startChargeAnimation(); else stopChargeAnimation(); update(); } // ... 其余代码见下文 };抗锯齿是必须开的。电池外壳是圆角矩形没有抗锯齿的话圆角边缘全是锯齿尤其在高分屏上非常明显。void paintEvent(QPaintEvent *) override { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true);3.2 绘制电池外壳与正极帽先画正极帽。这是一个圆角小矩形我用QPainterPath而不是直接drawRoundedRect是因为后续要做路径填充和裁剪路径方式更通用而且能完美控制描边不溢出。const int w width(); const int h height(); QRectF headRect(w - 12, h * 0.35, 10, h * 0.3); QPainterPath headPath; headPath.addRoundedRect(headRect, 2, 2); painter.fillPath(headPath, QColor(#555555));正极帽的高度我取的是控件高度的0.3倍然后垂直居中。这个比例在120x60的尺寸下是18px视觉刚好。如果比例取太大正极帽就会像一个多余的尾巴。接着是外壳。外壳的矩形和正极帽有1px重叠这样两个部件之间不会出现白线缝隙。我用了一个浅灰色填充再画2px宽的深色边框。QRectF bodyRect(3, 3, w - 18, h - 6); QPainterPath bodyPath; bodyPath.addRoundedRect(bodyRect, 6, 6); painter.fillPath(bodyPath, QColor(#f5f5f5)); QPen borderPen(QColor(#555555), 2); painter.setPen(borderPen); painter.drawPath(bodyPath);这里有个经验边框宽度2px时路径的绘制会以路径为中心线向内外各画1px所以外壳的填充色会稍微盖住边框内侧。如果你先用fillPath再用drawPath顺序正确的话填充色是在边框内部不会被覆盖。这个顺序不要反过来。3.3 电量填充与颜色渐变电量填充是动态部分。先根据电量百分比算出填充宽度再构造一个圆角矩形路径填充。这里要注意填充区域的圆角半径要比外壳小否则溢出的圆角会碰到外壳边框。QRectF innerRect bodyRect.adjusted(4, 4, -4, -4); qreal fillWidth innerRect.width() * m_power / 100.0; QRectF fillRect(innerRect.left(), innerRect.top(), fillWidth, innerRect.height()); QColor fillColor; if (m_power 20) fillColor QColor(#e53935); else if (m_power 50) fillColor QColor(#fb8c00); else fillColor QColor(#43a047); if (m_charging m_chargeDirection) fillColor fillColor.lighter(120); if (fillWidth 1) { QPainterPath fillPath; fillPath.addRoundedRect(fillRect, 4, 4); painter.fillPath(fillPath, fillColor); }电量等级的配色我实测下来20%以下用红色、20%~50%用橙色、50%以上用绿色是最直观的用户扫一眼就知道状态。也有人喜欢50%以下用黄色这个看产品需求。还有一个细节当电量是0%时填充宽度可能只有0.x像素这种小于1像素的矩形会产生奇怪的渲染效果所以加了个fillWidth 1的判断低于阈值干脆不画。3.4 电量百分比文字绘制与状态切换文字层很关键因为电池本身是有内边距的文字如果直接用painter.drawText(rect(), Qt::AlignCenter, ...)会覆盖到正极帽和外壳的边框附近看起来是浮在电池上的。正确做法是把文字约束在外壳矩形区域内而不是整个控件区域。painter.setPen(QColor(#333333)); QFont font painter.font(); font.setPixelSize(h * 0.4); font.setBold(true); painter.setFont(font); painter.drawText(bodyRect, Qt::AlignCenter, QString::number(m_power) %);bodyRect就是之前定义的外壳矩形我直接把字符串传进去Qt会自动处理水平和垂直居中。字体大小用控件高度的40%在120x60的控件下大约是24px看起来比较协调。充电状态我用一个QTimer交替切换m_chargeDirection每500ms翻转一次让填充颜色在亮和暗之间切换。这就是充电时的呼吸效果。QTimer不用在构造时创建只有开启充电时才创建可以有效避免无谓的定时器开销。private: void startChargeAnimation() { m_chargeDirection true; if (!m_timer) { m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, [this]() { m_chargeDirection !m_chargeDirection; update(); }); } m_timer-start(500); } void stopChargeAnimation() { if (m_timer) m_timer-stop(); }4. 细节优化与踩坑记录4.1 边框宽度对坐标的影响刚开始画电池外壳时我直接用了painter.drawRoundedRect(bodyRect, 6, 6)没有先用路径填充。结果在高DPI屏幕上外壳边框看起来一边粗一边细内部填充还会漏出白边。原因是QPen设成2px后Qt绘制矩形边框时以矩形边界为中心向内外各扩1px而填充矩形又是原坐标两者叠加后就会出现偏移。解决办法就是我在3.2里说的先用fillPath填充底色再用drawPath描边两次绘制都基于同一个QPainterPath就不会错位。4.2 高DPI缩放适配如果你的应用启用了Qt::AA_EnableHighDpiScalingQt6默认开启那么在2x缩放的屏幕上QPainter的坐标系统已经自动乘以了设备像素比你不需要手动处理devicePixelRatio()。但如果你用的绘图坐标是写死的像素值缩放后整体效果会偏小。最稳妥的做法是控件尺寸跟随布局伸缩绘制时所有尺寸都按width()/height()比例推导不要写死任何绝对数值。这也是我前面反复强调相对于控件宽高推导的原因本质上就是为了在高DPI下不糊不缩。4.3 充电动画与低电量闪烁的复用充电动画和低电量闪烁很多人会用两个定时器分别控制。其实不需要一个定时器足够。我这里的做法是充电时用m_chargeDirection控制颜色明暗低电量时也可以复用这个flag只在paintEvent里根据当前状态选择是否采用交替颜色。如果你还要处理低电量闪烁只需在paintEvent里把红色填充改成半透明和全亮交替不需要额外定时器。定时器只是一个驱动update信号的节拍器真正的视觉逻辑全部集中在绘制函数里这样代码逻辑最好维护。4.4 常见问题速查表现象原因解决方式正极帽和外壳之间有白色缝隙两个矩形没有重叠或缩放时浮点误差让正极帽左边缘与外壳右边缘重叠1~2px圆角锯齿明显没有开启抗锯齿painter.setRenderHint(QPainter::Antialiasing, true);高DPI下控件整体偏小绘制坐标写死了像素值用width()/height()比例推导所有尺寸频繁update()时窗口闪烁Qt默认会先填充背景再执行paintEvent在构造函数加setAttribute(Qt::WA_OpaquePaintEvent);电量变了但界面不刷新忘记调用update()setPower结尾调用update()电量0%时出现残留色块填充宽度小于1px产生异常渲染加fillWidth 1判断文字压到外壳边框drawText用了整个控件矩形改用外壳矩形bodyRect作为文本区域4.5 一个容易被忽略的小坑最小尺寸约束setMinimumSize(120, 60)不是随便写的。如果你不设最小尺寸控件被压缩到60x30时正极帽和外壳比例会完全失调文字也会溢出。给布局用的控件建议再配合setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding)让它在布局里均匀伸缩。如果需要在更小尺寸下使用单独调整headRect宽度、bodyRect右侧留白这些常量的比例即可核心绘制逻辑不用动。写在最后电池进度条这类自定义控件真正值钱的不是那几行绘制代码而是坐标体系和状态分离的思路。把控件拆成静态外壳和动态填充两个层级所有绘制参数从这两个层级推导后期调尺寸、改颜色、加动画都很好改。我后来在项目里用同一套结构画过音量条、温度计和信号强度条只需要把填充方向从水平改成垂直、换个渐变颜色其他逻辑直接复用。最后再分享一个小技巧给这个控件加一个setValue(int)的别名内部转给setPower这样将来接业务数据时不管是电池电量、内存占用还是网络信号代码读起来都非常自然。把状态接口设计成和业务字段一一对应比画得好看更重要因为控件最终是要为真实数据服务的。本文还有配套的精品资源点击获取
返回列表