1. 为什么动画在自定义 QStyle 中不可或缺

没有动画的 UI 就像没有过渡的 PPT——功能可用,但体验生硬。在 QStyle 中引入动画带来三个维度的提升:

1.1 感知响应速度(Perceived Responsiveness)

考虑一个按钮的 hover 效果:无动画时,鼠标移入瞬间背景色从白色跳变为蓝色。用户的视觉系统需要时间"理解"这个变化——这 16ms 的跳变在主观感受上反而比 200ms 的渐变更"慢”。

// ❌ 无动画:颜色直接跳变
auto bgColor = isHovered ? hoverColor : normalColor;

// ✅ 有动画:从当前值平滑过渡到目标值
auto bgColor = manager.animateBackgroundColor(
    widget, targetColor, animationDuration);

动画的本质是把离散的状态变化转化为连续的视觉流。人眼对连续变化的感知阈值远低于离散跳变,所以 200ms 的 fade-in 给人的感觉比 0ms 的 instant 更快。

1.2 焦点指示(Focus Indication)

键盘聚焦是 Qt 可访问性的核心。不做任何动画,QStyle::PE_FrameFocusRect 就是一个死板的虚线框。但如果你让焦点边框的颜色在 borderColor(普通)和 focusColor(聚焦)之间平滑渐变,用户会感受到一种自然的状态反馈。

1.3 自定义控件反馈(Custom Widget Feedback)

标准 Qt 控件只有少数状态有动画(比如 QAbstractButton 没有内置 hover 动画)。一旦你开始自绘控件——Switch 的滑块滑动、Slider 的填充条跟踪——动画就是刚需。


2. WidgetAnimationManager 设计

2.1 核心思路

qlementine 的动画系统由三层组成:

WidgetAnimation<T>         ← 对单个属性的 QVariantAnimation 包装
    ↓
WidgetAnimator              ← 一个 widget 的所有动画属性集合
    ↓
WidgetAnimationManager      ← 全局管理所有 widget → WidgetAnimator 映射

WidgetAnimationManager 是 Style 直接调用的入口。它的核心 API:

// 调用即动画:传入目标色,返回当前插值结果
QColor animateBackgroundColor(const QWidget* w,
    const QColor& target, int duration);

// 查询当前值(不启动新动画)
std::optional<QColor> getAnimatedBackgroundColor(const QWidget* w) const;

2.2 缓存策略

每个 widget + property 组合只创建一次动画对象。WidgetAnimator 内部使用 lazy 初始化:

// 第一次调用 getBackgroundColorAnimation() 时创建
if (!_BackgroundColor) {
    _BackgroundColor = std::make_unique<WidgetAnimation<QColor>>(parentWidget);
}

后续该 widget 的任何 animateBackgroundColor() 调用都会复用同一个 WidgetAnimation<T> 实例。restartIfNeeded() 会检查目标值是否改变,避免重复启停动画。

2.3 Widget 生命周期管理

WidgetAnimationManager 使用 std::unordered_map<const QWidget*, WidgetAnimator*> 维护映射。当 widget 被销毁时,通过 QObject::destroyed 信号自动清理:

QObject::connect(widget, &QObject::destroyed, widget, [this, widget]() {
    removeWidget(widget);
});

2.4 动画禁用策略

setEnabled(false) 时,animateXxx 的宏展开直接将 duration 设为 0,颜色瞬间到达目标值——这对性能敏感的场景(如窗口拖拽时的频繁 repaint)至关重要。

2.5 完整实现

以下是一份简化但完整的 WidgetAnimationManager,涵盖核心逻辑,约 130 行,直接可编译:

WidgetAnimation.h — 单个属性的类型安全动画包装

#pragma once

#include <QObject>
#include <QVariantAnimation>
#include <QWidget>
#include <memory>
#include <cassert>

template<typename T>
class WidgetAnimation : public QObject {
public:
    explicit WidgetAnimation(QWidget* parent)
        : QObject(parent)
    {
        assert(parent);
        _anim.setEasingCurve(QEasingCurve::OutCubic);
        QObject::connect(&_anim, &QVariantAnimation::valueChanged,
            parent, [parent]() { parent->update(); },
            Qt::QueuedConnection);
        parent->installEventFilter(this);
    }

    void setDuration(int ms) {
        if (ms != _anim.duration()) {
            stop();
            _anim.setDuration(ms);
        }
    }

