Qt Meta System over Network. Часть 1 — свойства

в 18:18, , рубрики: metasystem, properties, qt, Qt Software, signalslots, метки: , , ,

Qt Meta System over Network. Часть 1 — свойства
У меня с завидной регулярностью появляется задача написания клиент-серверных приложений с использованием Qt. И я подумал – почему бы не упростить этот процесс? В самом деле, зачем каждый раз изобретать какой-то новый протокол, если можно использовать привычные сигналы и слоты? Что-то подобное уже есть, например D-Bus или QRemoteSignal, но мне они показались не очень удобными, да и некоторых возможностей в них нет.

Согласитесь, было бы очень удобно писать как-то так:
Компьютер 1:

// делаем доступным для изменения свойство xValue
net.addProprety("value", "xValue", object);
// добавляем сигнал
net.addSignal("started", object, SIGNAL(started(int, QString))); 
// добавляем функцию
net.addFunction("start", object, "method_name");

Компьютер 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 – это какой либо абстрактный интерфейс доступа к сети

Так же без труда можно написать методы, делающие доступными сразу целый список свойств или сигналов объекта. И это ещё не всё! Можно легко приделать ко всему этому Qt Quick. В общем, разобравшись с тем, как же всё-таки узнавать об изменении свойств, ловить сигналы, и выполнять любые слоты во время выполнения с любыми типами, можно сделать очень многое.
Начнём с наиболее простого – свойств.

1. Свойства

Сначала рассмотрим как изменять любые свойства динамически, и что более важно получать сигналы об их изменении вида: <имя свойства, новое значение>

Именно в имени свойства и заключается проблема — если подключать сигнал об изменении всех свойств к одному слоту, то мы не сможем узнать имя изменившегося свойства. Мы можем узнать только, кто отправил это изменение с помощью функции sender(), а вот имя свойства таким образом никак не узнать. Тут сразу приходит на ум создавать для каждого свойства объект какого-либо класса, который будет хранить имя свойства, принимать сигнал об его изменении, и генерировать новый сигнал, но уже с именем.

Метод «в лоб»

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);
}

Теперь если мы хотим узнавать об изменении свойств p1, p2 объекта object, мы можем написать следующий код:

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));

Далее просто подключаемся к сигналу PropertyMapper::mapped и получаем сигналы с именем свойства и его новым значением. Но тут сразу видно очевидные проблемы: бесполезная трата памяти и ресурсов процессора, а так же, что наверное более важно, невозможность работать со свойствами других типов без создания дополнительных слотов под каждый тип, преобразований, и т.д. В общем есть гораздо более элегантный способ, решающий все эти проблемы разом.

Продвинутый метод

Для начала давайте разберемся, как происходит вызов слота, и как работает функция QObject::connect().
Вызов слота
Макрос Q_OBJECT добавленный в объявление класса приводит (помимо всего остального) к добавлению метода qt_metacall(). Именно через него и вызываются слоты, устанавливаются свойства. Причём все проверки на существование слота, приведение аргументов реализованы именно в ней. Стандартная реализация выглядит приблизительно так:

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

Вкратце посмотрим на выполняемые этой функцией действия:
1) Преобразование имён сигналов и слотов в нормализованный вид, т.е. удаление лишних пробелов, и некоторые другие преобразования (подробнее QMetaObject::normalizedSignature())
2) Проверка соответствия типов
3) Вычисление индексов слотов и сигналов по их именам, при помощи object->metaObject()->indexOfSlot(indexOfSignal) ()
4) И самое интересное – соединение сигнала со слотом по индексам при помощи QMetaObject::connect().
Я думаю, многие уже догадались, что нужно сделать – написать свою реализацию qt_metacall и подключиться к сигналу об изменении свойства вручную. Приступим:

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;
};

Не буду расписывать для чего нужны все поля, сейчас всё станет понятным. Рассмотрим ключевые моменты по кусочкам (целиком можно скачать в конце статьи).

Добавление свойства с именем propertyName, при этом все действия будут происходить со свойством mappingPropertyName объекта mappingObject. Если мы хотим сделать данный фокус с Qt Quick свойством, необходимо установить isQuickProperty в true (дальше станет понятно как это сделано).

Для начала проверяем нет ли свойства с таким же именем. (m_propertyIndices содержит пары имя_свойства – индекс_свойства):

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;
    }

Получаем индекс свойства, и далее по индексу QMetaProperty:

    int propertyIdx =
            mappingObject->metaObject()->indexOfProperty(mappingPropertyName);
    QMetaProperty metaProperty = mappingObject->metaObject()->property(propertyIdx);

Сохраняем информацию о добавленном свойстве:

    int id = m_properties.size();
    m_propertyIndices[propertyName] = id;
    m_properties.push_back({propertyName, metaProperty.type(),
                            mappingPropertyName, mappingObject,
                            isQuickProperty, QVariant()});

Теперь самое интересное – получаем индекс сигнала об изменении свойства, и подключаемся к нему, проверка типов не выполняется, т.к. мы сохраняем тип свойства (metaProperty.type()) и будем приводить к нему полученное значение свойства:

    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;
}

И самое главное – 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());

Получаем сохранённую ранее информацию о свойстве:

    property_t &p = m_properties[id];

Фокус с quick свойством: т.к. сигнал об изменении quick свойства имеет вид smthChanged() т.е. без собственно значения, получаем его вручную. А далее просто вызываем указанный при создании объекта класса метод (мы не можем сгенерировать сигнал, т.к. не добавили макрос Q_OBJECT, конечно можно сделать и без него, но зачем всё усложнять без необходимости…):

    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;
}

Так же мы храним последнее значение свойства, этого можно не делать, только если у нас один клиент и один сервер, а вот если участников много, то изменение свойства извне может привести к лавинному эффекту (а именно к многократной установке это свойства в одно и тоже значение).

Небольшой пример использования:

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 — всего лишь содержит два свойства, а Reciever следующий метод:

Q_INVOKABLE void mapped(const QString &propertyName, const QVariant &newValue)
{
    qDebug() << propertyName << newValue;
}

Запускаем:

«name_m» QVariant(QString, «Button1»)
«value_m» QVariant(int, 123)

На сегодня всё:)

Класс целиком

Автор: romixlab

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js