☰
Qt侧边栏渐入渐出动画组件:属性动画与布局避坑实战
2026/10/26 3:37:54 网站建设 项目流程

简介:SideWidget.zip 是一份面向 Qt 桌面界面开发者的侧边栏渐入渐出动画示例工程,适合希望用抽屉式交互提升界面流畅度与视觉表现的学习者参考。压缩包共 12 个文件,约 9KB,包含 4 个 cpp 源文件、3 个 h 头文件、3 个 ui 界面文件以及 pro 工程配置和 user 用户配置,分别承载动画逻辑、类声明、界面布局与编译环境设置,体量轻便,便于直接打开研读。工程围绕自定义侧边栏控件展开,借助 QPropertyAnimation 对位置、尺寸与透明度做过渡,并以滑出、滑入槽函数和信号槽连接响应用户操作,同时可通过 duration 与 easingCurve 调整动画节奏。已有 1242 人学习,读者可从中梳理控件封装、动画参数设置和交互触发方式,理解如何将侧边栏展开收起做得平滑自然,并将思路迁移到工具栏、设置面板等场景。

1. 侧边栏不是把宽度从 0 拉到 260 就完事:SideWidget.zip 拆包第一印象

第一次拿到 SideWidget.zip,我以为又是那种把 QWidget 宽度从 0 拉到 260、挂个 QPropertyAnimation 就交差的玩具。跑起来才发现不是:它的 Qt 侧边栏 渐入渐出 把宽度、透明度、缓动曲线三条线拆开控制,展开和收起还用了不同的时长配比,收起时透明度故意拖慢,避免露出半截空白面板再"啪"地消失。这东西解决的是桌面工具里最磨人的一个诉求——主界面要留足画布,功能入口又不能全塞进三级菜单。适合正在用 Qt Widgets 写后台工具、工业上位机、数据标注客户端的同学,尤其是被"侧边栏一拉就抖、一收就闪"折腾过的那批人。它不管业务逻辑,只把这一块的动画节奏和布局边界处理干净,拿过来改改 QSS 就能用。

2. 从压缩包到类结构:SideWidget 的三层职责怎么分

这类部件最常见的翻车方式是"一个 QWidget 全干了":既当容器、又改自己的宽度、又挂透明度特效。代码看着短,跑起来每帧都在重建离屏位图。SideWidget.zip 里的组织方式不复杂,真正值得读的是它把职责切成了三层,切完之后每个坑都有地方落脚。

2.1 容器层、面板层、动画层各自负责什么

拆开看,SideWidget 这个类本身是"壳"。它继承 QWidget,对外只暴露两个能力和一个属性:展开、收起、以及一个可被动画驱动的 sidebarWidth。壳的宽度决定了它在父布局里占多少位置,这一层不加任何效果,纯粹做尺寸。

真正的视觉部分是内部的 panel,也就是"肉"。按钮、列表、树控件、图标都挂在 panel 上,透明度效果也加在 panel 上。为什么不是加在壳上?因为壳的宽度每帧都在变,QGraphicsOpacityEffect 的离屏渲染缓冲依赖 widget 的 size,宽度一变缓冲就作废,等于每帧重新分配一次 pixmap。把这个成本从"变宽度的对象"上挪开,是这类动画能不能稳住 60 帧的分水岭。

动画层是"筋",只管插值,不碰布局。一个 QPropertyAnimation 驱动壳的自定义属性,另一个驱动 opacity,两者塞进 QParallelAnimationGroup 统一启停。下面这段是壳的头文件,属性声明是整块动画的地基:

// sidewidget.h #ifndef SIDEWIDGET_H #define SIDEWIDGET_H #include <QWidget> class QPropertyAnimation; class QParallelAnimationGroup; class QGraphicsOpacityEffect; class SideWidget : public QWidget { Q_OBJECT // 动画只改这个属性,真正的 setFixedWidth 落在 setter 里 Q_PROPERTY(int sidebarWidth READ sidebarWidth WRITE setSidebarWidth) public: explicit SideWidget(QWidget *parent = nullptr); int sidebarWidth() const { return m_sidebarWidth; } void setSidebarWidth(int w); void expand(); void collapse(); void toggle(); private: void buildUi(); void buildAnimation(); QWidget *m_panel = nullptr; QPropertyAnimation *m_widthAnim = nullptr; QPropertyAnimation *m_fadeAnim = nullptr; QParallelAnimationGroup *m_group = nullptr; QGraphicsOpacityEffect *m_opacity = nullptr; int m_sidebarWidth = 0; int m_expandedWidth = 260; // 展开后的目标宽度 bool m_expanded = false; }; #endif // SIDEWIDGET_H

