📜 ⬆️ ⬇️

Qt Meta System over Network. Part 1 - Properties


With enviable regularity, I have the task of writing client-server applications using Qt. And I thought - why not simplify this process? In fact, why reinvent a new protocol every time if you can use the usual signals and slots? Something similar already exists, for example, D-Bus or QRemoteSignal, but they seemed to me not very convenient, and there are no some possibilities in them.

Agree, it would be very convenient to write something like this:
Computer 1:
//      xValue net.addProprety("value", "xValue", object); //   net.addSignal("started", object, SIGNAL(started(int, QString))); //   net.addFunction("start", object, "method_name"); 


Computer 2:
 //     net.setProperty("value", 123); //    -  ,  QLineEdit net.bindProperty("value", lineEdit, "text"); //     net.connect(SIGNAL(started(int, QString)), object, SLOT(onStarted(int, QString))); //   ( ) bool ok; QVariant ret = net.call("start", QVariantList() << "str1" << 1, &ok); //   (      ) net.call("start", QVariantList() << "str1" << 1, object, SLOT(startCalled(bool, QVariant))); 

net is an abstract network access interface

It is also easy to write methods that make available immediately a whole list of properties or signals of an object. And that is not all! You can easily attach to all this Qt Quick. In general, having figured out how to still learn about changing properties, catch signals, and execute any slots at runtime with any types, a lot can be done.
Let's start with the simplest - properties.
')

1. Properties


First, consider how to change any properties dynamically, and more importantly, to receive signals about their change in the form: <property name, new value>

It is in the name of the property that the problem lies - if we connect the signal about the change of all properties to one slot, we will not be able to find out the name of the changed property. We can only find out who sent this change using the sender () function, but the name of the property cannot be recognized in this way. Here it immediately comes to mind to create for each property an object of some class that will store the name of the property, receive a signal about its change, and generate a new signal, but already with a name.

Method "in the forehead"

 Class Property { public: Proprety(const QString &name) : m_name(name) {} public slots: void propertyChanged(const QVariant &newValue) { emit mapped(m_name, newValue); } signals: void mapped(const QString &propertyName, const QVariant &newValue); } 

Now, if we want to find out about changes in the p1, p2 properties of the object, we can write the following code:
 PropertyMapper *m1 = new PropertyMapper("p1"); connect(object, SIGNAL(p1Changed(QVariant)), m1, SLOT(propertyChanged(QVariant)); PropertyMapper *m2 = new PropertyMapper("p2"); connect(object, SIGNAL(p2Changed(QVariant)), m2, SLOT(propertyChanged(QVariant)); 

Next, simply connect to the signal PropertyMapper :: mapped and get signals with the name of the property and its new value. But here you can immediately see obvious problems: a waste of memory and processor resources, as well as, perhaps more importantly, the inability to work with the properties of other types without creating additional slots for each type, transformations, etc. In general, there is a much more elegant way to solve all these problems at once.

Advanced method

First, let's look at how the slot is called and how the QObject :: connect () function works.
Slot call
The Q_OBJECT macro added to the class declaration results (among other things) in adding the qt_metacall () method. It is through it that slots are called, properties are set. And all the checks for the existence of a slot, the casting of arguments are implemented in it. The standard implementation looks like this:
 int Counter::qt_metacall(QMetaObject::Call _c, int _id, void **_a) { _id = QObject::qt_metacall(_c, _id, _a); if (_id < 0) return _id; if (_c == QMetaObject::InvokeMetaMethod) { switch (_id) { case 0: valueChanged((*reinterpret_cast< int(*)>(_a[1]))); break; case 1: setValue((*reinterpret_cast< int(*)>(_a[1]))); break; } _id -= 2; } return _id; } 


QObject :: connect

Briefly look at the actions performed by this function:
1) The conversion of the names of signals and slots into a normalized form, i.e. remove extra spaces, and some other transformations (more QMetaObject :: normalizedSignature ())
2) Type checking
3) Calculation of slot indices and signals by their names using object-> metaObject () -> indexOfSlot (indexOfSignal) ()
4) And the most interesting thing is connecting the signal to the slot by index using QMetaObject :: connect ().
I think many have already guessed what needs to be done - write your own implementation of qt_metacall and connect to the signal about a property change manually. Let's start:

PropertyMapper.h
 class PropertyMapper : public QObject //     Q_OBJECT { public: PropertyMapper(QObject *mapToObject, const char *mapToMethod, QObject *parent = 0); int addProperty(const QString &propertyName, const char *mappingPropertyName, QObject *mappingObject, bool isQuickProperty); void setMappedProperty(const QString &name, const QVariant &value); QVariant mappedProperty(const QString &name) const; int qt_metacall(QMetaObject::Call call, int id, void **arguments); private: QObject *m_mapTo; const char *m_toMethod; QHash<QString, int> m_propertyIndices; typedef struct { QString name; QVariant::Type type; const char *mappingName; QObject *mappingObject; bool isQuickProperty; // need to call mappingObject->property to get value QVariant lastValue; } property_t; QList<property_t> m_properties; }; 

I will not describe what all the fields are for, now everything will become clear. Consider the key points in pieces (you can download the whole at the end of the article).

Adding a property named propertyName, while all actions will occur with the mappingPropertyName property of the mappingObject object. If we want to do this focus with the Qt Quick property, it is necessary to set isQuickProperty to true (it will become clear how this is done later).

First, we check if there is a property with the same name. (m_propertyIndices contains property_name_pairs and property_index):
 int PropertyMapper::addProperty(const QString &propertyName, const char *mappingPropertyName, QObject *mappingObject, bool isQuickProperty) { if (m_propertyIndices.contains(propertyName)) { qWarning() << "can't create" << propertyName << "property, already exist!"; return -1; } 

We get the index of the property, and then by the QMetaProperty index:
  int propertyIdx = mappingObject->metaObject()->indexOfProperty(mappingPropertyName); QMetaProperty metaProperty = mappingObject->metaObject()->property(propertyIdx); 

Save information about the added property:
  int id = m_properties.size(); m_propertyIndices[propertyName] = id; m_properties.push_back({propertyName, metaProperty.type(), mappingPropertyName, mappingObject, isQuickProperty, QVariant()}); 

Now the most interesting thing is to get the index of the signal about a property change, and connect to it, the type checking is not performed, because we save the property type (metaProperty.type ()) and we will bring to it the resulting property value:
  int signalId = metaProperty.notifySignalIndex(); if (signalId < 0) { qWarning() << "can't create" << propertyName << "(notify signal doesn't exist)"; return -1; } if (!QMetaObject::connect(mappingObject, signalId, this, id + metaObject()->methodCount())) { qWarning() << "can't connect to notify signal:" << mappingPropertyName; return -1; } return id; } 


And most importantly - qt_metacall ():
 int PropertyMapper::qt_metacall(QMetaObject::Call call, int id, void **arguments) { // ,   ,  ,    id = QObject::qt_metacall(call, id, arguments); if (id < 0 || call != QMetaObject::InvokeMetaMethod) return id; Q_ASSERT(id < m_properties.size()); 

Get the previously saved information about the property:
  property_t &p = m_properties[id]; 

Focus with quick property: The signal about changing the quick property is smthChanged () without the actual value, we get it manually. And then we just call the method specified when creating a class object (we cannot generate a signal, because we did not add the Q_OBJECT macro, of course, we can do it without it, but why complicate things unnecessarily ...):
  QVariant value; if (p.isQuickProperty) { value = p.mappingObject->property(p.mappingName); } else { const void *data = arguments[1]; value = QVariant(p.type, data); } if (value != p.lastValue) { p.lastValue = value; QMetaObject::invokeMethod(m_mapTo, m_toMethod, Q_ARG(QString, p.name), Q_ARG(QVariant, value)); } return -1; } 

We also keep the last value of the property, this can not be done only if we have one client and one server, but if there are many participants, then changing the property from the outside can lead to an avalanche effect (namely, multiple installation of this property at the same time). value).

A small example of use:
 Reciever reciever; PropertyMapper mapper(&reciever, "mapped"); Tester tester; mapper.addProperty("value_m", "value", &tester); mapper.addProperty("name_m", "name", &tester); tester.setName("Button1"); tester.setValue(123); 

Tester only contains two properties, and Reciever has the following method:
 Q_INVOKABLE void mapped(const QString &propertyName, const QVariant &newValue) { qDebug() << propertyName << newValue; } 

Run:
"Name_m" QVariant (QString, "Button1")
"Value_m" QVariant (int, 123)

That's all for now :)

Whole class

Source: https://habr.com/ru/post/198270/


All Articles