Evgenii Legotckoi
Evgenii Legotckoi27 листопада 2017 р. 02: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.

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

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Стабільний хостинг, на якому розміщується соціальна мережа EVILEG. Для проектів на Django радимо VDS хостинг.

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

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

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

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

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

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
d
  • dsfs
  • 26 квітня 2024 р. 11:56

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80бали,
  • Рейтинг балів4
d
  • dsfs
  • 26 квітня 2024 р. 11:45

C++ - Тест 002. Константы

  • Результат:50бали,
  • Рейтинг балів-4
d
  • dsfs
  • 26 квітня 2024 р. 11:35

C++ - Тест 001. Первая программа и типы данных

  • Результат:73бали,
  • Рейтинг балів1
Останні коментарі
k
kmssr09 лютого 2024 р. 02:43
Qt Linux - Урок 001. Автозапуск програми Qt під Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко05 лютого 2024 р. 09:50
Qt WinAPI - Урок 007. Робота з ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 грудня 2023 р. 18:30
Boost - статичне зв&#39;язування в проекті CMake під Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 грудня 2023 р. 16:38
Boost - статичне зв&#39;язування в проекті CMake під Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 грудня 2023 р. 05:01
Qt/C++ - Урок 056. Підключення бібліотеки Boost в Qt для компіляторів MinGW і MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Тепер обговоріть на форумі
IscanderChe
IscanderChe30 квітня 2024 р. 11:22
Во Flask рендер шаблона не передаётся в браузер Доброе утро! Имеется вот такой шаблон: <!doctype html><html> <head> <title>{{ title }}</title> <link rel="stylesheet" href="{{ url_…
G
Gar22 квітня 2024 р. 12:46
Clipboard Как скопировать окно целиком в clipb?
DA
Dr Gangil Academics20 квітня 2024 р. 14:45
Unlock Your Aesthetic Potential: Explore MSC in Facial Aesthetics and Cosmetology in India Embark on a transformative journey with an msc in facial aesthetics and cosmetology in india . Delve into the intricate world of beauty and rejuvenation, guided by expert faculty and …
a
a_vlasov14 квітня 2024 р. 13:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 квітня 2024 р. 09:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь

Слідкуйте за нами в соціальних мережах