Why Animate in Qt?
Smooth animations make embedded HMIs and desktop apps feel professional. Qt’s animation framework works by animating Qt properties (Q_PROPERTY) over time using easing curves.
QAbstractAnimation
├── QVariantAnimation
│ └── QPropertyAnimation ← animates Q_PROPERTY values
├── QPauseAnimation ← delay in a sequence
└── QAnimationGroup
├── QSequentialAnimationGroup ← run one after another
└── QParallelAnimationGroup ← run all simultaneously
QPropertyAnimation — Basics
#include <QPropertyAnimation>
QPushButton *btn = new QPushButton("Connect", this);
btn->setGeometry(0, 200, 120, 40);
// Animate the geometry property
QPropertyAnimation *anim = new QPropertyAnimation(btn, "geometry");
anim->setDuration(600); // 600 ms
anim->setStartValue(QRect(0, 200, 120, 40));
anim->setEndValue (QRect(200, 200, 120, 40));
anim->setEasingCurve(QEasingCurve::OutCubic);
anim->start(QAbstractAnimation::DeleteWhenStopped); // auto-delete
Common Animatable Properties
| Widget | Property | Type |
|---|---|---|
| QWidget | geometry | QRect |
| QWidget | pos | QPoint |
| QWidget | size | QSize |
| QWidget | windowOpacity | qreal (0.0–1.0) |
| QGraphicsItem | pos | QPointF |
| Custom | Any Q_PROPERTY | Any variant type |
Easing Curves
// Linear — constant speed
anim->setEasingCurve(QEasingCurve::Linear);
// Ease in/out (smooth)
anim->setEasingCurve(QEasingCurve::InOutQuad);
// Bounce at end
anim->setEasingCurve(QEasingCurve::OutBounce);
// Elastic overshoot
anim->setEasingCurve(QEasingCurve::OutElastic);
// Back (slight overshoot)
anim->setEasingCurve(QEasingCurve::OutBack);
Easing Curve Shapes:
Linear: ──────────────────
InOutQuad: ──╱────────────╲──
OutBounce: ────────────╱╲╱╲──
OutElastic: ─────────────╱~╲─
OutBack: ──────────────╱─╲
Sequential Animation Group
QSequentialAnimationGroup *seq = new QSequentialAnimationGroup(this);
// 1) Fade in
QPropertyAnimation *fadeIn = new QPropertyAnimation(widget, "windowOpacity");
fadeIn->setDuration(300);
fadeIn->setStartValue(0.0);
fadeIn->setEndValue(1.0);
// 2) Pause 500ms
QPauseAnimation *pause = new QPauseAnimation(500);
// 3) Slide right
QPropertyAnimation *slide = new QPropertyAnimation(widget, "pos");
slide->setDuration(400);
slide->setStartValue(QPoint(0, 100));
slide->setEndValue (QPoint(300, 100));
slide->setEasingCurve(QEasingCurve::OutQuart);
seq->addAnimation(fadeIn);
seq->addAnimation(pause);
seq->addAnimation(slide);
seq->start();
connect(seq, &QSequentialAnimationGroup::finished, [](){
qDebug() << "Animation sequence complete";
});
Parallel Animation Group
QParallelAnimationGroup *par = new QParallelAnimationGroup(this);
// Both run at the same time
QPropertyAnimation *grow = new QPropertyAnimation(btn, "size");
grow->setDuration(500);
grow->setStartValue(QSize(80, 30));
grow->setEndValue (QSize(160, 60));
grow->setEasingCurve(QEasingCurve::OutQuint);
QPropertyAnimation *brighten = new QPropertyAnimation(btn, "windowOpacity");
brighten->setDuration(500);
brighten->setStartValue(0.3);
brighten->setEndValue(1.0);
par->addAnimation(grow);
par->addAnimation(brighten);
par->start();
Animating Custom Properties
class StatusIndicator : public QWidget {
Q_OBJECT
Q_PROPERTY(float glow READ glow WRITE setGlow NOTIFY glowChanged)
float m_glow = 0.0f;
public:
float glow() const { return m_glow; }
void setGlow(float g) { m_glow = g; emit glowChanged(g); update(); }
signals:
void glowChanged(float g);
void pulseAnimate() {
QPropertyAnimation *pulse = new QPropertyAnimation(this, "glow");
pulse->setDuration(800);
pulse->setStartValue(0.0f);
pulse->setEndValue(1.0f);
pulse->setEasingCurve(QEasingCurve::InOutSine);
pulse->setLoopCount(-1); // infinite
pulse->start();
}
protected:
void paintEvent(QPaintEvent *) override {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
QRadialGradient grad(rect().center(), width() / 2.0);
grad.setColorAt(0.0, QColor(166, 227, 161, static_cast<int>(255 * m_glow)));
grad.setColorAt(1.0, Qt::transparent);
p.fillRect(rect(), grad);
p.setBrush(QColor("#a6e3a1"));
p.setPen(Qt::NoPen);
p.drawEllipse(rect().center(), 8, 8);
}
};
Visual: Animation Timeline
QSequentialAnimationGroup (total: 1200ms)
│
├─ fadeIn [0ms ─────────── 300ms]
│
├─ pause [300ms ─────────── 800ms]
│
└─ slide [800ms ─────────── 1200ms]
QParallelAnimationGroup (total: 500ms — max of children)
│
├─ grow [0ms ─────────── 500ms]
└─ brighten [0ms ─────────── 500ms]
Lab 1 — Animated Notification Toast
class Toast : public QLabel {
Q_OBJECT
public:
static void show(QWidget *parent, const QString &msg,
int durationMs = 2500)
{
auto *toast = new Toast(msg, parent);
toast->show();
auto *seq = new QSequentialAnimationGroup(toast);
// Slide in from bottom
auto *slideIn = new QPropertyAnimation(toast, "pos");
slideIn->setDuration(300);
slideIn->setStartValue(QPoint(20, parent->height()));
slideIn->setEndValue (QPoint(20, parent->height() - 60));
slideIn->setEasingCurve(QEasingCurve::OutCubic);
// Wait
seq->addAnimation(slideIn);
seq->addAnimation(new QPauseAnimation(durationMs));
// Fade out
auto *fadeOut = new QPropertyAnimation(toast, "windowOpacity");
fadeOut->setDuration(300);
fadeOut->setStartValue(1.0);
fadeOut->setEndValue(0.0);
seq->addAnimation(fadeOut);
QObject::connect(seq, &QSequentialAnimationGroup::finished,
toast, &QObject::deleteLater);
seq->start();
}
private:
Toast(const QString &msg, QWidget *parent)
: QLabel(msg, parent)
{
setStyleSheet("background:#313244; color:#cdd6f4; "
"border-radius:8px; padding:8px 16px;");
adjustSize();
setWindowOpacity(1.0);
}
};
// Usage
QPushButton *btn = new QPushButton("Connect");
connect(btn, &QPushButton::clicked, [=](){
Toast::show(this, "Device connected successfully!");
});
Lab 2 — Loading Spinner
class Spinner : public QWidget {
Q_OBJECT
Q_PROPERTY(float angle READ angle WRITE setAngle)
float m_angle = 0.0f;
public:
Spinner(QWidget *p = nullptr) : QWidget(p) {
setFixedSize(48, 48);
QPropertyAnimation *spin = new QPropertyAnimation(this, "angle");
spin->setDuration(1000);
spin->setStartValue(0.0f);
spin->setEndValue(360.0f);
spin->setLoopCount(-1);
spin->start();
}
float angle() const { return m_angle; }
void setAngle(float a) { m_angle = a; update(); }
protected:
void paintEvent(QPaintEvent *) override {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.translate(width()/2, height()/2);
p.rotate(m_angle);
int n = 8;
for (int i = 0; i < n; ++i) {
float alpha = 255 * (i + 1) / n;
p.setPen(QPen(QColor(137, 180, 250, static_cast<int>(alpha)), 3,
Qt::SolidLine, Qt::RoundCap));
p.drawLine(0, 8, 0, 16);
p.rotate(360.0 / n);
}
}
};
Interview Questions
Q1: What types can be animated with QPropertyAnimation?
Any Qt property type that is registered with
Q_PROPERTYand is aQVariant-compatible type:int,float,double,QRect,QPoint,QSize,QColor,qreal, etc. Custom types need a registered interpolation function.
Q2: What happens when you call start() on an already-running animation?
The animation stops and restarts from the beginning (or current start value). To resume from the current position, use
pause()andresume().
Q3: How do you make an animation repeat forever?
Call
setLoopCount(-1). UsesetLoopCount(n)for a specific number of repeats.
Q4: What is the difference between QSequentialAnimationGroup and QParallelAnimationGroup?
Sequential runs animations one after another. Parallel runs them simultaneously. You can nest them — e.g., a sequential group containing a parallel group.
Q5: How do you animate a custom property (not a built-in one)?
Declare it with
Q_PROPERTYin aQObjectsubclass, providingREADandWRITEaccessors. TheWRITEslot should callupdate()to repaint if it affects appearance.QPropertyAnimationwill then interpolate the property automatically.
Application: Animated HMI Dashboard
class AnimatedDashboard : public QWidget {
Q_OBJECT
public:
AnimatedDashboard(QWidget *p = nullptr) : QWidget(p) {
setWindowTitle("Animated Dashboard");
resize(800, 500);
setStyleSheet("background:#1e1e2e;");
auto *lay = new QHBoxLayout(this);
// 4 gauge cards that animate on startup
QList<QWidget*> cards;
for (auto &title : QStringList{"Temp", "Voltage", "Humidity", "Pressure"}) {
auto *card = new QFrame;
card->setStyleSheet(
"QFrame{ background:#313244; border-radius:12px; }");
card->setFixedSize(160, 200);
auto *cardLay = new QVBoxLayout(card);
auto *lbl = new QLabel(title);
lbl->setAlignment(Qt::AlignCenter);
lbl->setStyleSheet("color:#cdd6f4; font-size:14px;");
auto *spinner = new Spinner(card);
cardLay->addWidget(spinner, 0, Qt::AlignCenter);
cardLay->addWidget(lbl);
lay->addWidget(card);
cards.append(card);
}
// Stagger entry animations
for (int i = 0; i < cards.size(); ++i) {
auto *card = cards[i];
card->setWindowOpacity(0.0);
QTimer::singleShot(i * 150, [card, i](){
auto *fadeIn = new QPropertyAnimation(card, "windowOpacity");
fadeIn->setDuration(400);
fadeIn->setStartValue(0.0);
fadeIn->setEndValue(1.0);
fadeIn->setEasingCurve(QEasingCurve::OutCubic);
auto *riseUp = new QPropertyAnimation(card, "pos");
riseUp->setDuration(400);
riseUp->setStartValue(card->pos() + QPoint(0, 40));
riseUp->setEndValue(card->pos());
riseUp->setEasingCurve(QEasingCurve::OutCubic);
auto *par = new QParallelAnimationGroup;
par->addAnimation(fadeIn);
par->addAnimation(riseUp);
par->start(QAbstractAnimation::DeleteWhenStopped);
});
}
}
};
References
| Resource | Link |
|---|---|
| Qt Docs: Animation Framework | https://doc.qt.io/qt-6/animation-overview.html |
| Qt Docs: QPropertyAnimation | https://doc.qt.io/qt-6/qpropertyanimation.html |
| Qt Docs: QEasingCurve | https://doc.qt.io/qt-6/qeasingcurve.html |
| Qt Docs: QAnimationGroup | https://doc.qt.io/qt-6/qanimationgroup.html |
Next tutorial → Qt Styling & Themes