- 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 пустой. Подскажите может что-то не так делаю.