Evgenii Legotckoi
27 ноября 2017 г. 13:42

QML - Урок 034. Передача структур данных из C++ слоя приложения в QML слой

Один из несомненных плюсов QML в Qt состоит в том, что он позволяет достаточно резко отделять backend-логику от интерфейса приложения. То есть весь backend мы пишем на C++, а в QML лишь отображаем необходимый результат.

При этом мы также можем и внутреннюю логику написать чисто на QML, минимизировав наличие C++ кода, или наоборот прописать дизайн некоторых элементов в C++, воспользовавшись возможностями OpenGL . Несмотря на разграничение QML и C++ частей кода на backend и frontend приложения, мы не имеем каких-либо больших ограничений.

Но иногда возникает необходимость передать некоторую структуру данных, которая будет нести несколько полей с информацией, в QML часть кода. Например такую структуру.

  1. struct Structure
  2. {
  3. int m_number;
  4. QString m_message;
  5. };

Просто так такую структуру не получиться передать в QML и первое, что приходит на ум, это сделать сигнал sendToQml , который будет высылать несколько аргументов. Каждый аргумент будет отвечать за определённое поле структуры.

  1. void sendToQml(int number, QString message);

Этот сигнал будет располагаться в вашем классе, который будет отправлять в QML определённую информацию с номерами, сообщениями и т.д. Минус подобного подхода очевиден, поскольку таких полей может быть очень много и будет глупо делать сигнал с парой десятков аргументов. Подобный подход будет полезен для пересылки небольшой информации, но никак не для структур данных.


Есть ещё вариант использовать QVariantMap. Поскольку он хорошо конвертируется в массив QML, из которого можно забирать данные по ключу. Пример использования есть официальной документации Qt.

Таким образом забирается информация в QML

  1. // MyItem.qml
  2. Item {
  3. function readValues(anArray, anObject) {
  4. for (var i=0; i<anArray.length; i++)
  5. console.log("Array item:", anArray[i])
  6.  
  7. for (var prop in anObject) {
  8. console.log("Object item:", prop, "=", anObject[prop])
  9. }
  10. }
  11. }

А таким образом эта информация добавляется в C++

  1. // C++
  2. QQuickView view(QUrl::fromLocalFile("MyItem.qml"));
  3.  
  4. QVariantList list;
  5. list << 10 << QColor(Qt::green) << "bottles";
  6.  
  7. QVariantMap map;
  8. map.insert("language", "QML");
  9. map.insert("released", QDate(2010, 9, 21));
  10.  
  11. QMetaObject::invokeMethod(view.rootObject(), "readValues",
  12. Q_ARG(QVariant, QVariant::fromValue(list)),
  13. Q_ARG(QVariant, QVariant::fromValue(map)));

Но мне этот подход не особо нравится, поскольку может возникнуть ситуация, когда нужно сделать определённые действия с данными, которые несёт в себе структура, а QVariantMap, таких методов не может иметь в принципе. Да и хочется более целостного представления информации, чем набор ключей и значений. Если это какое-то сообщение или объект данных, который передаётся в рамках C++ кода и работа происходит с ним как с целостным объектом, то почему мы должны превращать его в набор пар ключ-значение, которые живут внутри QVariantMap?

Правильно, это необязательно. Дело в том, что из C++ можно передавать в QML указатель на объект класса QObject . А учитывая возможности полиморфизма , наследования и т.д. можно наследоваться от класса QObject и передать указатель на этот объект в QML в качестве указателя на базовый класс QObject . Причём, даже не регистрируя этот новый класс в качестве MetaType в мета-объектной системе Qt .

Что это нам даст? Мы понимаем, что если наследоваться от какого-то класса и добавить в наследованный класса какие-то методы и поля, то с ними можно будет работать и вызывать их, но при этом нужно будет иметь указатель на объект соответствующего класса, то есть если мы возьмём указатель на базовый класс и присвоим ему указатель на наследованный класс, то в рамках кода мы не сможем записать вызов метода наследованного класса при работе с указателем базового класса. То есть следующий код не скомпилируется.

  1. class A
  2. {
  3. public:
  4. A() {}
  5. };
  6.  
  7. class B : public A
  8. {
  9. public:
  10. B() {}
  11. void someMethod();
  12. };
  13.  
  14. int main(int argc, char *argv[])
  15. {
  16. A *a = new B(); // Ok. Можно присвоить указатель на наследованный класс указателю на базовый класс
  17. a->someMethod(); // Ошибка. А вот вызвать метод наследованного класса уже нельзя, базовый класс об это ничего не знает
  18. return 0;
  19. }

И по логике, нет никакого смысла наследоваться от QObject без регистрации класса через qmlRegisterType. Однако здесь всё не так, как мы привыкли. Дело в том, что если метод помечен как сигнал , слот или Q_INVOKABLE метод или какое-то из полей помечено как Q_PROPERTY , то мета-объектная система Qt уже знает об этом методе или поле. И метод, например, можно вызвать через статический метод класса QMetaObject. За счёт этого эти методы можно вызывать в QML, даже, если будет иметься указатель на базовый класс QObject.

