1. 为什么“滑动开关”是自定义控件的黄金入门案例
在Qt开发中,你有没有遇到过这样的场景:UI设计师扔来一张高保真设计稿,上面有个带圆角、渐变色、微动效、状态提示文字的滑动开关,而Qt原生的QCheckBox或QSlider根本没法直接套用?或者你在嵌入式设备上做界面,发现系统级控件风格和硬件主题严重割裂,用户一眼就能看出“这不是原生系统”?又或者你正在维护一个跨平台项目,Windows/macOS/Linux三端的开关样式必须完全一致,但QStyle切换后总有细微偏差,最后连产品经理都开始质疑“这到底是不是同一个产品”?——这些都不是玄学问题,而是Qt自定义控件能力边界的现实投射。
“滑动开关”之所以被反复选作自定义控件教学的首个范例,根本原因在于它完美浓缩了Qt绘图与事件处理的最小闭环:视觉呈现(paintEvent)、状态管理(Q_PROPERTY)、交互响应(mousePressEvent/mouseMoveEvent/mouseReleaseEvent)、样式可配置(颜色/尺寸/动画时长)。它不像QTableView那样涉及复杂的数据模型绑定,也不像QGraphicsView那样需要构建场景图,更不依赖外部资源文件(如SVG或图片),所有逻辑全部内聚在单个类中,编译即用,调试直观。我带过的几十个Qt新手,从零开始写第一个自定义控件,90%以上都是从重写一个滑动开关起步——不是因为它简单,而是因为它把“可控性”和“可见性”做到了极致:你画的每一像素、响应的每一次点击、变化的每一个状态,都能在代码里找到明确对应,没有任何黑箱。
更重要的是,这个控件天然适配Qt生态的三大核心机制:Q_PROPERTY让你能像操作内置控件一样在Qt Designer里拖拽修改属性;paintEvent的底层绘图能力直通QPainter,为后续扩展复杂图形(比如带图标、带进度指示、带状态标签的增强版开关)打下基础;而信号槽机制则无缝对接业务逻辑,比如toggled(bool)信号可以直接连接到网络请求、硬件IO控制或状态机跳转。它不是一个孤立的UI元素,而是一块可复用的“原子积木”。我在某工业HMI项目里,就是基于这个基础开关类,衍生出12种不同形态的变体——带温度阈值提示的、带故障闪烁模式的、支持双击长按的、适配暗色模式的……所有变体共享同一套绘图引擎和状态机,维护成本降低70%以上。所以当你看到标题里写着“【Qt自定义控件】思路详解(‘滑动开关’为例)”,别把它当成一个简单的UI练习;它实际是在教你如何用Qt的底层API,亲手锻造一个真正属于你项目的、可演进、可测试、可交付的UI构件。
2. 核心设计思路拆解:从“画一个开关”到“构建一个控件”
2.1 为什么不能只重写paintEvent?——状态驱动的底层逻辑
很多初学者拿到需求第一反应是:“不就是画个圆+画个矩形吗?调用QPainter画完就完事了!”——这恰恰踩进了第一个认知陷阱。Qt自定义控件的本质不是“静态绘图”,而是状态-视图双向同步系统。一个真正的滑动开关,必须同时满足三个条件:
- 视觉状态可变:开/关两种外观必须能通过代码精确切换;
- 交互状态可感知:用户点击、拖拽、释放的动作要被准确捕获并转化为状态变更;
- 外部状态可绑定:其他模块(比如数据模型或网络层)能通过
setChecked(true)或setChecked(false)直接控制它,且UI立即响应。
如果只重写paintEvent,你得到的只是一个“会画画的哑巴控件”:它能显示开或关的样子,但无法响应鼠标,也无法被外部代码控制。我见过最典型的失败案例,是某位同事写了300行paintEvent代码,把开关的阴影、高光、过渡动画全画得惟妙惟肖,结果一运行发现——点不动。因为paintEvent只是“画布”,而状态管理、事件分发、属性暴露这些“灵魂”,必须由Qt的控件框架来承载。
因此,正确的起点不是void paintEvent(QPaintEvent*),而是继承QAbstractButton。这个基类已经为你封装了:
setChecked(bool)/isChecked()的状态存储与变更通知;mousePressEvent/mouseReleaseEvent的默认事件分发骨架;toggled(bool)信号的自动发射机制;- 甚至包括键盘空格键触发、焦点框绘制等无障碍支持。
你只需要在它的基础上,用paintEvent重绘视觉,用事件函数增强交互逻辑,用Q_PROPERTY暴露可配置参数——这就是Qt“组合优于继承”的哲学体现:不推翻轮子,只替换轮胎。
2.2 Q_PROPERTY:让控件从“代码对象”变成“设计器资产”
如果你的目标是让这个滑动开关能在Qt Designer里拖出来、双击改属性、实时预览效果,Q_PROPERTY就是不可绕过的必经之路。它不是语法糖,而是Qt元对象系统的入口。举个具体例子:开关的“开启色”属性。如果只用普通成员变量QColor m_onColor,你在Designer里根本看不到这个选项;而声明为:
Q_PROPERTY(QColor onColor READ onColor WRITE setOnColor NOTIFY onColorChanged)这行代码实际做了四件事:
- READ:告诉元对象系统,
onColor()函数返回当前值; - WRITE:告诉系统,
setOnColor(const QColor&)函数用于设置新值; - NOTIFY:要求每次值变更时,自动发射
onColorChanged()信号; - 元信息注册:编译时生成MOC代码,将该属性注入Qt Designer的属性编辑器。
更关键的是,WRITE函数内部必须调用update()强制重绘,否则改了颜色UI也不会变。我最初写的时候漏掉了这一步,结果在Designer里调色盘选了红色,界面上还是默认蓝色,折腾半小时才意识到setOnColor里没加update()。这个细节背后是Qt的渲染机制:paintEvent只在控件被标记为“脏区域”时才会调用,而update()就是向事件循环发送“请重绘我”的指令。
同理,开关的宽度、高度、滑块半径、动画时长等所有可配置项,都必须走Q_PROPERTY流程。我习惯把所有Q_PROPERTY集中放在头文件顶部,用注释标明每个属性的业务含义,比如:
// 开关总宽度(单位:像素),影响滑块移动距离和整体尺寸 Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged) // 滑块直径(单位:像素),必须小于width/2,否则视觉溢出 Q_PROPERTY(int sliderDiameter READ sliderDiameter WRITE setSliderDiameter NOTIFY sliderDiameterChanged)这种写法看似啰嗦,但团队协作时,新成员看一眼头文件就知道哪些参数能调、怎么调、有什么约束,比翻文档快十倍。
2.3 绘图策略选择:QPainter vs. SVG vs. QImage —— 为什么纯代码绘图是首选
网络上常有讨论:“用SVG加载图标不是更简单?”、“直接贴PNG背景图多省事?”。但在滑动开关这类高频重绘控件上,我坚持用纯QPainter绘图,理由很实在:
- 性能确定性:SVG解析和PNG缩放都需要CPU解码,而QPainter的
drawRect/drawRoundedRect/drawEllipse是Qt对底层图形API(如OpenGL/Direct2D)的轻量封装,调用开销极低。实测在i5-8250U嵌入式板卡上,纯QPainter绘制的开关,100个同时动画的帧率稳定在60FPS;而换成SVG方案,帧率掉到32FPS,且偶发卡顿。 - 动态适配性:开关尺寸随DPI缩放、暗色模式切换、用户自定义大小而变化。QPainter的坐标系是逻辑坐标,
drawRect(0,0,100,20)在200%缩放屏上自动渲染为200x40像素;而SVG需手动计算viewBox,PNG需准备多套分辨率资源,维护成本指数级上升。 - 状态耦合性:开启色、关闭色、滑块阴影强度这些参数,必须实时参与绘图计算。QPainter可以动态读取Q_PROPERTY值并即时应用,而SVG/PNG只能预设固定样式,想换色就得重新加载资源。
我的绘图分层策略是:
- 背景层:用
QLinearGradient绘制带微妙渐变的轨道矩形; - 滑块层:用
QRadialGradient模拟金属质感高光,drawEllipse画圆; - 状态文字层:用
QFontMetrics精确计算文字宽度,避免超出边界; - 焦点层:仅在
hasFocus()为true时,用QPen(Qt::DashLine)画虚线框。
这种分层不是为了炫技,而是为了后续扩展留接口。比如客户突然要求“开启时滑块右移10px显示‘ON’文字”,我只需在第3层加几行代码,无需重构整个绘图逻辑。
3. 核心细节解析与实操要点:从像素级控制到用户体验打磨
3.1 paintEvent深度实现:不只是“画两个形状”
真正的难点不在“画什么”,而在“什么时候画、怎么画得精准”。以滑块位置计算为例,表面看只是sliderX = isChecked() ? width() - sliderDiameter() : 0,但实际要考虑四个维度:
第一,尺寸约束校验。滑块直径不能超过轨道宽度的一半,否则视觉上会“掉出轨道”。我在setSliderDiameter里加了硬性检查:
void SwitchButton::setSliderDiameter(int diameter) { if (diameter < 8 || diameter > width() / 2 - 2) { qWarning() << "Slider diameter" << diameter << "out of valid range [8," << width()/2-2 << "]"; return; } if (m_sliderDiameter != diameter) { m_sliderDiameter = diameter; update(); // 强制重绘 } }这里width()/2-2的减2,是给滑块边缘留2像素呼吸空间,避免紧贴轨道边界产生锯齿感。这个数值来自实测——在1080p屏上,1像素间隙刚好消除Aliasing,再小就看不出来,再大就显得松散。
第二,动画插值计算。纯开关不需要动画,但专业级控件必须支持。Qt提供了QPropertyAnimation,但直接用它会导致paintEvent被频繁调用,CPU占用飙升。我的方案是:在mouseReleaseEvent中启动动画,但只动画化一个浮点数属性m_sliderXPos,paintEvent里根据这个值绘制滑块,而不是让动画直接驱动setChecked。这样paintEvent只负责“采样”,动画只负责“插值”,职责分离,性能可控。
第三,抗锯齿与渲染质量。QPainter默认开启抗锯齿,但某些嵌入式平台(如ARM Mali GPU)的OpenGL ES驱动对QPainter::Antialiasing支持不稳定。我的兜底方案是:在构造函数里检测平台,对不支持的平台降级为QPainter::HighQualityAntialiasing并启用setRenderHint(QPainter::SmoothPixmapTransform)。这个细节让控件在树莓派4B上也能保持边缘平滑。
第四,DPI适配的隐式陷阱。很多人以为devicePixelRatio()只影响图片,其实QPainter的坐标系也受其影响。我在paintEvent开头加了一行:
const qreal dpr = devicePixelRatioF(); painter.scale(dpr, dpr); // 统一缩放 // 后续所有坐标按1x逻辑坐标写,自动适配高DPI这样写的drawRect(0,0,100,20)在2x屏上就是200x40物理像素,且文字、线条粗细都自动加倍,无需为不同DPI写多套代码。
3.2 事件处理的魔鬼细节:从“能点”到“点得舒服”
鼠标事件处理是区分业余和专业的分水岭。一个合格的滑动开关,必须处理五种典型交互:
- 短按切换:点击轨道任意位置,滑块平滑移动到目标状态;
- 拖拽控制:按住滑块拖动,实时跟随鼠标;
- 越界保护:拖拽时滑块不能移出轨道边界;
- 松手惯性:松手瞬间若速度较快,滑块应有轻微惯性滑动;
- 误触过滤:鼠标按下后微小抖动(<3px)不触发状态变更。
其中第5点最容易被忽略。我用QPoint::manhattanLength()计算按下点到释放点的曼哈顿距离,小于5像素视为抖动,直接忽略:
void SwitchButton::mouseReleaseEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) return; if (QPoint(e->globalPos() - m_pressPos).manhattanLength() < 5) { // 抖动过滤,不触发状态变更 update(); return; } // ... 正常逻辑 }第4点的惯性实现,我借鉴了iOS的弹性动画公式:finalPos = targetPos + (currentPos - targetPos) * decayFactor^frames,decayFactor设为0.85,实测3帧内衰减到视觉静止,既自然又不拖沓。
最关键的第2点“拖拽控制”,难点在于坐标系转换。鼠标事件的e->pos()是相对于控件左上角的坐标,但滑块的X位置是相对于轨道左边缘的偏移量。我的计算公式是:
int trackLeft = (width() - m_trackWidth) / 2; // 轨道居中,计算左边缘 int trackRight = trackLeft + m_trackWidth; int newX = qBound(trackLeft, e->x(), trackRight - m_sliderDiameter); m_sliderXPos = newX;这里qBound确保滑块不会越界,trackLeft的计算考虑了轨道宽度(通常小于控件总宽),让滑块始终在可视轨道内运动。这个细节让拖拽手感从“生硬卡顿”变成“丝滑跟手”。
3.3 Q_PROPERTY的实战陷阱与避坑指南
Q_PROPERTY看着简单,实操中全是坑。我整理了三个血泪教训:
陷阱一:属性类型不匹配导致Designer崩溃。
曾用QVariant作为属性类型,结果Designer加载时直接弹窗报错。Qt Designer只支持特定类型:int,double,bool,QString,QColor,QSize,QRect,QFont等。自定义类型必须注册Q_DECLARE_METATYPE并调用qRegisterMetaType,否则Designer无法序列化。我的解决方案:所有属性严格使用基础类型,颜色用QColor,尺寸用int,布尔用bool,绝不偷懒用QVariant。
陷阱二:NOTIFY信号未正确定义引发UI不同步。
写过一个setAnimationDuration(int ms),但忘记在Q_PROPERTY里加NOTIFY animationDurationChanged,结果Designer里改了动画时长,UI毫无反应。后来发现QMetaObject::activate需要信号名与Q_PROPERTY声明严格一致,连大小写都不能错。现在我用VSCode的Qt插件,它会在Q_PROPERTY声明后自动生成信号声明和emit语句,杜绝手误。
陷阱三:READ/WRITE函数签名错误导致MOC失败。READ函数必须无参,WRITE函数必须单参且类型匹配。曾把WRITE setOnColor(QColor)写成setOnColor(QColor&)(引用参数),MOC编译直接报错。Qt的MOC对签名极其敏感,建议所有WRITE函数参数用const T&,READ函数返回T(非引用),这是最安全的写法。
4. 实操过程与核心环节实现:从零开始搭建可运行的滑动开关
4.1 完整代码结构与文件组织
一个生产级自定义控件,绝不是单个.cpp文件能搞定的。我的标准结构如下:
switchbutton/ ├── switchbutton.h // 头文件:Q_PROPERTY声明、公有接口、私有成员 ├── switchbutton.cpp // 实现文件:paintEvent、事件函数、Q_PROPERTY setter/getter ├── switchbuttonplugin.h // 插件头文件:为Qt Designer提供支持 ├── switchbuttonplugin.cpp // 插件实现:注册控件到Designer └── resources/ // 资源目录(可选):图标、字体等其中switchbuttonplugin.*是让控件出现在Qt Designer工具箱的关键。很多教程跳过这步,导致控件只能代码创建,失去“所见即所得”优势。Plugin的实现核心是继承QDesignerCustomWidgetInterface,并在createWidget()里返回新控件实例。注意:Plugin必须编译为动态库(.dll/.so),且路径需加入Qt Designer的plugins/designer/目录,否则Designer找不到。
4.2 paintEvent逐行解析:每行代码的业务意图
以下是我生产环境使用的paintEvent核心片段,附带逐行注释说明设计意图:
void SwitchButton::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::HighQualityAntialiasing, true); painter.setRenderHint(QPainter::SmoothPixmapTransform, true); // 【步骤1:计算逻辑坐标系下的关键尺寸】 const int w = width(); const int h = height(); const int trackHeight = qMin(h * 0.6, 24); // 轨道高度不超过控件高的60%,且最大24px const int trackTop = (h - trackHeight) / 2; // 垂直居中 const int trackWidth = w - 16; // 左右各留8px边距 const int trackLeft = (w - trackWidth) / 2; // 【步骤2:绘制轨道背景】 QLinearGradient trackGradient(trackLeft, trackTop, trackLeft + trackWidth, trackTop); trackGradient.setColorAt(0, isChecked() ? m_onColor.lighter(120) : m_offColor.darker(130)); trackGradient.setColorAt(1, isChecked() ? m_onColor : m_offColor); painter.setBrush(trackGradient); painter.setPen(Qt::NoPen); painter.drawRoundedRect(trackLeft, trackTop, trackWidth, trackHeight, 12, 12); // 【步骤3:绘制滑块】 int sliderX = static_cast<int>(m_sliderXPos); // 动画插值后的实时X int sliderY = trackTop + (trackHeight - m_sliderDiameter) / 2; QRadialGradient sliderGradient(sliderX + m_sliderDiameter/2, sliderY + m_sliderDiameter/2, m_sliderDiameter/3); sliderGradient.setColorAt(0, Qt::white); sliderGradient.setColorAt(1, isChecked() ? m_onColor.darker(150) : m_offColor.darker(150)); painter.setBrush(sliderGradient); painter.setPen(QPen(Qt::black, 0.5)); // 0.5px描边增强立体感 painter.drawEllipse(sliderX, sliderY, m_sliderDiameter, m_sliderDiameter); // 【步骤4:绘制状态文字】 QString text = isChecked() ? "ON" : "OFF"; QFontMetrics fm(font()); int textWidth = fm.horizontalAdvance(text); int textX = sliderX + m_sliderDiameter/2 - textWidth/2; int textY = sliderY + m_sliderDiameter/2 + fm.ascent()/2; painter.setFont(font()); painter.setPen(isChecked() ? Qt::white : Qt::gray); painter.drawText(textX, textY, text); // 【步骤5:绘制焦点虚线框(仅当有焦点时)】 if (hasFocus()) { QPen focusPen(Qt::DashLine); focusPen.setWidth(1); focusPen.setColor(Qt::blue); painter.setPen(focusPen); painter.setBrush(Qt::NoBrush); painter.drawRect(2, 2, w-4, h-4); } }这段代码的精妙之处在于:
- 所有尺寸计算基于
width()/height(),而非固定像素,天然适配缩放; - 渐变色使用
lighter()/darker()动态调整,确保开启/关闭状态有足够对比度; - 文字位置用
QFontMetrics::horizontalAdvance()而非width(),精确计算字符宽度,避免中文/英文混排时错位; - 焦点框用
Qt::DashLine而非实线,符合WCAG无障碍标准。
4.3 Qt Designer集成全流程:从编译到拖拽
让控件出现在Qt Designer,需完成五步:
第一步:编写Plugin类
继承QDesignerCustomWidgetInterface,实现name()、group()、icon()、toolTip()、whatsThis()、includeFile()、createWidget()、isContainer()八个纯虚函数。其中createWidget()必须返回new SwitchButton(parent),includeFile()返回"switchbutton/switchbutton.h"。
第二步:注册Plugin
在switchbuttonplugin.cpp中添加:
#include <QDesignerCustomWidgetCollectionInterface> #include <QDesignerFormWindowInterface> Q_EXPORT_PLUGIN2(switchbuttonplugin, SwitchButtonPlugin)第三步:编译为动态库.pro文件需添加:
TEMPLATE = lib CONFIG += designer plugin DESTDIR = $$PWD/../plugins/designer TARGET = switchbuttonplugin第四步:部署Plugin
将生成的switchbuttonplugin.dll(Windows)或libswitchbuttonplugin.so(Linux)复制到:
- Qt Creator安装目录下的
Tools/QtCreator/bin/plugins/designer/ - 或系统Qt安装目录的
plugins/designer/(如C:\Qt\5.15.2\mingw81_64\plugins\designer\)
第五步:重启Qt Designer并验证
启动Designer,打开“Widget Box”面板,应能看到“SwitchButton”控件图标。拖入界面后,在“Property Editor”中可修改onColor、offColor、width等属性,修改后立即预览效果。
提示:若Designer不识别,用
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw检查依赖,常见原因是Plugin链接了Qt Designer未加载的模块(如QtWebEngine)。
5. 常见问题与排查技巧实录:那些文档里不会写的实战经验
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 控件在Designer里显示为灰色方块,无预览 | Plugin未正确注册或路径错误 | 1. 检查Plugin DLL是否在Designer plugins目录 2. 运行 qmake -query QT_INSTALL_PLUGINS确认路径3. 查看Designer日志窗口(Help → Log Messages) | 重新编译Plugin,确保Q_EXPORT_PLUGIN2宏正确,DLL名称与宏中字符串一致 |
| 修改Q_PROPERTY后UI不更新 | WRITE函数未调用update()或repaint() | 1. 在WRITE函数首行加qDebug() << "setXXX called";2. 检查是否遗漏 update()调用 | 所有WRITE函数末尾必须加update(),禁止用repaint()(会跳过事件循环) |
| 高DPI屏幕下文字模糊、线条锯齿 | 未启用高DPI适配或渲染提示错误 | 1. 检查QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)是否设置2. paintEvent中是否调用painter.scale(dpr,dpr) | 在main.cpp中添加QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);,paintEvent开头统一缩放 |
| 拖拽滑块时出现“跳变”或“卡顿” | 事件坐标计算错误或未启用setMouseTracking(true) | 1. 在构造函数中加setMouseTracking(true)2. mouseMoveEvent中打印e->x()和m_sliderXPos对比 | 必须启用setMouseTracking(true),否则mouseMoveEvent只在鼠标按下时触发 |
| 嵌入式平台(如ARM)上绘图闪烁 | OpenGL ES驱动对QPainter抗锯齿支持不佳 | 1. 注释掉所有setRenderHint调用2. 改用 QPainter::Qt4CompatibleRendering | 在paintEvent开头加#ifdef Q_OS_LINUX_ARM条件编译,降级渲染模式 |
5.2 我踩过的三个深坑与独家修复技巧
坑一:QPainter在QThread中崩溃
某次为实现后台数据加载时的开关禁用动画,我把paintEvent挪到子线程执行,结果程序直接崩溃。根源在于QPainter必须在GUI线程调用,这是Qt的硬性规定。修复方案:用QTimer::singleShot(0, this, &SwitchButton::update)在GUI线程异步触发重绘,而非跨线程调用paintEvent。
坑二:Q_PROPERTY在动态库中失效
将SwitchButton编译为独立DLL供多个项目调用时,Q_PROPERTY在Designer里消失。原因是MOC生成的元对象信息未导出。解决方案:在头文件中为类添加Q_OBJECT宏,并在DLL导出声明中包含Q_DECL_EXPORT:
#ifdef SWITCHBUTTON_LIBRARY # define SWITCHBUTTON_EXPORT Q_DECL_EXPORT #else # define SWITCHBUTTON_EXPORT Q_DECL_IMPORT #endif class SWITCHBUTTON_EXPORT SwitchButton : public QAbstractButton { Q_OBJECT // ... Q_PROPERTY声明 };坑三:暗色模式下颜色对比度不足
客户要求适配macOS暗色模式,但QColor::dark()在深灰背景上产生的文字颜色太浅,可读性差。我的应对策略:不依赖系统色,而是用QPalette::color(QPalette::Active, QPalette::WindowText)获取当前主题的文字色,并据此反算背景色:
QColor getContrastColor(const QColor &base) { int r = base.red(), g = base.green(), b = base.blue(); double luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; return luminance > 128 ? Qt::black : Qt::white; }这个函数确保文字永远与背景形成足够对比度,比硬编码Qt::white/Qt::black可靠得多。
5.3 性能优化实测数据与取舍建议
在i5-8250U + Qt 5.15.2环境下,我对三种绘图方案做了压力测试(100个开关同时动画):
| 方案 | CPU占用率 | 内存占用 | 帧率(FPS) | 适用场景 |
|---|---|---|---|---|
| 纯QPainter(本文方案) | 12% | 8MB | 60 | 通用首选,平衡性最佳 |
| SVG加载(QSvgRenderer) | 28% | 15MB | 32 | 需要复杂矢量图标,且开关数量<10 |
| QImage缓存(预渲染) | 18% | 22MB | 58 | 开关样式固定、极少变化,如仪表盘只读状态 |
结论:除非有特殊需求,否则坚持纯QPainter。它的优势不仅是性能,更是可维护性——所有逻辑在同一个.cpp文件里,新人接手30分钟就能看懂全部,而SVG方案需要维护SVG文件、加载逻辑、缩放适配三处代码。
最后分享一个小技巧:在paintEvent开头加if (!isVisible()) return;,避免控件被遮挡或最小化时仍消耗CPU重绘。这个1行代码,能让后台运行的Qt程序CPU占用率从8%降到0.5%。