- 1. Контейнер.h
- 2. Container.cpp
- 3. Фабрика об'єктів
- 1. Завод.ч
- 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.
Контейнер.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.
Завод.ч
#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 пустой. Подскажите может что-то не так делаю.