Заметки по языку с и с++

Q_PROPERTY на практике: связывание свойств C++ с UI без шаблонного кода

Введение

Помимо базового использования Q_PROPERTY для экспорта свойств в QML (рассмотренного в отдельной статье про связку QML и C++), система свойств Qt полезна и в чисто C++-коде Qt Widgets: она позволяет анимировать произвольные свойства через QPropertyAnimation, сериализовать состояние объекта без написания индивидуального кода для каждого поля, и единообразно обращаться к разным типам объектов через общий интерфейс setProperty()/property(). В статье разберём практическое применение Q_PROPERTY за пределами QML.

Концепция

QPropertyAnimation — класс для анимации значения произвольного Q_PROPERTY от начального к конечному значению за заданное время, с поддержкой кривых сглаживания (easing curves) — он работает с любым свойством, удовлетворяющим минимальным требованиям (наличие READ/WRITE, тип, поддерживаемый QVariant), без необходимости писать собственный код анимации для каждого конкретного случая. Это превращает Q_PROPERTY в универсальный «крюк» для анимационной инфраструктуры Qt, применимый одинаково и к встроенным свойствам виджетов (geometry, windowOpacity), и к собственным пользовательским свойствам.

Пример кода: Q_PROPERTY на практике

// fadingwidget.h
#pragma once
#include <QWidget>
#include <QPainter>

class FadingWidget : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity)

public:
    explicit FadingWidget(QWidget *parent = nullptr) : QWidget(parent) {}

    qreal opacity() const { return m_opacity; }
    void setOpacity(qreal opacity)
    {
        m_opacity = opacity;
        update();
    }

protected:
    void paintEvent(QPaintEvent *) override
    {
        QPainter painter(this);
        painter.setOpacity(m_opacity);
        painter.fillRect(rect(), Qt::blue);
    }

private:
    qreal m_opacity = 1.0;
};
// Использование QPropertyAnimation для анимации кастомного свойства
#include "fadingwidget.h"
#include <QPropertyAnimation>
#include <QSequentialAnimationGroup>
#include <QApplication>

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

    FadingWidget widget;
    widget.resize(300, 200);
    widget.show();

    auto *fadeOut = new QPropertyAnimation(&widget, "opacity");
    fadeOut->setDuration(800);
    fadeOut->setStartValue(1.0);
    fadeOut->setEndValue(0.1);
    fadeOut->setEasingCurve(QEasingCurve::OutQuad);

    auto *fadeIn = new QPropertyAnimation(&widget, "opacity");
    fadeIn->setDuration(800);
    fadeIn->setStartValue(0.1);
    fadeIn->setEndValue(1.0);
    fadeIn->setEasingCurve(QEasingCurve::InQuad);

    auto *group = new QSequentialAnimationGroup(&widget);
    group->addAnimation(fadeOut);
    group->addAnimation(fadeIn);
    group->setLoopCount(-1); // бесконечное повторение
    group->start();

    return app.exec();
}

Пояснения к коду

FadingWidget объявляет кастомное свойство opacity через Q_PROPERTY, не имеющее ничего общего со встроенным QWidget::windowOpacity — это полностью пользовательское свойство, влияющее на прозрачность отрисовки внутри собственного paintEvent(). Конструктор QPropertyAnimation(&widget, "opacity") принимает целевой объект и имя свойства как строку — именно через эту строку и метаобъектную систему (описанную в одной из предыдущих статей) QPropertyAnimation находит и вызывает setOpacity() на каждом кадре анимации с промежуточным интерполированным значением.

QSequentialAnimationGroup объединяет две анимации (затухание и появление) в последовательность, а setLoopCount(-1) заставляет всю последовательность бесконечно повторяться — это даёт эффект пульсации без единой строчки кода, вычисляющего кадры вручную или используемого QTimer напрямую.

Подводные камни

  • QPropertyAnimation молча не работает при опечатке в имени свойства. Поскольку имя свойства передаётся как строка ("opacity"), опечатка ("opacty") не обнаруживается компилятором — анимация просто не будет иметь видимого эффекта, и единственный способ обнаружить проблему — явная проверка через отладку или вывод предупреждения Qt в рантайме о ненайденном свойстве.
  • Тип свойства должен поддерживаться QVariant и быть интерполируемым. QPropertyAnimation умеет интерполировать стандартные числовые и геометрические типы «из коробки», но для пользовательских типов нужна явная регистрация интерполятора через qRegisterAnimationInterpolator() — без неё анимация для нестандартного типа просто «перескакивает» между значениями без плавного перехода.
  • Изменение свойства напрямую (не через анимацию) во время выполнения активной анимации того же свойства приводит к конфликту — оба источника пытаются управлять одним и тем же значением, и результат становится непредсказуемым в зависимости от порядка операций в каждом кадре event loop.
  • Память, занимаемая объектами анимации, созданными через new без явного родителя или владения. Если объект анимации не привязан к родителю через конструктор (как в примере, где &widget передаётся как родитель) и не удаляется вручную, он может остаться висеть в памяти после завершения — Qt-анимации с DeleteWhenStopped (флаг, передаваемый в start()) автоматически удаляют себя по завершении, что часто удобнее ручного управления.