    void setEasing(const QEasingCurve& curve) {
        _anim.setEasingCurve(curve);
    }

    void setTarget(const T& value) {
        if (value != _target || !_hasTarget) {
            if (!_hasStart)
                _start = value;
            else
                _start = currentValue();
            _target = value;
            _hasTarget = true;
            _hasStart = true;
            _anim.stop();
            _anim.setStartValue(QVariant::fromValue(_start));
            _anim.setEndValue(QVariant::fromValue(_target));
            _anim.setLoopCount(1);
            _anim.start();
        }
    }

    T currentValue() const {
        if (_anim.state() == QVariantAnimation::Running) {
            return _anim.currentValue().template value<T>();
        }
        return _target;
    }

    const T& target() const { return _target; }
    bool isRunning() const { return _anim.state() == QVariantAnimation::Running; }

    void stop() {
        _anim.stop();
        if (_hasTarget) _start = _target;
    }

protected:
    bool eventFilter(QObject* obj, QEvent* evt) override {
        if (evt->type() == QEvent::Hide)
            stop();
        return QObject::eventFilter(obj, evt);
    }

private:
    QVariantAnimation _anim;
    T _start{};
    T _target{};
    bool _hasStart{false};
    bool _hasTarget{false};
};

WidgetAnimationManager.h

#pragma once

#include "WidgetAnimation.h"

#include <QColor>
#include <QEasingCurve>
#include <unordered_map>
#include <optional>

class WidgetAnimator : public QObject {
public:
    explicit WidgetAnimator(QWidget* parent) : QObject(parent), _parent(parent) {}

    // ─── 颜色属性 ───
    QColor getBackgroundColor() const { return getBg().currentValue(); }
    void setBackgroundColor(const QColor& c) { getBg().setTarget(c); }
    void setBackgroundColorDuration(int ms) { getBg().setDuration(ms); }
    void setBackgroundColorEasing(const QEasingCurve& e) { getBg().setEasing(e); }

    QColor getBorderColor() const { return getBorder().currentValue(); }
    void setBorderColor(const QColor& c) { getBorder().setTarget(c); }
    void setBorderColorDuration(int ms) { getBorder().setDuration(ms); }
    void setBorderColorEasing(const QEasingCurve& e) { getBorder().setEasing(e); }

    // ─── 数值属性 ───
    qreal getFocusBorderProgress() const { return getFbProgress().currentValue(); }
    void setFocusBorderProgress(qreal v) { getFbProgress().setTarget(v); }
    void setFocusBorderProgressDuration(int ms) { getFbProgress().setDuration(ms); }
    void setFocusBorderProgressEasing(const QEasingCurve& e) { getFbProgress().setEasing(e); }

    qreal getProgress() const { return getPr().currentValue(); }
    void setProgress(qreal v) { getPr().setTarget(v); }
    void setProgressDuration(int ms) { getPr().setDuration(ms); }
    void setProgressEasing(const QEasingCurve& e) { getPr().setEasing(e); }

private:
    QWidget* _parent;

    // Lazy init helpers
    WidgetAnimation<QColor>& getBg() const {
        if (!_bg) _bg = std::make_unique<WidgetAnimation<QColor>>(_parent);
        return *_bg;
    }
    WidgetAnimation<QColor>& getBorder() const {
        if (!_border) _border = std::make_unique<WidgetAnimation<QColor>>(_parent);
        return *_border;
    }
    WidgetAnimation<qreal>& getFbProgress() const {
        if (!_fbProgress) _fbProgress = std::make_unique<WidgetAnimation<qreal>>(_parent);
        return *_fbProgress;
    }
    WidgetAnimation<qreal>& getPr() const {
        if (!_progress) _progress = std::make_unique<WidgetAnimation<qreal>>(_parent);
        return *_progress;
    }

    mutable std::unique_ptr<WidgetAnimation<QColor>> _bg;
    mutable std::unique_ptr<WidgetAnimation<QColor>> _border;
    mutable std::unique_ptr<WidgetAnimation<qreal>> _fbProgress;
    mutable std::unique_ptr<WidgetAnimation<qreal>> _progress;
};

class WidgetAnimationManager {
public:
    WidgetAnimationManager() = default;
    ~WidgetAnimationManager();

    bool enabled() const { return _enabled; }
    void setEnabled(bool on) { _enabled = on; if (!on) stopAll(); }

