Введение
QSS поддерживает базовые CSS-подобные transition-свойства для плавного изменения некоторых параметров при смене состояния виджета, но поддержка анимаций в чистом QSS ограничена. В статье разберём, как комбинировать QSS-анимации с программными анимациями через QPropertyAnimation для создания более выразительных переходов между визуальными состояниями виджетов.
Концепция
QSS поддерживает свойство transition для плавного изменения некоторых свойств (в первую очередь — цветов фона/границы) при смене псевдосостояния, но эта поддержка значительно более ограничена, чем в полноценном CSS — не все свойства поддерживают плавный переход, и сложные, нелинейные анимации недостижимы только через QSS. Для более выразительных переходов (изменение размера, позиции, прозрачности с кастомной кривой анимации) используется QPropertyAnimation, работающий поверх обычных Qt-свойств виджета, что комбинируется с базовым QSS-стилем для статичного оформления.
Пример кода: Анимации переходов состояний
app.setStyleSheet(R"(
QPushButton {
background-color: #2563eb;
border-radius: 6px;
color: white;
padding: 10px 20px;
}
/* Базовая поддержка transition в QSS — плавное изменение цвета фона при hover */
QPushButton:hover {
background-color: #1d4ed8;
}
)");
// animatedtogglebutton.h — программная анимация позиции "ползунка" переключателя
// через QPropertyAnimation, скомбинированная с QSS для статичного оформления
#pragma once
#include <QWidget>
#include <QPropertyAnimation>
#include <QPainter>
#include <QMouseEvent>
class AnimatedToggleSwitch : public QWidget
{
Q_OBJECT
Q_PROPERTY(qreal handlePosition READ handlePosition WRITE setHandlePosition)
public:
explicit AnimatedToggleSwitch(QWidget *parent = nullptr) : QWidget(parent)
{
setFixedSize(50, 26);
m_animation = new QPropertyAnimation(this, "handlePosition", this);
m_animation->setDuration(200);
m_animation->setEasingCurve(QEasingCurve::OutCubic); // нелинейная кривая —
// недостижимо средствами одного QSS
}
qreal handlePosition() const { return m_handlePosition; }
void setHandlePosition(qreal position)
{
m_handlePosition = position;
update(); // запрос перерисовки при каждом шаге анимации
}
bool isChecked() const { return m_checked; }
protected:
void mousePressEvent(QMouseEvent *event) override
{
Q_UNUSED(event);
m_checked = !m_checked;
m_animation->stop();
m_animation->setStartValue(m_handlePosition);
m_animation->setEndValue(m_checked ? 1.0 : 0.0);
m_animation->start();
emit toggled(m_checked);
}
void paintEvent(QPaintEvent *event) override
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
// Фон переключателя плавно меняет цвет в зависимости от текущей позиции ползунка
const QColor backgroundColor = lerpColor(QColor("#d1d5db"), QColor("#2563eb"), m_handlePosition);
painter.setPen(Qt::NoPen);
painter.setBrush(backgroundColor);
painter.drawRoundedRect(rect(), height() / 2.0, height() / 2.0);
// Позиция ползунка вычисляется из анимируемого свойства handlePosition (0.0–1.0)
const double handleX = 3 + m_handlePosition * (width() - height());
painter.setBrush(Qt::white);
painter.drawEllipse(QRectF(handleX, 3, height() - 6, height() - 6));
}
signals:
void toggled(bool checked);
private:
static QColor lerpColor(const QColor &from, const QColor &to, qreal t)
{
return QColor::fromRgb(
from.red() + (to.red() - from.red()) * t,
from.green() + (to.green() - from.green()) * t,
from.blue() + (to.blue() - from.blue()) * t);
}
qreal m_handlePosition = 0.0;
bool m_checked = false;
QPropertyAnimation *m_animation;
};
Пояснения к коду
Q_PROPERTY(qreal handlePosition ...) объявляет анимируемое Qt-свойство — именно через эту систему свойств QPropertyAnimation способен плавно изменять значение от начального до конечного, вызывая setHandlePosition() на каждом шаге анимации (определяемом частотой обновления и заданной длительностью). setEasingCurve(QEasingCurve::OutCubic) задаёт нелинейную, «пружинящую» кривую изменения значения во времени — переключатель быстро начинает движение и плавно замедляется к концу, что выглядит заметно более естественно, чем линейное движение, и недостижимо средствами одного только базового QSS transition.
paintEvent() использует текущее значение m_handlePosition (изменяемое анимацией) для вычисления как цвета фона (через линейную интерполяцию lerpColor), так и позиции самого ползунка — поскольку оба визуальных аспекта зависят от одного и того же анимируемого значения, они естественным образом синхронизированы между собой без необходимости в отдельной координации.
Подводные камни
- QSS
transitionподдерживается ограниченным набором свойств и стилей, и попытка положиться только на него для сложных, многоступенчатых анимаций обычно заканчивается разочарованием в возможностях — для чего-то более выразительного, чем простое изменение цвета при hover, практически всегда нуженQPropertyAnimationили ручная отрисовка черезpaintEvent(), как в примере с переключателем. - Конфликт между QSS-стилизацией виджета и кастомной отрисовкой через
paintEvent()— если виджет одновременно имеет применённый QSS (например, заданный фон черезbackground-color) и переопределённыйpaintEvent(), который сам рисует фон, может произойти неожиданное визуальное наложение или, наоборот, один из способов окажется проигнорирован — для виджетов с полностью кастомной отрисовкой (какAnimatedToggleSwitch) обычно не применяют QSS к самому этому виджету вовсе, оставляя всю стилизацию полностью внутриpaintEvent(). - Забытый вызов
update()после изменения анимируемого свойства — без этого вызова Qt не узнает, что виджет нуждается в перерисовке, и анимация, формально продолжающая изменять значение свойства, не будет видна на экране; в примереsetHandlePosition()явно вызываетupdate()именно по этой причине, и пропуск этого шага — частая причина «анимации, которая просто не анимируется визуально». - Слишком частые вызовы
update()во время анимации могут создавать заметную нагрузку на CPU/GPU, особенно при множестве одновременно анимируемых виджетов на экране — для embedded-устройств со слабым железом (основной цикл статей, статья 242) стоит ограничивать количество и сложность одновременных программных анимаций, выбирая, действительно ли конкретный визуальный эффект оправдывает связанные с ним накладные расходы на рендеринг.