© EVILEG 2015-2018
Рекомендует хостинг
TIMEWEB

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

QML, Qt, Meta, Q_INVOKABLE

Один из несомненных плюсов 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.

Container.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.

Factory.h

#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.

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

Комментарии

27 ноября 2017 г. 11:12

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

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
24 сентября 2018 г. 17:42
edorofeeva

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

  • Результат 100баллов,
  • Очки рейтинга10
24 сентября 2018 г. 17:37
edorofeeva

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

  • Результат 66баллов,
  • Очки рейтинга-1
23 сентября 2018 г. 14:38
No Names

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

  • Результат 60баллов,
  • Очки рейтинга-1
Последние комментарии
25 сентября 2018 г. 15:24
pasagir

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Это запись метода которая работает параллельно с БД, данные из парсера поступают в БД и в наш метод одновременно
25 сентября 2018 г. 14:56
pasagir

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Как можно динамически отображать данные в таблице? На COM-порт непрерывно приходят данные, я их принимаю сохраняю в БД, а после остановка приема/передачи данные отображаются в таблице. В табли...
25 сентября 2018 г. 10:43
Евгений Легоцкой

Qt/C++ - Урок 017. QGraphicsScene или как работать с графикой в Qt

Прямо так не написано. Хотя соглашусь, что в качестве улучшения вызов данного метода здесь к месту.
25 сентября 2018 г. 10:37
reef425

Qt/C++ - Урок 017. QGraphicsScene или как работать с графикой в Qt

В статье написано, что таймер сработает один раз. Но это не так. Было бы хорошо добавить timer->setSingleShot(true); После инициализации таймера.
24 сентября 2018 г. 15:09
Евгений Легоцкой

Qt Linux - Урок 001. Автозапуск Qt приложения под Linux

А вот здесь у меня есть пример использования supervisor. https://evileg.com/ru/post/3/ Вся статья вам там не интересна, интересен только шаг с настройкой supervisor. Он получается ...
Сейчас обсуждают на форуме
25 сентября 2018 г. 15:57
Евгений_Канусовский@1981

Чтение файлов в python

Вот код: import sys from re import matchfrom vira import *from PyQt5 import QtCore, QtGui, QtWidgetsfrom PyQt5.QtWidgets import (QDialog, QFileDialog, QMessageBox, QLineEdit, QProgr...
25 сентября 2018 г. 13:51
DmitrySD

Трансляция видео с помощью VLC по RTP

Спасибо! Данная команда не дала результата. В итоге сделал трансляцию через ffmpeg. ffmpeg.exe -f gdigrab -framerate 30 -i desktop -vcodec libx264 -preset:v veryfast -b:v 4000k -f...
25 сентября 2018 г. 13:39
Arrow

Настройка Qt Creator для Android

Конечно отпишусь.
25 сентября 2018 г. 12:22
avovana

Автозапуск и авторестарт Qt Gui Application в Linux

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