    // ─── 便捷动画 API ───
    QColor animateBackgroundColor(const QWidget* w,
        const QColor& target, int duration,
        QEasingCurve easing = QEasingCurve::OutCubic)
    {
        return animateProperty(w, target, duration, easing,
            [](WidgetAnimator* a, int d, QEasingCurve e) {
                a->setBackgroundColorDuration(d);
                a->setBackgroundColorEasing(e);
            },
            [](WidgetAnimator* a, const QColor& c) { a->setBackgroundColor(c); },
            [](WidgetAnimator* a) { return a->getBackgroundColor(); });
    }

    QColor animateBorderColor(const QWidget* w,
        const QColor& target, int duration,
        QEasingCurve easing = QEasingCurve::OutCubic)
    {
        return animateProperty(w, target, duration, easing,
            [](WidgetAnimator* a, int d, QEasingCurve e) {
                a->setBorderColorDuration(d);
                a->setBorderColorEasing(e);
            },
            [](WidgetAnimator* a, const QColor& c) { a->setBorderColor(c); },
            [](WidgetAnimator* a) { return a->getBorderColor(); });
    }

    qreal animateFocusBorderProgress(const QWidget* w,
        qreal target, int duration,
        QEasingCurve easing = QEasingCurve::Linear)
    {
        return animateProperty(w, target, duration, easing,
            [](WidgetAnimator* a, int d, QEasingCurve e) {
                a->setFocusBorderProgressDuration(d);
                a->setFocusBorderProgressEasing(e);
            },
            [](WidgetAnimator* a, qreal v) { a->setFocusBorderProgress(v); },
            [](WidgetAnimator* a) { return a->getFocusBorderProgress(); });
    }

    qreal animateProgress(const QWidget* w,
        qreal target, int duration,
        QEasingCurve easing = QEasingCurve::OutCubic)
    {
        return animateProperty(w, target, duration, easing,
            [](WidgetAnimator* a, int d, QEasingCurve e) {
                a->setProgressDuration(d);
                a->setProgressEasing(e);
            },
            [](WidgetAnimator* a, qreal v) { a->setProgress(v); },
            [](WidgetAnimator* a) { return a->getProgress(); });
    }

    void stopAll();

private:
    template<typename T>
    T animateProperty(const QWidget* w, const T& target, int duration,
        QEasingCurve easing,
        std::function<void(WidgetAnimator*, int, QEasingCurve)> setDurEase,
        std::function<void(WidgetAnimator*, const T&)> setTarget,
        std::function<T(WidgetAnimator*)> getValue)
    {
        if (!_enabled || !w) return target;
        auto* anim = getOrCreateAnimator(w);
        auto actualDuration = w->isEnabled() ? duration : 0;
        setDurEase(anim, actualDuration, easing);
        setTarget(anim, target);
        return getValue(anim);
    }

    WidgetAnimator* getOrCreateAnimator(const QWidget* w) {
        auto it = _map.find(w);
        if (it != _map.end()) return it->second;
        auto* anim = new WidgetAnimator(const_cast<QWidget*>(w));
        _map[w] = anim;
        QObject::connect(w, &QObject::destroyed, [this, w]() { removeWidget(w); });
        return anim;
    }

    void removeWidget(const QWidget* w) {
        auto it = _map.find(w);
        if (it != _map.end()) {
            it->second->deleteLater();
            _map.erase(it);
        }
    }

    bool _enabled{true};
    std::unordered_map<const QWidget*, WidgetAnimator*> _map;
};

inline WidgetAnimationManager::~WidgetAnimationManager() {
    for (auto& [w, anim] : _map) anim->deleteLater();
    _map.clear();
}

inline void WidgetAnimationManager::stopAll() {
    auto copy = _map;
    for (auto& [w, anim] : copy) {
        anim->deleteLater();
    }
    _map.clear();
}

2.6 Style 中如何使用

drawControl(CE_PushButton, ...) 中,Style 计算目标颜色后交给 Manager:

void MyStyle::drawControl(ControlElement ce, const QStyleOption* opt,
                          QPainter* p, const QWidget* w) const
{
    if (ce == CE_PushButton) {
        // 1. 计算目标背景色
        bool hovered = opt->state & State_MouseOver;
        bool pressed = opt->state & State_Sunken;
        QColor targetBg = pressed ? theme.pressedColor
                         : hovered ? theme.hoverColor
                         : theme.buttonColor;

        // 2. 通过 Manager 插值获取当前颜色
        QColor currentBg = _animMgr->animateBackgroundColor(
            w, targetBg, theme.animationDuration);

        // 3. 绘制
        p->setBrush(currentBg);
        p->setPen(Qt::NoPen);
        p->drawRoundedRect(opt->rect, theme.radius, theme.radius);
    }
}

