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,展示滑块+颜色双重动画 |
小结
- WidgetAnimationManager 的核心职责是:接收目标值,返回当前插值。调用方不需要关心动画是否在运行、当前帧在什么位置。
widget → Animator → Animation<T>三层结构:Map 寻址 → 属性聚合 → 单值动画。每一层都有明确的职责边界。- 焦点动画用 Progress 混合而非直接动画颜色——因为它同时影响颜色和宽度两个维度。
- Switch 自己管自己的动画——控件内部的 toggle/hover 逻辑 Manager 管不到,也不该管。
restartIfNeeded()+ 当前值作为起点是平滑中断的关键:多次快速切换不会造成视觉跳变。- Easing 的选择是设计决策,不是数学题:InOutQuad 中性、OutCubic 有重量感、Linear 低调。
动画系统是整个 QStyle 框架的"最后一块拼图”。加上它之后,你的 Style 不再是静态的贴图——它活起来了。