逻辑说明:Q_PROPERTY 是 QPropertyAnimation 能找到 setter 的唯一依据,名字必须和new QPropertyAnimation(this, "sidebarWidth")里的字符串完全一致,大小写错了不会报编译错误,只会在运行时不生效——这条我踩过不止一次。

参数说明:m_expandedWidth 建议做成构造参数或 setter,不要写死在 cpp 里,因为同一个项目里主侧栏和次级抽屉的宽度往往不一样;m_sidebarWidth 是当前实时宽度,动画过程中它的值就是壳的实际宽度,任何时候读它都能拿到真值,比读 width() 可靠。

2.2 为什么用 QPropertyAnimation 而不是 QTimer 手写插值

很多人第一反应是开个 QTimer,每 16 毫秒算一次w = start + (end - start) * t,然后 setFixedWidth。写出来二十行,跑起来也像那么回事,但只要遇到三种情况就会露馅:一是窗口被拖动、系统卡了一下,定时器回调丢了几帧,动画就走成了变速运动;二是用户在动画中途点了别的按钮,你需要立刻中断并反向,手写版本得自己维护一个方向标记和起始值;三是你想换条缓动曲线,得自己实现 easing 函数。

QPropertyAnimation 背后是 QVariantAnimation,由 Qt 内部的统一定时器驱动,大致 60Hz 量级,掉帧时有时间补偿,有 start/stop/pause 状态机,有 finished 信号,有 QEasingCurve 的三十多种曲线。拿它和手写版做个对照:

维度QTimer 手写插值QPropertyAnimation
时间基准自己累加,掉帧后失准按 elapsed 时间插值,掉帧自动补偿
中断与反向需自己维护方向、起始值stop 后重设 startValue 即可
缓动曲线手写,通常只有线性QEasingCurve 内置几十种
结束通知自己判断finished 信号
调试无状态可看state() 可查 Running/Stopped

唯一的代价是它依赖属性系统,得写 Q_PROPERTY 和 setter。但这一步顺便把"宽度怎么设"收敛到一个函数里,后面排查布局问题反而更省事。

2.3 宽度与透明度的组合:QParallelAnimationGroup 的取舍

两条动画同时跑,最直觉的写法是分别 start()。问题在于你需要知道"两条都结束了"才算动画完成,否则收起动画还没结束就允许用户再次展开,宽度会跳。QParallelAnimationGroup 帮你收口:里面的子动画并行播放,整个 group 在耗时最长的那条结束时发出 finished。

// sidewidget.cpp(节选) void SideWidget::buildAnimation() { // 宽度:驱动壳的自定义属性 m_widthAnim = new QPropertyAnimation(this, "sidebarWidth", this); m_widthAnim->setDuration(220); m_widthAnim->setEasingCurve(QEasingCurve::OutCubic); // 透明度:驱动面板的 graphics effect m_fadeAnim = new QPropertyAnimation(m_opacity, "opacity", this); m_fadeAnim->setDuration(180); m_fadeAnim->setEasingCurve(QEasingCurve::OutQuad); m_group = new QParallelAnimationGroup(this); m_group->addAnimation(m_widthAnim); m_group->addAnimation(m_fadeAnim); connect(m_group, &QParallelAnimationGroup::finished, this, [this]() { // 收尾对齐:动画最后一帧是浮点插值,宽度可能差 1px setSidebarWidth(m_expanded ? m_expandedWidth : 0); }); } void SideWidget::setSidebarWidth(int w) { m_sidebarWidth = w; setFixedWidth(w); // 壳:压住父布局,别让它把宽度拉回去 m_panel->setFixedWidth(w); // 面板:跟着壳一起变,避免内容提前换行 }

逻辑说明:setter 里同时设了壳和面板的 fixedWidth。只设壳不设面板的话,面板会被内部布局按最小尺寸撑住,出现"壳窄了、内容还溢在外面"的穿帮画面。finish 回调里补一次对齐,是因为插值到终点时值可能是 259.6 被截成 259,肉眼看不出来,但如果你在 finished 里读宽度做判断就会差一像素。

