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
OI
  • Ora Iro
  • 24 грудня 2024 р. 06:38

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

  • Результат:40бали,
  • Рейтинг балів-8
AD

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

  • Результат:50бали,
  • Рейтинг балів-4
m
  • molni99
  • 26 жовтня 2024 р. 01:37

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

  • Результат:80бали,
  • Рейтинг балів4
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 11:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 14:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
Donald Randolph
Donald Randolph30 грудня 2024 р. 02:59
Personal Injury lawyer Santa Monica As an experienced Santa Monica personal injury lawyer, Donald C. Randolph has recovered over $100 Million in verdicts and settlements for our clients. In severe injury cases, this compensation i…
Nirvana Yoga School
Nirvana Yoga School30 грудня 2024 р. 05:13
OAuth2.0 через VK, получение email Nirvana Yoga School is one of the most trusted and reputed traditional Rishikesh yoga courses , India certified by Yoga Alliance, USA. We aim to spread traditional yoga teachings so t…
s
sripark30 грудня 2024 р. 04:47
Mobile app development company in Chennai A Mobile app development company in Chennai focuses on creating personalized mobile applications to meet various business requirements. These companies offer a full range of services,…
a
amit8830 грудня 2024 р. 04:45
Excel in Exams with PSLE Maths Tuition Singapore Preparing for the PSLE can be challenging, but the right guidance makes all the difference. PSLE Maths tuition Singapore offers personalized coaching to help students master key concepts, improv…
a
awinash6230 грудня 2024 р. 04:23
Unlock Your Potential with the Certified Public Accountant Credential" Becoming a Certified Public Accountant (CPA) is a career milestone that opens doors to unparalleled opportunities in the world of accounting and finance. This globally recognized qualification s…

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