每次 paintEvent 都调用 animateBackgroundColor()。Manager 内部会复用已有的动画对象,setTarget() 只在目标值变化时才重新启停动画。


3. 焦点动画

焦点动画的特殊之处在于:

  • 时间更长:hover 动画通常 128-192ms,焦点动画可以长到 384ms,让用户的余光自然捕捉到边框变化。
  • 缓动不同:焦点动画使用 Linear(见第 5 节讨论)或 OutBack(qlementine 实际用了带 overshoot 的 OutBack),避免过度吸引注意力。
  • 混合策略:不仅改变颜色,还可以同时改变边框宽度。

3.1 实现

// 在 drawControl(CE_PushButton, ...) 绘制焦点边框时
if (opt->state & State_HasFocus) {
    qreal targetProgress = 1.0;
    qreal currentProgress = _animMgr->animateFocusBorderProgress(
        w, targetProgress, theme.focusAnimationDuration);

    // 通过 alpha 混合焦点色和普通边框色
    QColor currentFocusColor;
    currentFocusColor.setRedF(
        theme.borderColor.redF() * (1.0 - currentProgress)
        + theme.focusColor.redF() * currentProgress);
    currentFocusColor.setGreenF(
        theme.borderColor.greenF() * (1.0 - currentProgress)
        + theme.focusColor.greenF() * currentProgress);
    currentFocusColor.setBlueF(
        theme.borderColor.blueF() * (1.0 - currentProgress)
        + theme.focusColor.blueF() * currentProgress);

    qreal borderWidth = currentProgress * theme.focusBorderWidth;
    p->setPen(QPen(currentFocusColor, borderWidth));
    p->setBrush(Qt::NoBrush);
    p->drawRoundedRect(focusRect, theme.radius, theme.radius);
}

3.2 关键决策:为什么用 Progress 而非直接动画 Color

你可能会问:为什么不直接 animateBorderColor(focusColor)

因为焦点动画同时影响两个参数:

  • 边框颜色(从 borderColor 渐变到 focusColor)
  • 边框宽度(从 0 渐变到 focusBorderWidth)

用一个 [0, 1] 的 progress 值,在 paint 内部做乘法,比维护两个独立动画更清晰、更不容易出现不同步。


4. Switch 控件动画

Switch(Toggle)的动画比按钮复杂得多——它有四个同时进行的动画:

动画 类型 变化范围 Easing
滑块位置 qreal (0→1) checked 状态决定 OutCubic
滑块填充色 QColor 关闭色→开启色 OutCubic
凹槽背景色 QColor 关闭背景→开启背景 OutCubic
凹槽边框色 QColor 普通边框→选中边框 OutCubic

4.1 为什么 Switch 自己管理动画而不是交给 WidgetAnimationManager?

WidgetAnimationManager 是 Style 的附属——它依附于 Style 的生命周期。但 Switch 是一个独立的自定义控件,它在 Style 不知情的情况下接收 mouse enter/leave/click 事件。

职责划分

  • Manager 管理Style 发起的动画(hover 颜色、焦点边框等)
  • Widget 内部的 QVariantAnimation 管理控件特有的动画(滑块位置、Switch 专属的颜色状态)

4.2 动画衔接策略

startAnimation() 的关键技巧:先读当前值作为起点,再设目标值

void Switch::startAnimation() {
    const auto duration = isVisible()
        ? style()->styleHint(QStyle::SH_Widget_Animation_Duration)
        : 0;

    // 关键:以动画当前值(而非目标值)作为新动画的起点
    const auto currentBg = _bgAnimation.currentValue();
    _bgAnimation.stop();
    _bgAnimation.setDuration(duration);
    _bgAnimation.setStartValue(currentBg);           // ← 当前插值位置
    _bgAnimation.setEndValue(getBgColor());           // ← 新目标
    _bgAnimation.start();

    // 滑块位置同理
    const auto currentX = _handleAnimation.currentValue();
    _handleAnimation.stop();
    _handleAnimation.setDuration(duration);
    _handleAnimation.setStartValue(currentX);
    _handleAnimation.setEndValue(
        checkState() == Qt::Checked ? 1.0
        : checkState() == Qt::Unchecked ? 0.0 : 0.5);
    _handleAnimation.start();
}

