- 1. Container.h
- 2. Container.cpp
- 3. Фабрика объектов
- 1. Factory.h
- 2. Factory.cpp
- 4. main.cpp
- 5. main.qml
Один из несомненных плюсов QML в Qt состоит в том, что он позволяет достаточно резко отделять backend-логику от интерфейса приложения. То есть весь backend мы пишем на C++, а в QML лишь отображаем необходимый результат.
При этом мы также можем и внутреннюю логику написать чисто на QML, минимизировав наличие C++ кода, или наоборот прописать дизайн некоторых элементов в C++, воспользовавшись возможностями OpenGL . Несмотря на разграничение QML и C++ частей кода на backend и frontend приложения, мы не имеем каких-либо больших ограничений.
Но иногда возникает необходимость передать некоторую структуру данных, которая будет нести несколько полей с информацией, в QML часть кода. Например такую структуру.
- struct Structure
- {
- int m_number;
- QString m_message;
- };
Просто так такую структуру не получиться передать в QML и первое, что приходит на ум, это сделать сигнал sendToQml , который будет высылать несколько аргументов. Каждый аргумент будет отвечать за определённое поле структуры.
- void sendToQml(int number, QString message);
Этот сигнал будет располагаться в вашем классе, который будет отправлять в QML определённую информацию с номерами, сообщениями и т.д. Минус подобного подхода очевиден, поскольку таких полей может быть очень много и будет глупо делать сигнал с парой десятков аргументов. Подобный подход будет полезен для пересылки небольшой информации, но никак не для структур данных.
Есть ещё вариант использовать QVariantMap. Поскольку он хорошо конвертируется в массив QML, из которого можно забирать данные по ключу. Пример использования есть официальной документации Qt.
Таким образом забирается информация в QML
- // MyItem.qml
- Item {
- function readValues(anArray, anObject) {
- for (var i=0; i<anArray.length; i++)
- console.log("Array item:", anArray[i])
- for (var prop in anObject) {
- console.log("Object item:", prop, "=", anObject[prop])
- }
- }
- }
А таким образом эта информация добавляется в C++
- // C++
- QQuickView view(QUrl::fromLocalFile("MyItem.qml"));
- QVariantList list;
- list << 10 << QColor(Qt::green) << "bottles";
- QVariantMap map;
- map.insert("language", "QML");
- map.insert("released", QDate(2010, 9, 21));
- QMetaObject::invokeMethod(view.rootObject(), "readValues",
- Q_ARG(QVariant, QVariant::fromValue(list)),
- Q_ARG(QVariant, QVariant::fromValue(map)));
Но мне этот подход не особо нравится, поскольку может возникнуть ситуация, когда нужно сделать определённые действия с данными, которые несёт в себе структура, а QVariantMap, таких методов не может иметь в принципе. Да и хочется более целостного представления информации, чем набор ключей и значений. Если это какое-то сообщение или объект данных, который передаётся в рамках C++ кода и работа происходит с ним как с целостным объектом, то почему мы должны превращать его в набор пар ключ-значение, которые живут внутри QVariantMap?
Правильно, это необязательно. Дело в том, что из C++ можно передавать в QML указатель на объект класса QObject . А учитывая возможности полиморфизма , наследования и т.д. можно наследоваться от класса QObject и передать указатель на этот объект в QML в качестве указателя на базовый класс QObject . Причём, даже не регистрируя этот новый класс в качестве MetaType в мета-объектной системе Qt .
Что это нам даст? Мы понимаем, что если наследоваться от какого-то класса и добавить в наследованный класса какие-то методы и поля, то с ними можно будет работать и вызывать их, но при этом нужно будет иметь указатель на объект соответствующего класса, то есть если мы возьмём указатель на базовый класс и присвоим ему указатель на наследованный класс, то в рамках кода мы не сможем записать вызов метода наследованного класса при работе с указателем базового класса. То есть следующий код не скомпилируется.
- class A
- {
- public:
- A() {}
- };
- class B : public A
- {
- public:
- B() {}
- void someMethod();
- };
- int main(int argc, char *argv[])
- {
- A *a = new B(); // Ok. Можно присвоить указатель на наследованный класс указателю на базовый класс
- a->someMethod(); // Ошибка. А вот вызвать метод наследованного класса уже нельзя, базовый класс об это ничего не знает
- return 0;
- }
И по логике, нет никакого смысла наследоваться от QObject без регистрации класса через qmlRegisterType. Однако здесь всё не так, как мы привыкли. Дело в том, что если метод помечен как сигнал , слот или Q_INVOKABLE метод или какое-то из полей помечено как Q_PROPERTY , то мета-объектная система Qt уже знает об этом методе или поле. И метод, например, можно вызвать через статический метод класса QMetaObject. За счёт этого эти методы можно вызывать в QML, даже, если будет иметься указатель на базовый класс QObject.
А теперь посмотрим на пример структуры и класса, которые наследованы от QObject, и которые будут передаваться в QML.
Container.h
- #ifndef CONTAINER_H
- #define CONTAINER_H
- #include <QObject>
- struct Structure : public QObject
- {
- explicit Structure(QObject *parent = nullptr);
- int m_number;
- QString m_message;
- private:
- Q_OBJECT
- // Сможем обращаться к полям из QML
- // параметр MEMBER указывает, что имеется возможность работать с этим полем и отслеживать его изменение в QML
- Q_PROPERTY(int number MEMBER m_number)
- Q_PROPERTY(QString message MEMBER m_message)
- public:
- // А также вызвать в QML этот метод
- Q_INVOKABLE QString getFullInfo() const;
- };
- class Container : public QObject
- {
- Q_OBJECT
- // Сможем обращаться к полям из QML
- Q_PROPERTY(int number READ number WRITE setNumber NOTIFY numberChanged)
- Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged)
- public:
- explicit Container(QObject *parent = nullptr);
- // А также вызвать в QML этот метод
- Q_INVOKABLE QString getFullInfo() const;
- int number() const;
- QString message() const;
- public slots:
- void setNumber(int number);
- void setMessage(QString message);
- signals:
- void numberChanged(int number);
- void messageChanged(QString message);
- private:
- int m_number;
- QString m_message;
- };
- #endif // CONTAINER_H
Здесь мы видим классический вариант объявления полей и методов для кастомного объекта, с которым будем работать в QML . То есть имеются макросы Q_PROPERTY и Q_INVOKABLE , а также имеются сигналы и слоты.
Container.cpp
Содержимое файла исходных кодов вообще не должно вызываться никаких вопросов.
- #include "Container.h"
- Structure::Structure(QObject *parent) : QObject(parent)
- {
- }
- QString Structure::getFullInfo() const
- {
- return QString("Full information from Structure %1").arg(m_number);
- }
- Container::Container(QObject *parent) : QObject(parent)
- {
- }
- QString Container::getFullInfo() const
- {
- return QString("Full information from Container %1").arg(m_number);
- }
- int Container::number() const
- {
- return m_number;
- }
- QString Container::message() const
- {
- return m_message;
- }
- void Container::setNumber(int number)
- {
- if (m_number == number)
- return;
- m_number = number;
- emit numberChanged(m_number);
- }
- void Container::setMessage(QString message)
- {
- if (m_message == message)
- return;
- m_message = message;
- emit messageChanged(m_message);
- }
Фабрика объектов
Для удобства созданий объектов в C++ коде и передаче указателя в QML я создам специальную фабрику, которая будет зарегистрирована в контексте QML, что позволит быстро создавать Структуры и Контейнеры с информацией и возвращать указатель на эти объекты в QML.
Factory.h
- #ifndef FACTORY_H
- #define FACTORY_H
- #include <QObject>
- class Factory : public QObject
- {
- Q_OBJECT
- public:
- explicit Factory(QObject *parent = nullptr);
- Q_INVOKABLE QObject* createContainer(); // Для создания контейнеров
- Q_INVOKABLE QObject* createStructure(); // Для создания структур
- private:
- int m_count {0};
- int m_structureCount {0};
- };
- #endif // FACTORY_H
Factory.cpp
Вы можете заметить, что в качестве parent я устанавливаю фабрику. При работе с указателями и передаче их из C++ в QML есть нюансы со владением указателем. Если у объекта не выставлен parent, то он может быть уничтожен сборщиком мусора в QML, в итоге можем получить невалидный указатель в C++. Подробнее в этой статье .
- #include "Factory.h"
- #include "Container.h"
- Factory::Factory(QObject *parent) : QObject(parent)
- {
- }
- QObject* Factory::createContainer()
- {
- Container* container = new Container(this);
- container->setNumber(++m_count);
- container->setMessage(QString("Container %1").arg(m_count));
- return container;
- }
- QObject* Factory::createStructure()
- {
- Structure* structure = new Structure(this);
- structure->m_number = ++m_structureCount;
- structure->m_message = QString("Structure %1").arg(m_structureCount);
- return structure;
- }
main.cpp
Посмотрим, на то, как выглядит регистрация фабрики в контексте QML. Это позволит обращаться к фабрике в любом месте кода QML.
- #include <QGuiApplication>
- #include <QQmlContext>
- #include <QQmlApplicationEngine>
- #include "Factory.h"
- int main(int argc, char *argv[])
- {
- QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
- QGuiApplication app(argc, argv);
- QQmlApplicationEngine engine;
- engine.load(QUrl(QLatin1String("qrc:/main.qml")));
- if (engine.rootObjects().isEmpty())
- return -1;
- Factory factory;
- engine.rootContext()->setContextProperty("factory", &factory);
- return app.exec();
- }
main.qml
После того, как все подготовительные операции были проведены, попробуем создать несколько структур и контейнеров и обратиться к их полям и методам, чтобы вывести информацию из них.
- import QtQuick 2.7
- import QtQuick.Controls 2.0
- import QtQuick.Layouts 1.3
- ApplicationWindow {
- visible: true
- width: 640
- height: 480
- title: qsTr("Hello World")
- // массив QML, в который можно поместить что угодно, в C++ это будет либо QVariantMap, либо QVariantList
- property var objectsArray: []
- Text {
- id: textView
- clip: true
- anchors {
- top: parent.top
- left: parent.left
- right: parent.right
- bottom: parent.verticalCenter
- margins: 5
- }
- }
- Button {
- id: addOBjectStructure
- text: qsTr("Add Structure")
- anchors {
- right: parent.horizontalCenter
- left: parent.left
- bottom: addOBjectButton.top
- margins: 5
- }
- onClicked: {
- // Добавляем структуру в массив
- objectsArray.push(factory.createStructure())
- }
- }
- Button {
- id: addOBjectButton
- text: qsTr("Add Object")
- anchors {
- right: parent.horizontalCenter
- left: parent.left
- bottom: parent.bottom
- margins: 5
- }
- onClicked: {
- // Добавляем контейнер в массив
- objectsArray.push(factory.createContainer())
- }
- }
- Button {
- text: qsTr("Read info from Objects")
- anchors {
- right: parent.right
- left: parent.horizontalCenter
- bottom: parent.bottom
- margins: 5
- }
- onClicked: {
- // выводим текст из всех объектов массива
- textView.text = ""
- for (var i = 0; i < objectsArray.length; ++i)
- {
- // главное, чтобы все объекты имели методы с одинаковыми названиями
- var str = objectsArray[i].number + " " + objectsArray[i].message + "\n" + objectsArray[i].getFullInfo() + "\n"
- textView.text += str
- }
- }
- }
- }
И структуры и контейнеры мы будем помещать в один и тот же массив. Я уже говорил выше, что QVariantMap и QVariantList преобразуются в QML`е в массив JavaScript, которому всё равно, какая в него помещается информация. Поэтому, когда мы попытаемся пройтись по всем элементам массива и вызвать методы number , message и getFullInfo() у нас не возникнет никаких проблем. В данном случае неявно все эти методы будут вызываться через метод QMetaObject::invokeMethod , и если метод зарегистрирован, то он будет вызван. А поскольку в массиве объекты двух разных классов, то главное будет здесь то, что методы должны иметь одинаковые названия. Это очень сильно напоминает поведение утиной типизации в Python . Хотя, конечно же, это не утиная типизация. Это особенность Мета-объектной системы Qt.
Результат будет следующим
Отличный пример удобной работы с передачей данных из крестов.
Добрый день. Полезный урок, подскажите как можно передать структуру данных из qml в c++ при помощи QVariantMap. Со стороны qml упаковываю в Map, добавляю несколько пар ключ значение и вызываю матод(с++) в аргумент Q_INVOKABLE метода (c++) вставляю Map. В методе с++ QvariantMap пустой. Подскажите может что-то не так делаю.