)
很多 Qt 开发者都会写界面却很少有人真正会写动画。于是我们经常看到这样的软件页面切换像“瞬移”没有任何过渡按钮点击后毫无反馈不知道是否响应弹窗直接出现、直接消失显得十分生硬菜单展开没有任何过渡效果仿佛回到了 Windows XP 时代。而真正优秀的软件例如 Qt Creator、WPS、QQ、微信 PC、腾讯会议、钉钉等几乎每一个交互都伴随着细腻的动画。很多人认为动画只是为了“炫酷”其实并不是。动画真正的作用是帮助用户理解界面发生了什么。例如一个窗口为什么突然消失一个按钮为什么不能点击一个菜单是从哪里出来的登录到底成功了没有动画就是界面与用户之间最自然的一种“语言”。Qt 从 Qt4 开始就提供了一套完整的 Animation Framework动画框架。很多人只会QPropertyAnimation其实真正掌握 Qt 动画需要理解整个动画体系。今天就带大家彻底学会 Qt Animation Framework并附上15 个可直接运行的完整 Demo涵盖按钮、菜单、侧边栏、抽屉、Toast、Loading、数字滚动、页面切换、卡片 Hover、进度动画等常用场景。一、Qt 动画框架到底长什么样Qt 的动画框架其实并不复杂核心类关系如下可以理解为QVariantAnimation负责计算中间值QPropertyAnimation负责修改属性QEasingCurve负责控制运动节奏AnimationGroup负责组合多个动画。真正企业项目中超过 90% 的动画都是由这几个类组合完成的。二、动画到底是什么很多人觉得动画很神秘。其实动画只有一句话在指定时间内把一个属性从 A 变成 B。例如按钮 X 坐标从 0 变到 300。如果直接调用button-move(300, 0);按钮就是瞬移。如果 Qt 每隔 16ms 修改一次坐标0 → 15 → 30 → ... → 300按钮就动起来了。所以动画的本质就是不断修改属性Qt 帮我们完成了这些计算和定时刷新。三、QVariantAnimation——所有动画的基础QVariantAnimation是 Qt 动画框架真正的核心。所有动画本质都是开始值 → 中间值 → 结束值。它并不直接操作任何界面元素只负责在给定时间内产生连续变化的数值。Demo 1数字滚动动画很多工业软件需要显示温度、速度等实时变化的数值比如从 25℃ 渐变到 80℃。我们完全不需要手动写定时器几行代码就能实现。auto *animation new QVariantAnimation(this); animation-setDuration(1500); animation-setStartValue(25); animation-setEndValue(80); animation-setEasingCurve(QEasingCurve::OutCubic); connect(animation, QVariantAnimation::valueChanged, this, [](const QVariant value) { ui-labelTemp-setText(QString(%1 ℃).arg(value.toInt())); }); // 点击按钮启动动画 connect(ui-btnStart, QPushButton::clicked, animation, [animation]() { animation-start(); });效果就是 25℃ → 26℃ → ... → 80℃整个过程中没有任何 Timer代码极其简洁。四、QPropertyAnimation——最常用的动画QPropertyAnimation是QVariantAnimation的子类它可以直接修改QObject的任意属性前提是该属性有Q_PROPERTY声明。我们日常开发中最常用的就是它。Demo 2按钮平滑移动auto *animation new QPropertyAnimation(ui-pushButton, pos); animation-setDuration(600); animation-setStartValue(QPoint(0, 0)); animation-setEndValue(QPoint(300, 0)); animation-setEasingCurve(QEasingCurve::OutCubic); animation-start(QAbstractAnimation::DeleteWhenStopped);整个过程无需手动调用move()按钮就会平滑地从 (0,0) 滑到 (300,0)。Demo 3现代侧边栏展开很多后台管理系统都有侧边栏滑出效果。假设左侧有一个QWidget名为leftWidget宽度 220平时隐藏在屏幕外x -220点击按钮后平滑滑入。void MainWindow::toggleSidebar() { bool isHidden ui-leftWidget-x() 0; auto *animation new QPropertyAnimation(ui-leftWidget, geometry); animation-setDuration(250); animation-setEasingCurve(QEasingCurve::OutCubic); if (isHidden) { animation-setStartValue(QRect(-220, 0, 220, height())); animation-setEndValue(QRect(0, 0, 220, height())); } else { animation-setStartValue(QRect(0, 0, 220, height())); animation-setEndValue(QRect(-220, 0, 220, height())); } animation-start(QAbstractAnimation::DeleteWhenStopped); }侧边栏就会平滑滑出/滑入而不是“突然出现”。这也是企业软件中最常见的动画之一。五、Hover 放大按钮完整 Demo很多现代 UI 中鼠标移到按钮上按钮会微微放大移开恢复。这能极大提升交互质感。Demo 4Hover 放大效果我们可以封装一个通用的动画函数利用geometry属性实现放大其实就是扩大控件的矩形区域。注意我们通常会保存控件的原始几何信息这里假设按钮初始位置固定我们直接基于当前几何信息计算。void MainWindow::playHoverAnimation(QPushButton *btn, bool enter) { QRect start btn-geometry(); QRect end; if (enter) { // 向四周扩展 3 像素 end start.adjusted(-3, -2, 3, 2); } else { // 恢复到原始几何这里简单写回原位置实际项目可保存原始值 end QRect(100, 100, 200, 50); // 假设原始位置 } auto *animation new QPropertyAnimation(btn, geometry); animation-setDuration(120); animation-setStartValue(start); animation-setEndValue(end); animation-setEasingCurve(QEasingCurve::OutQuad); animation-start(QAbstractAnimation::DeleteWhenStopped); }然后在eventFilter或重写enterEvent/leaveEvent中调用bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj ui-btnLogin) { if (event-type() QEvent::Enter) { playHoverAnimation(ui-btnLogin, true); } else if (event-type() QEvent::Leave) { playHoverAnimation(ui-btnLogin, false); } } return QMainWindow::eventFilter(obj, event); }安装事件过滤器ui-btnLogin-installEventFilter(this);六、透明度动画弹窗淡入淡出Qt 控件本身没有opacity属性但可以通过QGraphicsOpacityEffect来实现。Demo 5弹窗淡入效果假设有一个自定义弹出面板popup我们想让它从完全透明逐渐显现。void MainWindow::fadeInPopup(QWidget *popup) { auto *effect new QGraphicsOpacityEffect(popup); effect-setOpacity(0); popup-setGraphicsEffect(effect); auto *animation new QPropertyAnimation(effect, opacity); animation-setDuration(300); animation-setStartValue(0); animation-setEndValue(1); animation-setEasingCurve(QEasingCurve::OutCubic); animation-start(QAbstractAnimation::DeleteWhenStopped); } void MainWindow::fadeOutPopup(QWidget *popup) { auto *effect qobject_castQGraphicsOpacityEffect*(popup-graphicsEffect()); if (!effect) return; auto *animation new QPropertyAnimation(effect, opacity); animation-setDuration(300); animation-setStartValue(1); animation-setEndValue(0); animation-setEasingCurve(QEasingCurve::OutCubic); connect(animation, QPropertyAnimation::finished, popup, QWidget::hide); animation-start(QAbstractAnimation::DeleteWhenStopped); }淡出时我们在动画结束后隐藏控件避免残留。七、自定义属性动画完整 CircleWidget 示例并非只有内置属性才能做动画。我们可以声明自己的Q_PROPERTY然后配合QPropertyAnimation驱动自定义控件。Demo 6圆形进度动画创建一个CircleWidget它有一个半径属性radius动画驱动半径变化圆从小变大。circlewidget.h#include QWidget class CircleWidget : public QWidget { Q_OBJECT Q_PROPERTY(int radius READ radius WRITE setRadius) public: explicit CircleWidget(QWidget *parent nullptr); int radius() const; void setRadius(int r); protected: void paintEvent(QPaintEvent *event) override; private: int m_radius 20; };circlewidget.cpp#include circlewidget.h #include QPainter CircleWidget::CircleWidget(QWidget *parent) : QWidget(parent) { setMinimumSize(200, 200); } int CircleWidget::radius() const { return m_radius; } void CircleWidget::setRadius(int r) { m_radius r; update(); // 触发重绘 } void CircleWidget::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); painter.setBrush(QColor(52, 152, 219)); painter.drawEllipse(rect().center(), m_radius, m_radius); }使用动画驱动半径从 0 变到 80auto *circle new CircleWidget(this); circle-setGeometry(100, 100, 200, 200); auto *anim new QPropertyAnimation(circle, radius); anim-setDuration(1000); anim-setStartValue(0); anim-setEndValue(80); anim-setEasingCurve(QEasingCurve::OutElastic); anim-start();八、QEasingCurve——为什么动画这么丝滑QEasingCurve控制的是动画的“节奏”即属性值随时间的变化速率。线性曲线就是匀速变化但真实世界的运动都有加速和减速所以我们需要缓动曲线。Qt 提供了 40 多种内置曲线常用的有曲线类型感觉适用场景Linear匀速、生硬进度条、旋转少数OutQuad/OutCubic快出慢停按钮反馈、Hover 放大OutBack超出后回弹提示消息ToastOutElastic弹性衰减趣味性动效InOutCubic先慢后快再慢页面切换、模态弹窗OutExpo极快衰减侧边栏、大尺寸移动实际开发中统一曲线风格非常重要。一般规则按钮 HoverOutQuad或OutCubic120ms按钮 PressOutQuad80ms侧边栏/大面板OutCubic或OutExpo250–300ms对话框弹入弹出InOutCubic220msToast 提示OutBack250ms页面切换InOutCubic260ms下面我们结合更多 Demo 继续感受。九、动画组合并行与串行现代 UI 中很少有单独一个动画往往需要多个动画配合。使用QParallelAnimationGroup并行和QSequentialAnimationGroup串行即可轻松实现。Demo 7按钮移动并渐隐并行按钮向右移动的同时逐渐透明。auto *effect new QGraphicsOpacityEffect(btn); btn-setGraphicsEffect(effect); effect-setOpacity(1); auto *moveAnim new QPropertyAnimation(btn, pos); moveAnim-setDuration(500); moveAnim-setStartValue(QPoint(50, 50)); moveAnim-setEndValue(QPoint(300, 50)); auto *fadeAnim new QPropertyAnimation(effect, opacity); fadeAnim-setDuration(500); fadeAnim-setStartValue(1); fadeAnim-setEndValue(0); auto *group new QParallelAnimationGroup(this); group-addAnimation(moveAnim); group-addAnimation(fadeAnim); group-start(QAbstractAnimation::DeleteWhenStopped);Demo 8登录按钮缩放 → Loading 旋转 → 窗口淡出串行模拟点击登录后的连续动画按钮先缩小再恢复表示按下的反馈然后出现一个 Loading 旋转动画加载完成后窗口淡出。// 按下缩放动画 auto *pressAnim new QPropertyAnimation(ui-btnLogin, geometry); pressAnim-setDuration(80); QRect origGeo ui-btnLogin-geometry(); QRect pressedGeo origGeo.adjusted(5, 5, -5, -5); pressAnim-setStartValue(origGeo); pressAnim-setEndValue(pressedGeo); pressAnim-setEasingCurve(QEasingCurve::OutQuad); // 恢复动画 auto *releaseAnim new QPropertyAnimation(ui-btnLogin, geometry); releaseAnim-setDuration(80); releaseAnim-setStartValue(pressedGeo); releaseAnim-setEndValue(origGeo); releaseAnim-setEasingCurve(QEasingCurve::OutQuad); // 串行组合 auto *seq new QSequentialAnimationGroup(this); seq-addAnimation(pressAnim); seq-addAnimation(releaseAnim); seq-start(QAbstractAnimation::DeleteWhenStopped); // 之后可以再串联 Loading 和窗口淡出...限于篇幅完整串行 Loading 旋转 Demo 见下面 Loading 部分。十、更多企业级动画 Demo 集锦下面一口气给出另外 7 个常用动画 Demo覆盖菜单、抽屉、Toast、Loading、页面切换、卡片 Hover 和进度动画。Demo 9菜单弹出动画类似右键菜单或下拉菜单从无到有向下展开的同时淡入。void MainWindow::showMenuAnimated(QMenu *menu, QPoint pos) { menu-setAttribute(Qt::WA_TranslucentBackground); // 需配合样式表 auto *effect new QGraphicsOpacityEffect(menu); effect-setOpacity(0); menu-setGraphicsEffect(effect); auto *fadeAnim new QPropertyAnimation(effect, opacity); fadeAnim-setDuration(180); fadeAnim-setStartValue(0); fadeAnim-setEndValue(1); fadeAnim-setEasingCurve(QEasingCurve::OutCubic); // 同时利用菜单的 sizeHint 做高度动画比较复杂这里仅演示透明度。 // 实际可用 QVariantAnimation 驱动菜单高度或使用 QWidget 模拟菜单。 fadeAnim-start(QAbstractAnimation::DeleteWhenStopped); menu-exec(pos); }Demo 10抽屉效果从底部滑出面板常见于移动端或桌面端的选择器。一个QFrame从窗口底部滑上来。void MainWindow::showDrawer(QWidget *drawer) { drawer-setVisible(true); int drawerHeight 300; int parentHeight this-height(); auto *anim new QPropertyAnimation(drawer, geometry); anim-setDuration(300); anim-setEasingCurve(QEasingCurve::OutCubic); anim-setStartValue(QRect(0, parentHeight, width(), drawerHeight)); anim-setEndValue(QRect(0, parentHeight - drawerHeight, width(), drawerHeight)); anim-start(QAbstractAnimation::DeleteWhenStopped); } void MainWindow::hideDrawer(QWidget *drawer) { int drawerHeight drawer-height(); auto *anim new QPropertyAnimation(drawer, geometry); anim-setDuration(300); anim-setEasingCurve(QEasingCurve::OutCubic); anim-setStartValue(drawer-geometry()); anim-setEndValue(QRect(0, height(), width(), drawerHeight)); connect(anim, QPropertyAnimation::finished, drawer, QWidget::hide); anim-start(QAbstractAnimation::DeleteWhenStopped); }Demo 11Toast 提示消息带弹性效果Toast 从透明出现并轻微回弹停留一会儿后淡出消失。void MainWindow::showToast(const QString msg) { auto *label new QLabel(msg, this); label-setStyleSheet(background: #333; color: white; padding: 12px 24px; border-radius: 6px;); label-adjustSize(); int x (width() - label-width()) / 2; int y height() - 100; label-move(x, y); label-show(); auto *effect new QGraphicsOpacityEffect(label); effect-setOpacity(0); label-setGraphicsEffect(effect); auto *fadeIn new QPropertyAnimation(effect, opacity); fadeIn-setDuration(250); fadeIn-setStartValue(0); fadeIn-setEndValue(1); fadeIn-setEasingCurve(QEasingCurve::OutBack); auto *stay new QPauseAnimation(1500); // 停留 1.5 秒 auto *fadeOut new QPropertyAnimation(effect, opacity); fadeOut-setDuration(250); fadeOut-setStartValue(1); fadeOut-setEndValue(0); auto *seq new QSequentialAnimationGroup(this); seq-addAnimation(fadeIn); seq-addAnimation(stay); seq-addAnimation(fadeOut); connect(seq, QSequentialAnimationGroup::finished, label, QObject::deleteLater); seq-start(QAbstractAnimation::DeleteWhenStopped); }Demo 12Loading 旋转动画使用QVariantAnimation驱动一个角度属性不断旋转一个QLabel或自定义控件。// 假设 ui-loadingLabel 是一个显示加载图标的 QLabel auto *anim new QVariantAnimation(this); anim-setDuration(1000); anim-setStartValue(0); anim-setEndValue(360); anim-setLoopCount(-1); // 无限循环 connect(anim, QVariantAnimation::valueChanged, this, [](const QVariant val) { QPixmap pix(:/loading.png); QTransform transform; transform.rotate(val.toInt()); ui-loadingLabel-setPixmap(pix.transformed(transform, Qt::SmoothTransformation)); }); anim-start();Demo 13页面切换动画左右平移模拟移动端页面切换当前页面向左滑出新页面从右侧滑入。void MainWindow::switchPage(QWidget *currentPage, QWidget *newPage) { int w width(); newPage-setGeometry(w, 0, w, height()); newPage-show(); auto *animCur new QPropertyAnimation(currentPage, pos); animCur-setDuration(260); animCur-setStartValue(QPoint(0, 0)); animCur-setEndValue(QPoint(-w, 0)); animCur-setEasingCurve(QEasingCurve::InOutCubic); auto *animNew new QPropertyAnimation(newPage, pos); animNew-setDuration(260); animNew-setStartValue(QPoint(w, 0)); animNew-setEndValue(QPoint(0, 0)); animNew-setEasingCurve(QEasingCurve::InOutCubic); auto *group new QParallelAnimationGroup(this); group-addAnimation(animCur); group-addAnimation(animNew); connect(group, QParallelAnimationGroup::finished, currentPage, QWidget::hide); group-start(QAbstractAnimation::DeleteWhenStopped); }Demo 14卡片 Hover 放大 阴影卡片是一个QFrame鼠标悬浮时整体放大并增加阴影通过样式表模拟。void MainWindow::cardHoverAnimation(QFrame *card, bool enter) { QRect start card-geometry(); QRect end; if (enter) { end start.adjusted(-5, -5, 5, 5); card-setStyleSheet(QFrame { background: white; border-radius: 8px; border: 1px solid #ddd; } QFrame:hover { border-color: #4A90E2; }); } else { end start; // 实际使用时应保存原始几何 card-setStyleSheet(QFrame { background: white; border-radius: 8px; border: 1px solid #ddd; }); } auto *anim new QPropertyAnimation(card, geometry); anim-setDuration(200); anim-setStartValue(start); anim-setEndValue(end); anim-setEasingCurve(QEasingCurve::OutCubic); anim-start(QAbstractAnimation::DeleteWhenStopped); }安装事件过滤器后在Enter和Leave时分别调用。Demo 15平滑进度条利用QVariantAnimation驱动进度值让进度条平滑变化而不是瞬间跳变。void MainWindow::setProgressSmooth(int targetValue) { auto *anim new QVariantAnimation(this); anim-setDuration(300); anim-setStartValue(ui-progressBar-value()); anim-setEndValue(targetValue); anim-setEasingCurve(QEasingCurve::OutCubic); connect(anim, QVariantAnimation::valueChanged, this, [](const QVariant val) { ui-progressBar-setValue(val.toInt()); }); anim-start(QAbstractAnimation::DeleteWhenStopped); }十一、企业项目中的动画规范在实际项目中建议统一动画参数避免风格混乱。可以参照下表场景时长推荐曲线Hover 放大120msOutQuad按下反馈80msOutQuad对话框弹入220msInOutCubic菜单展开180msOutCubic侧边栏滑入300msOutExpoToast 提示250msOutBack页面切换260msInOutCubic抽屉弹出300msOutCubic统一时长和曲线后软件整体交互会显得非常专业、一致。十二、Qt 动画开发中的常见坑避坑指南1. 动画没有效果动画对象被提前释放动画对象创建在栈上函数结束时就被析构。务必在堆上创建并设置DeleteWhenStopped或者用成员变量保存。属性名拼写错误post而不是pos或者对象根本没有对应的Q_PROPERTY。起始值与结束值相同动画不会触发任何变化。2. 对 geometry 做动画时控件抖动如果控件放在布局QLayout中布局管理器会在动画过程中不断重新计算位置导致抖动。解决办法动画前将控件临时移出布局layout-removeWidget()动画结束再加回去。或者直接对父容器没有布局约束做动画。3. 动画越来越卡可能的原因同时启动了过多动画例如列表每一项都在播放。请限制同时运行的动画数量。在valueChanged信号中执行了重计算或复杂绘制。每一帧都调用了repaint()或update()可能形成递归。避免在动画进行中频繁调整布局、创建/销毁控件。4. 透明动画不生效一定要检查控件是否设置了setAttribute(Qt::WA_TranslucentBackground)并且背景样式透明否则QGraphicsOpacityEffect可能绘制异常。5. 动画结束时属性没有停留在终点默认情况下动画结束后属性会保持在最终值但如果设置了setEndValue()并且动画中途被停止可能需要手动处理。建议监听finished信号确保最终状态。总结Qt Animation Framework 并不复杂但它决定了软件的“质感”。真正优秀的桌面应用动画不会喧宾夺主而是在恰当的时候给予用户明确的反馈让每一次点击、每一次切换都更加自然。本文从零开始梳理了整个动画框架的体系结构并提供了15 个可直接使用的完整 Demo数字滚动按钮移动侧边栏展开Hover 放大弹窗淡入淡出圆形进度动画自定义属性按钮移动并渐隐并行组合登录按钮按压缩放 串行基础菜单弹出抽屉效果Toast 提示Loading 旋转页面切换卡片 Hover 放大平滑进度条