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 частину коду. Наприклад, таку структуру.

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.

Результат буде наступним

Вам це подобається? Поділіться в соціальних мережах!

BlinCT
  • 27 листопада 2017 р. 17:12

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

ilya.guzikov
  • 20 травня 2021 р. 16:33

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

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up