这意味着:即使用户在动画中途快速切换,滑块也会从当前位置平滑地转向新目标,而不是突兀地跳回起点。

4.3 完整 Switch 实现(~150 行)

#pragma once

#include <QAbstractButton>
#include <QVariantAnimation>
#include <QPainter>

class Switch : public QAbstractButton {
    Q_OBJECT
public:
    explicit Switch(QWidget* parent = nullptr)
        : QAbstractButton(parent)
    {
        setCheckable(true);
        setChecked(false);
        setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
        setFixedSize(44, 24);

        // 初始化所有动画 — 连接 valueChanged → update()
        auto connectAnim = [this](QVariantAnimation& anim) {
            QObject::connect(&anim, &QVariantAnimation::valueChanged,
                this, [this]() { update(); });
        };
        connectAnim(_handleAnim);
        connectAnim(_bgAnim);
        connectAnim(_borderAnim);
        connectAnim(_fgAnim);

        // 状态变化触发动画
        connect(this, &QAbstractButton::toggled,
                this, &Switch::startAnimation);
    }

    QSize sizeHint() const override { return {44, 24}; }

protected:
    void paintEvent(QPaintEvent*) override {
        QPainter p(this);
        p.setRenderHint(QPainter::Antialiasing);

        const int h = height();
        const int grooveH = h - 2;
        const int grooveY = (h - grooveH) / 2;
        const qreal radius = grooveH / 2.0;
        const QRectF grooveRect(0, grooveY, width(), grooveH);

        // ── 凹槽 ──
        QColor bgColor = _bgAnim.currentValue().value<QColor>();
        p.setPen(Qt::NoPen);
        p.setBrush(bgColor);
        p.drawRoundedRect(grooveRect, radius, radius);

        // 凹槽边框
        p.setBrush(Qt::NoBrush);
        p.setPen(QPen(_borderAnim.currentValue().value<QColor>(), 1.0));
        p.drawRoundedRect(grooveRect.adjusted(0.5,0.5,-0.5,-0.5), radius, radius);

        // ── 滑块 ──
        qreal ratio = _handleAnim.currentValue().toDouble();
        int pad = 2;
        int handleD = grooveH - pad * 2;
        qreal travel = width() - pad * 2 - handleD;
        qreal handleX = pad + travel * ratio;
        QRectF handleRect(handleX, grooveY + pad, handleD, handleD);
        p.setPen(Qt::NoPen);
        p.setBrush(_fgAnim.currentValue().value<QColor>());
        p.drawEllipse(handleRect);
    }

    void nextCheckState() override {
        setChecked(!isChecked());
    }

private:
    void startAnimation() {
        const int duration = isVisible() ? 192 : 0;
        const QEasingCurve curve(QEasingCurve::OutCubic);

        // 每个动画:读取当前值作为起点 → 设置新目标 → 启动
        auto restartAnim = [duration, curve](QVariantAnimation& anim, const QVariant& target) {
            QVariant start = anim.currentValue();
            anim.stop();
            anim.setDuration(duration);
            anim.setEasingCurve(curve);
            anim.setStartValue(start);
            anim.setEndValue(target);
            anim.start();
        };

        bool checked = isChecked();
        restartAnim(_handleAnim, checked ? 1.0 : 0.0);

        restartAnim(_bgAnim, QColor(checked ? "#34C759" : "#E5E5EA"));
        restartAnim(_borderAnim, QColor(checked ? "#34C759" : "#C6C6C8"));
        restartAnim(_fgAnim, QColor("#FFFFFF"));
    }

    QVariantAnimation _handleAnim;
    QVariantAnimation _bgAnim;
    QVariantAnimation _borderAnim;
    QVariantAnimation _fgAnim;
};

5. QEasingCurve 选择指南

Qt 提供 40+ 种缓动曲线,但实际 UI 动画中只会用到少数几种:

5.1 InOutQuad — Hover 转场

QEasingCurve(QEasingCurve::InOutQuad)
  • 使用场景:hover 颜色渐变(按钮、菜单项、列表行)
  • 特征:起点和终点都平缓过渡,中间线性。视觉上最"中性”——不快不慢,不突兀。
  • 时长:128-192ms

5.2 OutCubic — 点击/释放反馈

