Введение
Связь между QML-интерфейсом и логикой на C++ — основа практически любого нетривиального Qt Quick приложения. Механизм Q_PROPERTY позволяет объявить свойство C++-класса так, что оно становится доступным из QML как обычное свойство объекта, с поддержкой привязок (bindings) и уведомлений об изменении. В статье разберём, как правильно объявлять такие свойства, чтобы они работали в QML предсказуемо и эффективно.
Концепция
Q_PROPERTY объявляет имя свойства, тип, методы чтения (READ) и записи (WRITE), и, что критично для реактивности QML, сигнал уведомления (NOTIFY), который должен испускаться при каждом изменении значения. Без NOTIFY-сигнала привязки в QML, зависящие от свойства, не обновятся автоматически при его изменении из C++ — QML просто не узнает, что значение поменялось, кроме как при следующем «полном» пересчёте.
Для свойств, не предполагающих внешнего чтения списка значений напрямую, может быть полезен CONSTANT (свойство не меняется после создания объекта, что позволяет QML-движку оптимизировать привязки) или READONLY (есть getter, но нет setter, доступного из QML).
Пример кода: QML и C++
// usermodel.h
#pragma once
#include <QObject>
#include <QString>
class UserModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged)
Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged)
Q_PROPERTY(bool isAdult READ isAdult NOTIFY ageChanged) // вычисляемое readonly-свойство
public:
explicit UserModel(QObject *parent = nullptr) : QObject(parent) {}
QString userName() const { return m_userName; }
void setUserName(const QString &name)
{
if (m_userName == name) return;
m_userName = name;
emit userNameChanged();
}
int age() const { return m_age; }
void setAge(int age)
{
if (m_age == age) return;
m_age = age;
emit ageChanged(); // влияет и на isAdult, который зависит от age
}
bool isAdult() const { return m_age >= 18; }
signals:
void userNameChanged();
void ageChanged();
private:
QString m_userName;
int m_age = 0;
};
// main.cpp — регистрация типа и передача в QML
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "usermodel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
qmlRegisterType<UserModel>("App.Models", 1, 0, "UserModel");
QQmlApplicationEngine engine;
engine.load(QUrl("qrc:/main.qml"));
return app.exec();
}
// main.qml
import QtQuick
import QtQuick.Controls
import App.Models 1.0
ApplicationWindow {
visible: true
width: 300
height: 200
UserModel {
id: user
userName: "Алексей"
age: 25
}
Column {
anchors.centerIn: parent
spacing: 10
Text { text: "Имя: " + user.userName }
Text { text: "Возраст: " + user.age }
Text { text: user.isAdult ? "Взрослый" : "Несовершеннолетний" }
Button {
text: "Увеличить возраст"
onClicked: user.age++
}
}
}
Пояснения к коду
Свойство isAdult не имеет собственного WRITE-метода и помечено как зависящее от сигнала ageChanged в NOTIFY — это типичный паттерн вычисляемого свойства: его значение производно от другого свойства (age), и переиспользование того же сигнала уведомления гарантирует, что QML пересчитает привязки, зависящие от isAdult, при каждом изменении age, даже хотя формально это «другое» свойство.
Регистрация через qmlRegisterType<UserModel> делает класс доступным как QML-тип с указанным именем и версией модуля — после этого UserModel можно создавать прямо в QML-дереве, как сделано в main.qml, и привязывать к его свойствам обычным образом, включая двустороннюю реакцию через onClicked: user.age++.
Подводные камни
- Забытый
NOTIFY-сигнал делает свойство «недвижимым» в глазах QML-привязок. Свойство технически можно прочитать, но привязкаText { text: user.someProperty }не обновится автоматически при изменении значения из C++ — это одна из самых частых причин «почему UI не обновляется» в смешанных QML/C++ проектах. - Изменение значения свойства без проверки на равенство перед эмиссией сигнала (
if (m_userName == name) return;в примере) приводит к лишним пересчётам привязок и потенциальным бесконечным циклам обновления при взаимно зависимых свойствах — проверка на отсутствие реального изменения должна быть стандартной практикой в каждом setter’е. - Использование сырых указателей на QObject, передаваемых в QML без управления владением. Если C++ передаёт указатель на объект через
setContextProperty()или возврат из функции, и затем C++ удаляет этот объект, QML может обратиться к уже разрушенному объекту — нужно либо гарантировать время жизни через parent-child иерархию, либо использоватьQQmlEngine::setObjectOwnership()для явного указания, кто отвечает за удаление. - Регистрация типа после загрузки QML-файла, использующего этот тип.
qmlRegisterTypeдолжен быть вызван доengine.load()— иначе движок не найдёт тип при разборе QML и выдаст ошибку о неизвестном компоненте.