参数说明:时长 220ms / 180ms 这组配比在 200–280px 宽度的侧边栏上比较稳,透明度比宽度短,意味着展开时宽度还在走、面板已经全不透明了,视觉上"料先到、框后到";但收起时这个配比要反过来,第 4 章会讲怎么动态改。QParallelAnimationGroup 的 duration 是只读的,由最长子动画决定,不要试图 setDuration,设了也不生效。

3. 把侧边栏挂进主窗口:工程配置到第一次滑出

代码看懂和跑起来是两件事。Qt 工程最容易卡住的不是动画,是构建系统接不接得上、布局给不给面子。这一章按"能跑起来"的顺序走一遍。

3.1 qmake 与 CMake 两种接入方式

资源里给的是标准 Qt Widgets 工程。qmake 侧只要保证 Widgets 模块在,cpp/h 加进 SOURCES 和 HEADERS,QSS 走 qrc:

# sidewidget.pro QT += core gui widgets CONFIG += c++17 TARGET = sidewidget_demo SOURCES += \ main.cpp \ mainwindow.cpp \ sidewidget.cpp HEADERS += \ mainwindow.h \ sidewidget.h RESOURCES += res.qrc

CMake 侧对应写法,Qt6 和 Qt5 的差别只在 find_package 的包名:

# CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(sidewidget_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) # Q_OBJECT 必须开,否则属性系统不生成元信息 set(CMAKE_AUTORCC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) # Qt5 换成 Qt5 add_executable(sidewidget_demo main.cpp mainwindow.cpp sidewidget.cpp sidewidget.h res.qrc ) target_link_libraries(sidewidget_demo PRIVATE Qt6::Widgets)

逻辑说明:AUTOMOC 必须开。Q_PROPERTY 和 Q_OBJECT 依赖 moc 生成的元对象代码,moc 没跑,编译能过,运行时QPropertyAnimation找不到属性,动画静默不动,控制台只给你一句 "you're trying to animate a non-existing property sidebarWidth" 的警告,很容易被忽略。参数说明:如果项目里同时有 Qt5 和 Qt6 环境,建议用find_package(QT NAMES Qt6 Qt5 ...)的写法做兼容,别在两个分支里维护两份 CMakeLists。

3.2 主窗口布局:谁负责撑开、谁负责让位

挂载这一步决定后面 80% 的布局坑。主窗口中央放一个 QHBoxLayout,左边 SideWidget,右边主内容,主内容必须给 stretch:

// mainwindow.cpp #include "mainwindow.h" #include "sidewidget.h" #include <QHBoxLayout> #include <QPushButton> #include <QTextEdit> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { auto *central = new QWidget(this); auto *layout = new QHBoxLayout(central); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); m_side = new SideWidget(central); m_side->setExpandedWidth(260); // 目标宽度在这里定,不要写死在类里 auto *editor = new QTextEdit(central); editor->setPlaceholderText(QStringLiteral("主内容区")); layout->addWidget(m_side, 0); // 关键:stretch 给 0 layout->addWidget(editor, 1); // 主内容吃掉剩余空间 setCentralWidget(central); resize(1024, 700); auto *btn = new QPushButton(QStringLiteral("切换"), central); connect(btn, &QPushButton::clicked, m_side, &SideWidget::toggle); btn->move(20, 20); } MainWindow::~MainWindow() = default;

逻辑说明:addWidget(m_side, 0)里的 0 是 stretch,意思是不参与剩余空间分配。少了这个 0,SideWidget 收起后留下的空间会被 QHBoxLayout 按比例再分回去,你会看到侧边栏明明设了 fixedWidth(0)、界面上却还空着一条。反过来,如果主内容也没给 stretch,两个控件会平分,展开侧边栏时主内容被压缩,QTextEdit 里的文字疯狂重排。参数说明:setContentsMargins(0,0,0,0) 和 setSpacing(0) 必须清掉,否则侧边栏和主内容之间永远有一条 9px 的缝,收起动画看起来像"没关严"。

3.3 toggle 的状态机:别用布尔标记判方向

toggle 看着简单,实际是并发问题的源头。用户手快,在 220ms 内连点三次,如果实现里只用 m_expanded 判断方向,第二次点击时 m_expanded 已经翻转,但宽度还在半路,动画会从"半路"往一个错误的方向走。