QEasingCurve(QEasingCurve::OutCubic)
  • 使用场景:press 背景色加深、Switch 滑块滑动、进度条填充
  • 特征:启动快,结束慢——符合物理世界"摩擦力减速"的直觉。滑块不会在终点"急刹车”。
  • 时长:192-256ms

5.3 Linear — 焦点动画

QEasingCurve(QEasingCurve::Linear)
  • 使用场景:焦点边框出现/消失
  • 特征:匀速变化,不吸引额外注意力。焦点动画应该"存在但不抢眼”,InOut/Out 的加速减速反而会让人注意到变化过程本身。
  • 时长:300-400ms

5.4 OutBack — 焦点动画(备选)

qlementine 实际使用了带 overshoot 的 OutBack:

_focusEasingCurve.setOvershoot(5.0);
_focusEasingCurve.setType(QEasingCurve::OutBack);

细微的 overshoot 给焦点渐变加了一点"弹性”,让聚焦/失焦的动作更有质感。

5.5 速查表

// 在每个 animate 调用中指定 easing
manager.animateBackgroundColor(w, target, duration,
    QEasingCurve::InOutQuad);   // hover
manager.animateBackgroundColor(w, target, duration,
    QEasingCurve::OutCubic);    // press
manager.animateFocusBorderProgress(w, progress, duration,
    QEasingCurve::Linear);      // focus

6. 统合练习:带 hover 动画的按钮

以下是一个完整可编译的 demo,展示 Style + Manager + Widget 三者的配合:

// ===== exercise.cpp — 完整可编译 =====
// g++ -std=c++17 exercise.cpp $(pkg-config --cflags --libs Qt6Widgets)

#include <QApplication>
#include <QCommonStyle>
#include <QPushButton>
#include <QVBoxLayout>
#include <QPainter>
#include <QEasingCurve>
#include <QTimer>
#include <unordered_map>
#include <optional>
#include <functional>
#include <memory>
#include <cassert>

// ═══════════════════════════════════════════════
//  WidgetAnimation template(来自第 2 节)
// ═══════════════════════════════════════════════
// (完整代码见上文 WidgetAnimation.h,此处省略以节省篇幅)

// ═══════════════════════════════════════════════
//  WidgetAnimationManager(来自第 2 节)
// ═══════════════════════════════════════════════
// (完整代码见上文 WidgetAnimationManager.h,此处省略以节省篇幅)

// ═══════════════════════════════════════════════
//  Theme 结构
// ═══════════════════════════════════════════════
struct Theme {
    QColor buttonColor      {"#007AFF"};
    QColor hoverColor       {"#0056CC"};
    QColor pressedColor     {"#004499"};
    QColor borderColor      {"#C6C6C8"};
    QColor focusColor       {"#007AFF"};
    QColor textColor        {"#FFFFFF"};
    int    animationDuration{192};
    int    focusAnimationDuration{384};
    qreal  radius{8.0};
    qreal  focusBorderWidth{3.0};
};

// ═══════════════════════════════════════════════
//  MyStyle — 使用 Manager 驱动动画
// ═══════════════════════════════════════════════
class MyStyle : public QCommonStyle {
public:
    MyStyle() = default;