А теперь посмотрим на пример структуры и класса, которые наследованы от QObject, и которые будут передаваться в QML.

Container.h

  1. #ifndef CONTAINER_H
  2. #define CONTAINER_H
  3.  
  4. #include <QObject>
  5.  
  6. struct Structure : public QObject
  7. {
  8. explicit Structure(QObject *parent = nullptr);
  9.  
  10. int m_number;
  11. QString m_message;
  12.  
  13. private:
  14. Q_OBJECT
  15. // Сможем обращаться к полям из QML
  16. // параметр MEMBER указывает, что имеется возможность работать с этим полем и отслеживать его изменение в QML
  17. Q_PROPERTY(int number MEMBER m_number)
  18. Q_PROPERTY(QString message MEMBER m_message)
  19.  
  20. public:
  21. // А также вызвать в QML этот метод
  22. Q_INVOKABLE QString getFullInfo() const;
  23. };
  24.  
  25. class Container : public QObject
  26. {
  27. Q_OBJECT
  28. // Сможем обращаться к полям из QML
  29. Q_PROPERTY(int number READ number WRITE setNumber NOTIFY numberChanged)
  30. Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged)
  31.  
  32. public:
  33. explicit Container(QObject *parent = nullptr);
  34.  
  35. // А также вызвать в QML этот метод
  36. Q_INVOKABLE QString getFullInfo() const;
  37.  
  38. int number() const;
  39. QString message() const;
  40.  
  41. public slots:
  42. void setNumber(int number);
  43. void setMessage(QString message);
  44.  
  45. signals:
  46. void numberChanged(int number);
  47. void messageChanged(QString message);
  48.  
  49. private:
  50. int m_number;
  51. QString m_message;
  52. };
  53.  
  54. #endif // CONTAINER_H

Здесь мы видим классический вариант объявления полей и методов для кастомного объекта, с которым будем работать в QML . То есть имеются макросы Q_PROPERTY и Q_INVOKABLE , а также имеются сигналы и слоты.

Container.cpp

Содержимое файла исходных кодов вообще не должно вызываться никаких вопросов.

  1. #include "Container.h"
  2.  
  3. Structure::Structure(QObject *parent) : QObject(parent)
  4. {
  5.  
  6. }
  7.  
  8. QString Structure::getFullInfo() const
  9. {
  10. return QString("Full information from Structure %1").arg(m_number);
  11. }
  12.  
  13. Container::Container(QObject *parent) : QObject(parent)
  14. {
  15.  
  16. }
  17.  
  18. QString Container::getFullInfo() const
  19. {
  20. return QString("Full information from Container %1").arg(m_number);
  21. }
  22.  
  23. int Container::number() const
  24. {
  25. return m_number;
  26. }
  27.  
  28. QString Container::message() const
  29. {
  30. return m_message;
  31. }
  32.  
  33. void Container::setNumber(int number)
  34. {
  35. if (m_number == number)
  36. return;
  37.  
  38. m_number = number;
  39. emit numberChanged(m_number);
  40. }
  41.  
  42. void Container::setMessage(QString message)
  43. {
  44. if (m_message == message)
  45. return;
  46.  
  47. m_message = message;
  48. emit messageChanged(m_message);
  49. }

Фабрика объектов

Для удобства созданий объектов в C++ коде и передаче указателя в QML я создам специальную фабрику, которая будет зарегистрирована в контексте QML, что позволит быстро создавать Структуры и Контейнеры с информацией и возвращать указатель на эти объекты в QML.

Factory.h

  1. #ifndef FACTORY_H
  2. #define FACTORY_H
  3.  
  4. #include <QObject>
  5.  
  6. class Factory : public QObject
  7. {
  8. Q_OBJECT
  9. public:
  10. explicit Factory(QObject *parent = nullptr);
  11.  
  12. Q_INVOKABLE QObject* createContainer(); // Для создания контейнеров
  13. Q_INVOKABLE QObject* createStructure(); // Для создания структур
  14.  
  15. private:
  16. int m_count {0};
  17. int m_structureCount {0};
  18. };
  19.  
  20. #endif // FACTORY_H

Factory.cpp

Вы можете заметить, что в качестве parent я устанавливаю фабрику. При работе с указателями и передаче их из C++ в QML есть нюансы со владением указателем. Если у объекта не выставлен parent, то он может быть уничтожен сборщиком мусора в QML, в итоге можем получить невалидный указатель в C++. Подробнее в этой статье .

  1. #include "Factory.h"
  2.  
  3. #include "Container.h"
  4.  
  5. Factory::Factory(QObject *parent) : QObject(parent)
  6. {
  7.  
  8. }
  9.  
  10. QObject* Factory::createContainer()
  11. {
  12. Container* container = new Container(this);
  13. container->setNumber(++m_count);
  14. container->setMessage(QString("Container %1").arg(m_count));
  15. return container;
  16. }
  17.  
  18. QObject* Factory::createStructure()
  19. {
  20. Structure* structure = new Structure(this);
  21. structure->m_number = ++m_structureCount;
  22. structure->m_message = QString("Structure %1").arg(m_structureCount);
  23. return structure;
  24. }