void SideWidget::expand() { if (m_expanded) return; m_expanded = true; m_widthAnim->stop(); // 先停,避免和上一次动画打架 m_fadeAnim->stop(); m_widthAnim->setStartValue(m_sidebarWidth); // 从当前真实宽度起步 m_widthAnim->setEndValue(m_expandedWidth); m_fadeAnim->setStartValue(m_opacity->opacity()); m_fadeAnim->setDuration(180); // 展开:淡入快 m_fadeAnim->setEndValue(1.0); m_group->start(); } void SideWidget::collapse() { if (!m_expanded) return; m_expanded = false; m_widthAnim->stop(); m_fadeAnim->stop(); m_widthAnim->setStartValue(m_sidebarWidth); m_widthAnim->setEndValue(0); m_fadeAnim->setStartValue(m_opacity->opacity()); m_fadeAnim->setDuration(260); // 收起:淡出慢,盖住宽度的尾巴 m_fadeAnim->setEndValue(0.0); m_group->start(); } void SideWidget::toggle() { if (m_expanded) { collapse(); } else { expand(); } }

逻辑说明:三次 stop + 从 m_sidebarWidth 重新设 startValue,等于把动画变成"可抢占"的。任何时候用户的输入都能打断当前动画并从当前位置接着走,这正是原生动画组件比手写插值省心的地方。参数说明:透明度时长做了分支,展开 180ms 短于宽度的 220ms,收起 260ms 长于宽度的 220ms。这个不对称是刻意的——收起时如果透明度先归零,你会看到一条空白条还在慢慢收窄,观感很差;让面板"晚一点消失",宽度收完时它刚好淡完,视觉上是一个整体动作。

4. 渐入渐出的手感调优:缓动曲线、时长与错峰

动画能跑只是及格线,手感是另一回事。同一个 220ms,换条缓动曲线,观感能差出一个档次。这一章讲三个可调的旋钮,以及各自的经验值。

4.1 缓动曲线怎么选:几条曲线的观感对照

QEasingCurve 内置三十多种曲线,侧边栏这类"位移 + 淡入"的组合,实际会被反复用到的就那么几条:

曲线观感描述适用场景建议时长
OutCubic起步快、末尾缓,最"顺"侧边栏、抽屉、Toast 默认首选200–260ms
OutQuart比 OutCubic 更急,收尾更干脆位移小于 160px 的小面板160–200ms
InOutCubic两头慢中间快,有仪式感常驻面板的宽窄切换280–360ms
OutBack末尾轻微过冲回弹,带"弹"味工具型浮动面板,正式软件慎用240–300ms
Linear匀速,机械感强基本不用,除非做进度类—

我的默认值一直是 OutCubic。OutBack 的过冲会让侧边栏在终点往里"挤"一下,视觉上像没对齐,调试阶段容易被误判成布局 Bug。参数说明:缓动曲线作用在 0–1 的归一化进度上,和时长独立,先定时长再挑曲线,顺序反过来会反复改。

4.2 时长、帧率与错峰:三个容易被忽略的量

时长不是越短越"高级"。220ms 以下,宽度变化只有几帧,人眼捕捉到的是"跳"而不是"滑";320ms 以上,用户会觉得软件卡。落在 200–280ms 这个区间,是基于"人类对界面响应 100ms 内认为是即时、300ms 内可以接受"的普遍经验,不是什么神秘数字。

帧率方面,Qt 动画由内部统一定时器驱动,大致 60Hz,你不要自己再开 QTimer 去 setFixedWidth 补帧,两套定时器叠加必然抖动。真正影响帧率的是第 5 章要讲的 QGraphicsOpacityEffect 成本。

错峰(stagger)是让两条动画不同时开始的小技巧。上面代码里靠"时长不对称"实现的是最简版本。想要更讲究,可以用 QSequentialAnimationGroup 把透明度延迟 40ms 再启动:

// 展开时让透明度晚 40ms 起步,宽度先动、内容后现 auto *seq = new QSequentialAnimationGroup(this); seq->addPause(40); auto *fadeIn = new QPropertyAnimation(m_opacity, "opacity", this); fadeIn->setDuration(160); fadeIn->setStartValue(0.0); fadeIn->setEndValue(1.0); seq->addAnimation(fadeIn); // 与宽度动画并行 auto *group = new QParallelAnimationGroup(this); group->addAnimation(m_widthAnim); group->addAnimation(seq); group->start();

逻辑说明:宽度先启动,40ms 后内容才开始浮现,视觉上是"框先划开、内容再长出来",比两条线同时起步更有层次。参数说明:40ms 是经验值,低于 20ms 人眼分辨不出,高于 80ms 会明显感觉内容出来晚了、界面像分两步加载。

4.3 用 QSS 配合动画时的三个约束

侧边栏一般都要上样式表,QSS 和动画撞车的地方有三处:

第一,接了 QGraphicsOpacityEffect 的控件,不能再挂 QGraphicsDropShadowEffect。一个 widget 只能有一个 graphics effect,后设的把先设的顶掉,你会看到阴影突然消失,或者透明度完全不动。要阴影就放在没收 effect 的父容器上。

第二,QSS 里的background-color: rgba(...)和 opacity effect 是两套透明度,叠加之后颜色会变得比预期更淡,而且不受动画控制。要么用 effect 统一控制,要么用 QSS,别混着来。

第三,带圆角的面板必须setAttribute(Qt::WA_StyledBackground, true),否则 QSS 的border-radius对自定义 QWidget 子类不生效,圆角出不来,动画过程中还会看到直角矩形一闪。

// 面板样式与效果的正确顺序 m_panel->setObjectName("sidePanel"); m_panel->setAttribute(Qt::WA_StyledBackground, true); // 圆角生效的前提 m_panel->setStyleSheet(R"( #sidePanel { background-color: #1f2329; border-right: 1px solid #2c3138; } )"); m_opacity = new QGraphicsOpacityEffect(m_panel); m_opacity->setOpacity(0.0); m_panel->setGraphicsEffect(m_opacity); // 最后一步,别被后面的 setGraphicsEffect 覆盖

逻辑说明:先样式后 effect。反过来的话,某些平台下 QSS 重绘会重置 effect 的缓存。参数说明:border-right画分割线比在外层加一条 QFrame 更省事,而且跟着宽度动画一起走,不会在收起时留下一条悬空的线。

5. 避坑与排查:侧边栏动画最容易翻车的五处

这一章的血泪经验全是"动画写完、界面看着对、实际一用就崩"的类型。每条按现象、原因、解决三步走。

5.1 布局与尺寸类:宽度压不住、收起留白

现象一:侧边栏展开后瞬间被弹回 0,或者只展开了一小半就停住。

原因:设了 setFixedWidth,但父级是 QSplitter,或者父布局在动画结束后又跑了一次 activate。QSplitter 用 setSizes 管理子控件,它会覆盖你设的固定宽度;另一种情况是主内容有 minimumSize 约束,总宽度不够时布局会强行压缩侧边栏。

解决:如果是 QSplitter,把侧边栏换成不受 splitter 管理的普通布局项,或者监听 splitterMoved 重新 setFixedWidth;如果是最小尺寸冲突,给主内容setMinimumWidth(0)并确认窗口本身宽度足够。排查时在 setter 里打一行qDebug() << w << width() << minimumWidth() << maximumWidth();,三个数不一致就说明有东西在改约束。

现象二:收起动画走完了,界面上还留着一条空白。

原因:setSpacing 或 contentsMargins 没清零,或者侧边栏外面还包了一层带 padding 的 QWidget。

解决:从 SideWidget 往上逐层检查 layout 的 spacing、margins,以及 QSS 里有没有 padding。这类问题的特征是"宽度确实是 0,但视觉上有缝",很好定位。

5.2 渲染与性能类:卡顿、闪烁、特效失效

现象三:动画期间 CPU 占用飙升,界面明显掉帧,尤其是侧边栏里塞了表格或富文本控件时。

原因:QGraphicsOpacityEffect 会让整个子控件树走离屏渲染,每一帧把面板画到 pixmap 上再合成。面板里如果有 QTableWidget、QTextEdit 这类重绘成本高的控件,宽度每变一次就等于整表重绘一次。

解决:把 effect 从"包含重型控件"的 widget 上摘掉,改成只对轻量部分(图标、标题文字、装饰条)做淡入;或者干脆砍掉透明度动画,只留宽度位移,靠 OutCubic 的顺滑感撑住观感。实测下来,砍掉透明度在 200px 宽度以内几乎察觉不到损失,这是最省事的后悔药。

现象四:动画结束时闪一下白,或者最后一帧宽度差一像素。

原因:插值到终点是浮点数,setFixedWidth 接收 int 时截断,最后一帧和 finished 里补设的值不一致,触发一次多余的 relayout。

解决:在 group 的 finished 里强制setSidebarWidth(终点值)(第 2 章代码里已经这么做了),或者把 setter 参数改成 qreal、内部 qRound。

5.3 连点与状态类:动画卡在半开

现象五:快速连点切换按钮,侧边栏停在半开状态不再响应。

原因:只用布尔标记判断方向,中断时 startValue 取了旧值;或者动画对象被 deleteLater 掉但指针没置空,下一次 start 访问野指针。

解决:每次 start 前stop()并从 m_sidebarWidth 重设 startValue;动画对象用 parent 托管,不要手动 delete;如果一定要重建,用 QPointer 持有。防御性写法是给 expand/collapse 加个 50ms 的节流,但更好的做法是像第 3 章那样让动画本身可抢占——用户想连点就让他连点,动画跟着走就行。

提示:这五条里,前两条属于布局、后两条属于渲染,只有最后一条是状态问题。排查时先看现象落在哪一类,别一上来就怀疑动画参数,十次里有七次是布局在背后捣鬼。

6. 进阶:把 SideWidget 抽成可复用组件并验证它真的到位

单个界面里能跑不算完,真正省时间的是把它变成能塞进任何工程的组件,并且有一套不靠肉眼判断对错的验证方式。

先抽出配置结构体,把时长、曲线、目标宽度、是否启用淡入全暴露出去:

struct SideWidgetConfig { int expandedWidth = 260; int expandMs = 220; int collapseMs = 220; int fadeMs = 180; QEasingCurve widthCurve = QEasingCurve::OutCubic; bool fadeEnabled = true; };

外部只传一个 config,类内部按值初始化动画参数。好处是同一套代码可以在不同窗口里用不同节奏——主侧栏 260px/220ms,次级抽屉 180px/160ms,不用改类本身。配置里留fadeEnabled这个开关也很关键,某些低配机器或者投屏场景下,关掉透明度动画的收益比想象中大。

验证这块我用 QTest 写最小用例,思路是不去截图比对,而是断言动画终点状态:

#include <QtTest> #include "sidewidget.h" class SideWidgetTest : public QObject { Q_OBJECT private slots: void expandReachesTargetWidth(); void collapseReturnsToZero(); }; void SideWidgetTest::expandReachesTargetWidth() { SideWidget w; w.setExpandedWidth(260); w.show(); w.expand(); // 动画 220ms,给 1000ms 余量,避免 CI 机器慢导致偶发失败 QTRY_COMPARE_WITH_TIMEOUT(w.sidebarWidth(), 260, 1000); QCOMPARE(w.width(), 260); // 壳的实际宽度也要对上 QVERIFY(w.isExpanded()); } void SideWidgetTest::collapseReturnsToZero() { SideWidget w; w.setExpandedWidth(260); w.show(); w.expand(); QTRY_COMPARE_WITH_TIMEOUT(w.sidebarWidth(), 260, 1000); w.collapse(); QTRY_COMPARE_WITH_TIMEOUT(w.sidebarWidth(), 0, 1000); QCOMPARE(w.width(), 0); QVERIFY(!w.isExpanded()); } QTEST_MAIN(SideWidgetTest) #include "sidewidget_test.moc"

逻辑说明:QTRY_COMPARE_WITH_TIMEOUT 会反复重试直到条件成立或超时,正好契合动画这种异步过程。同时断言 sidebarWidth 和 width 两个值,能抓住"属性改了但布局没跟上"这一类问题——这种 Bug 靠肉眼看动画是看不出来的,因为动画过程看起来完全正常,只有停下来才发现宽度不对。

参数说明:超时给 1000ms 是动画时长的 4 倍多,CI 环境里够用;如果想验证"动画中途被打断"这个场景,可以在 expand 后立刻 collapse,再断言最终停在 0 且没有异常,这比测终点更能暴露状态机问题。

另一个实用技巧是把时长做成可配的 0:expandMs = 0时 QPropertyAnimation 会直接跳到终点,测试里瞬间完成,不用等。这个开关同时也能给最终用户当"关闭动效"选项,一举两得。

从那以后我每次接侧边栏需求,都先把两件事写进自检单再动手改动画——setFixedWidth有没有被父布局覆盖、透明度 effect 挂在了哪个控件上。这两条确认完,剩下的曲线和时长都是锦上添花。侧边栏这种"看起来五分钟、调起来两小时"的部件,真正值钱的地方从来不是动画 API,而是这些边界条件有没有人替你趟过。压缩包就在附件里,先把它跑起来,再按自己的工程改 config,比对着文档空想快得多。希望帮到你。

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

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

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

立即咨询