    void drawControl(ControlElement ce, const QStyleOption* opt,
                     QPainter* p, const QWidget* w) const override
    {
        if (ce == CE_PushButton) {
            const auto* btnOpt = qstyleoption_cast<const QStyleOptionButton*>(opt);
            if (!btnOpt) return;

            bool hovered = opt->state & State_MouseOver;
            bool pressed = opt->state & State_Sunken;
            bool focused = opt->state & State_HasFocus;
            bool enabled = opt->state & State_Enabled;

            // ── 背景色动画 ──
            QColor targetBg = !enabled ? theme.buttonColor.lighter(150)
                            : pressed  ? theme.pressedColor
                            : hovered  ? theme.hoverColor
                            :             theme.buttonColor;

            QColor bg = _mgr.animateBackgroundColor(w, targetBg,
                theme.animationDuration, QEasingCurve::InOutQuad);

            // ── 绘制背景 ──
            p->setRenderHint(QPainter::Antialiasing);
            p->setPen(Qt::NoPen);
            p->setBrush(bg);
            auto r = opt->rect.adjusted(1, 1, -1, -1);
            p->drawRoundedRect(r, theme.radius, theme.radius);

            // ── 焦点动画 ──
            if (focused) {
                qreal progress = _mgr.animateFocusBorderProgress(w, 1.0,
                    theme.focusAnimationDuration, QEasingCurve::Linear);

                // 混合边框色
                QColor focusBorder;
                focusBorder.setRedF(
                    theme.borderColor.redF() * (1.0-progress)
                  + theme.focusColor.redF() * progress);
                focusBorder.setGreenF(
                    theme.borderColor.greenF() * (1.0-progress)
                  + theme.focusColor.greenF() * progress);
                focusBorder.setBlueF(
                    theme.borderColor.blueF() * (1.0-progress)
                  + theme.focusColor.blueF() * progress);

                qreal bw = progress * theme.focusBorderWidth;
                QRectF focusRect = r.adjusted(-bw/2, -bw/2, bw/2, bw/2);
                p->setBrush(Qt::NoBrush);
                p->setPen(QPen(focusBorder, bw));
                p->drawRoundedRect(focusRect, theme.radius + bw/2, theme.radius + bw/2);
            }

            // ── 文本 ──
            p->setPen(theme.textColor);
            p->setBrush(Qt::NoBrush);
            p->drawText(opt->rect, Qt::AlignCenter, btnOpt->text);
        } else {
            QCommonStyle::drawControl(ce, opt, p, w);
        }
    }

    void polish(QWidget* w) override {
        // 为按钮安装 WA_Hover 以接收鼠标悬浮事件
        if (qobject_cast<QPushButton*>(w)) {
            w->setAttribute(Qt::WA_Hover, true);
        }
        QCommonStyle::polish(w);
    }

private:
    Theme theme;
    mutable WidgetAnimationManager _mgr;
};

// ═══════════════════════════════════════════════
//  main
// ═══════════════════════════════════════════════
int main(int argc, char* argv[]) {
    QApplication app(argc, argv);

    app.setStyle(new MyStyle);

    auto* w = new QWidget;
    auto* lay = new QVBoxLayout(w);
    lay->setSpacing(16);

    auto* btn1 = new QPushButton("Hover Me");
    btn1->setMinimumSize(160, 48);
    lay->addWidget(btn1);

    auto* btn2 = new QPushButton("Click & Hold");
    btn2->setMinimumSize(160, 48);
    btn2->setCheckable(true);
    lay->addWidget(btn2);

    // 加上 Switch(来自第 4 节)
    auto* sw = new Switch;
    lay->addWidget(sw);

    w->resize(300, 200);
    w->show();

    return app.exec();
}

编译运行:

g++ -std=c++17 exercise.cpp $(pkg-config --cflags --libs Qt6Widgets)
./a.out

将光标移到按钮上——你会看到背景色从 #007AFF 平滑过渡到 #0056CC,鼠标移开后平滑回到原色。点击 Switch 的 toggle,滑块从一侧滑向另一侧。


7. 参考文件

文件 内容
lib/include/oclero/qlementine/animation/WidgetAnimation.hpp 类型安全的 QVariantAnimation 包装模板
lib/include/oclero/qlementine/animation/WidgetAnimator.hpp 单 widget 全部动画属性集合(宏展开)
lib/src/animation/WidgetAnimationManager.cpp 全局 manager:lifecycle、easing curves 初始化
lib/src/style/QlementineStyle.cpp 在各 drawControl() 中调用 animateXxx()
lib/src/widgets/Switch.cpp Switch 自管理 QVariantAnimation,展示滑块+颜色双重动画

小结

  1. WidgetAnimationManager 的核心职责是:接收目标值,返回当前插值。调用方不需要关心动画是否在运行、当前帧在什么位置。
  2. widget → Animator → Animation<T> 三层结构:Map 寻址 → 属性聚合 → 单值动画。每一层都有明确的职责边界。
  3. 焦点动画用 Progress 混合而非直接动画颜色——因为它同时影响颜色和宽度两个维度。
  4. Switch 自己管自己的动画——控件内部的 toggle/hover 逻辑 Manager 管不到,也不该管。
  5. restartIfNeeded() + 当前值作为起点是平滑中断的关键:多次快速切换不会造成视觉跳变。
  6. Easing 的选择是设计决策,不是数学题:InOutQuad 中性、OutCubic 有重量感、Linear 低调。

动画系统是整个 QStyle 框架的"最后一块拼图”。加上它之后,你的 Style 不再是静态的贴图——它活起来了。