- 1. Container.h
- 2. Container.cpp
- 3. Factory of objects
- 1. Factory.h
- 2. Factory.cpp
- 4. main.cpp
- 5. main.qml
One of the undoubted advantages of QML in Qt is that it allows a fairly sharp separation of backend logic from the application interface. That is, we write all the backend in C ++, and in QML we only display the required result.
In doing so, we can also write internal logic purely on QML, minimizing the presence of C ++ code, or vice versa, design the design of some elements in C ++, taking advantage of the OpenGL capabilities. Despite the distinction between QML and C ++ code parts on the backend and frontend applications, we do not have any big restrictions.
But sometimes it becomes necessary to transfer some data structure that will carry several fields with information, in the QML part of the code. For example, such a structure.
struct Structure { int m_number; QString m_message; };
It's just that this structure can not be passed to QML and the first thing that comes to mind is to make the sendToQml signal, which will send out several arguments. Each argument will be responsible for a specific field structure.
void sendToQml(int number, QString message);
This signal will be located in your class, which will send to QML certain information with numbers, messages, etc. The minus of this approach is obvious, since there can be a lot of such fields and it will be foolish to make a signal with a couple of dozens of arguments. This approach will be useful for sending small information, but not for data structures.
There is another option to use QVariantMap. Because it is well-converted into an array of QML, from which you can collect data by key. An example of use is the official Qt documentation.
Thus, the information in 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]) } } }
And thus this information is added to 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)));
But I do not particularly like this approach, since there can be a situation when it is necessary to do certain actions with data that the structure carries in itself, and QVariantMap, of such methods can not have in principle. Yes, and I want a more complete presentation of information than a set of keys and values. If this is some message or data object that is passed within the C ++ code and work happens to it as an integral object, then why should we turn it into a set of key-value pairs that live inside QVariantMap?
That's right, it's not necessary. The matter is that from C ++ it is possible to transfer in QML the pointer on object of class QObject . And given the possibilities of polymorphism , inheritance, etc. You can inherit from the QObject class and pass a pointer to this object in QML as a pointer to the QObject base class. Moreover, without even registering this new class as MetaType in the Qt meta-object system.
What will it give us? We understand that if you inherit from a class and add some methods and fields to the inherited class, you can work with them and call them, but you will need to have a pointer to the object of the corresponding class, that is, if we take pointer to the base class and assign it a pointer to the inherited class, then within the code we can not write a call to the method of the derived class when working with the base class pointer. That is, the following code is not compiled.
class A { public: A() {} }; class B : public A { public: B() {} void someMethod(); }; int main(int argc, char *argv[]) { A *a = new B(); // Ok. You can assign a pointer to an inherited class to a pointer to a base class a->someMethod(); // Error. But you can not call the method of the inherited class anymore, the base class does not know anything about it return 0; }
And logically, there is no sense to inherit from QObject without registering the class via qmlRegisterType . However, everything is different here, as we are accustomed to. The fact is that if the method is marked as a signal, a slot or Q_INVOKABLE method or some of the fields is marked as Q_PROPERTY , then the meta-object system Qt already knows about this method or field. And the method, for example, can be called through the static method of the QMetaObject class. Due to this, these methods can be called in QML, even if there is a pointer to the base class QObject.
And now let's look at an example of the structure and class that are inherited from QObject, and which will be passed to 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 // We can access the fields from QML // the MEMBER parameter indicates that it is possible to work with this field and track its change in 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 // We can access the fields from 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); // And also call this method in 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 , а также имеются сигналы и слоты.
Here we see a classic variant of declaring fields and methods for a custom object, which will work in QML . That is, there are macros Q_PROPERTY and Q_INVOKABLE , and there are signals and slots.
Container.cpp
The contents of the source file should not be called at all.
#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); }
Factory of objects
For the convenience of creating objects in C ++ code and passing the pointer in QML, I will create a special factory that will be registered in the context of QML, which will quickly create Structures and Containers with information and return a pointer to these objects in 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(); // To create containers Q_INVOKABLE QObject* createStructure(); // To create structures private: int m_count {0}; int m_structureCount {0}; }; #endif // FACTORY_H
Factory.cpp
You can notice that as a parent, I set up a factory. When working with pointers and passing them from C ++ to QML, there are nuances with pointer ownership. If the object is not exposed to parent, then it can be destroyed by the garbage collector in QML, eventually we can get an invalid pointer in C ++. Read more in this article .
#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
Let's see how the registration of the factory looks in the context of QML. This will allow you to access the factory anywhere in the QML code.
#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
After all the preparatory operations have been carried out, we will try to create several structures and containers and refer to their fields and methods to derive information from them.
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: { // Adding a structure to the array objectsArray.push(factory.createStructure()) } } Button { id: addOBjectButton text: qsTr("Add Object") anchors { right: parent.horizontalCenter left: parent.left bottom: parent.bottom margins: 5 } onClicked: { // Add the container to the array objectsArray.push(factory.createContainer()) } } Button { text: qsTr("Read info from Objects") anchors { right: parent.right left: parent.horizontalCenter bottom: parent.bottom margins: 5 } onClicked: { // output text from all array objects textView.text = "" for (var i = 0; i < objectsArray.length; ++i) { // the main thing is that all objects have methods with the same names var str = objectsArray[i].number + " " + objectsArray[i].message + "\n" + objectsArray[i].getFullInfo() + "\n" textView.text += str } } } }
And we will place structures and containers in the same array. I already said above that QVariantMap and QVariantList are converted in QML into a JavaScript array, which does not care which information is placed in it. Therefore, when we try to go through all the elements of the array and call the methods number, message and getFullInfo() , we will not have any problems. In this case, all these methods will be implicitly called through the QMetaObject::invokeMethod method, and if the method is registered, it will be called. Because in the array objects of two different classes, the main thing here is that the methods should have the same name. This is very much like the behavior of duck typing in Python . Although of course this is not duck typing. This is a feature of the Meta-object system Qt .
The result will be as follows
Отличный пример удобной работы с передачей данных из крестов.
Добрый день. Полезный урок, подскажите как можно передать структуру данных из qml в c++ при помощи QVariantMap. Со стороны qml упаковываю в Map, добавляю несколько пар ключ значение и вызываю матод(с++) в аргумент Q_INVOKABLE метода (c++) вставляю Map. В методе с++ QvariantMap пустой. Подскажите может что-то не так делаю.