main.cpp

Посмотрим, на то, как выглядит регистрация фабрики в контексте QML. Это позволит обращаться к фабрике в любом месте кода QML.

  1. #include <QGuiApplication>
  2. #include <QQmlContext>
  3. #include <QQmlApplicationEngine>
  4.  
  5. #include "Factory.h"
  6.  
  7. int main(int argc, char *argv[])
  8. {
  9. QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
  10. QGuiApplication app(argc, argv);
  11.  
  12. QQmlApplicationEngine engine;
  13. engine.load(QUrl(QLatin1String("qrc:/main.qml")));
  14. if (engine.rootObjects().isEmpty())
  15. return -1;
  16.  
  17. Factory factory;
  18. engine.rootContext()->setContextProperty("factory", &factory);
  19.  
  20. return app.exec();
  21. }

main.qml

После того, как все подготовительные операции были проведены, попробуем создать несколько структур и контейнеров и обратиться к их полям и методам, чтобы вывести информацию из них.

  1. import QtQuick 2.7
  2. import QtQuick.Controls 2.0
  3. import QtQuick.Layouts 1.3
  4.  
  5. ApplicationWindow {
  6. visible: true
  7. width: 640
  8. height: 480
  9. title: qsTr("Hello World")
  10.  
  11. // массив QML, в который можно поместить что угодно, в C++ это будет либо QVariantMap, либо QVariantList
  12. property var objectsArray: []
  13.  
  14. Text {
  15. id: textView
  16. clip: true
  17. anchors {
  18. top: parent.top
  19. left: parent.left
  20. right: parent.right
  21. bottom: parent.verticalCenter
  22. margins: 5
  23. }
  24. }
  25.  
  26. Button {
  27. id: addOBjectStructure
  28. text: qsTr("Add Structure")
  29. anchors {
  30. right: parent.horizontalCenter
  31. left: parent.left
  32. bottom: addOBjectButton.top
  33. margins: 5
  34. }
  35.  
  36. onClicked: {
  37. // Добавляем структуру в массив
  38. objectsArray.push(factory.createStructure())
  39. }
  40. }
  41.  
  42. Button {
  43. id: addOBjectButton
  44. text: qsTr("Add Object")
  45. anchors {
  46. right: parent.horizontalCenter
  47. left: parent.left
  48. bottom: parent.bottom
  49. margins: 5
  50. }
  51.  
  52. onClicked: {
  53. // Добавляем контейнер в массив
  54. objectsArray.push(factory.createContainer())
  55. }
  56. }
  57.  
  58. Button {
  59. text: qsTr("Read info from Objects")
  60. anchors {
  61. right: parent.right
  62. left: parent.horizontalCenter
  63. bottom: parent.bottom
  64. margins: 5
  65. }
  66.  
  67. onClicked: {
  68. // выводим текст из всех объектов массива
  69. textView.text = ""
  70. for (var i = 0; i < objectsArray.length; ++i)
  71. {
  72. // главное, чтобы все объекты имели методы с одинаковыми названиями
  73. var str = objectsArray[i].number + " " + objectsArray[i].message + "\n" + objectsArray[i].getFullInfo() + "\n"
  74. textView.text += str
  75. }
  76. }
  77. }
  78. }

И структуры и контейнеры мы будем помещать в один и тот же массив. Я уже говорил выше, что QVariantMap и QVariantList преобразуются в QML`е в массив JavaScript, которому всё равно, какая в него помещается информация. Поэтому, когда мы попытаемся пройтись по всем элементам массива и вызвать методы number , message и getFullInfo() у нас не возникнет никаких проблем. В данном случае неявно все эти методы будут вызываться через метод QMetaObject::invokeMethod , и если метод зарегистрирован, то он будет вызван. А поскольку в массиве объекты двух разных классов, то главное будет здесь то, что методы должны иметь одинаковые названия. Это очень сильно напоминает поведение утиной типизации в Python . Хотя, конечно же, это не утиная типизация. Это особенность Мета-объектной системы Qt.

Результат будет следующим

Вам это нравится? Поделитесь в социальных сетях!

BlinCT
  • 27 ноября 2017 г. 17:12

Отличный пример удобной работы с передачей данных из крестов.

ilya.guzikov
  • 20 мая 2021 г. 16:33

Добрый день. Полезный урок, подскажите как можно передать структуру данных из qml в c++ при помощи QVariantMap. Со стороны qml упаковываю в Map, добавляю несколько пар ключ значение и вызываю матод(с++) в аргумент Q_INVOKABLE метода (c++) вставляю Map. В методе с++ QvariantMap пустой. Подскажите может что-то не так делаю.

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь