Qt Meta System over Network. Часть 2 — сигналы и слоты

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

Qt Meta System over Network. Часть 2 — сигналы и слоты
Часть 1 — Свойства
Продолжаем разбираться с метасистемой Qt. В этот раз рассмотрим создание виртуальных сигналов и слотов.

1. Сигналы

А точнее подключение к любым сигналам. Тут всё очень похоже на работу со свойствами, только аргументов может быть от 0 до 10. Цель — получить класс DynamicQObject, который сможет подключиться к любому сигналу и при его активации, вызвать некоторый метод с именем сигнала и переданными аргументами в виде QVariantList. Также буду разбивать код на кусочки с комментариями, приступим:

dynamicqobject.h
class DynamicQObject : public QObject
{
public:
    DynamicQObject(QObject *mapToObject,
                   const char *signalCatchMethod,
                   QObject *parent = 0);

    bool addSlot(QObject *object,
                 const char *signal,
                 const QString &slotName);
    bool removeSlot(const QString &name);

    bool addSignal(const QString &name, QObject *object, const char *slot);
    bool removeSignal(const QString &name);
    bool activate(const QString &signalName, const QVariantList &args);

    int qt_metacall(QMetaObject::Call call, int id, void **arguments);
private:
    // virtual slots
    bool containsSlot(const QString &name);
    QObject *m_mapTo;
    const char *m_catchMethod;
    typedef struct {
        bool isEmpty; // true after removeSlot()
        QObject *object;
        int signalIdx;
        QString name; // virtual slot name
        QVector<int> parameterTypes;
    } slot_t;
    QVector<slot_t> m_slotList;

    // virtual signals
    typedef struct {
        bool isEmpty; // // true after removeSignal()
        QObject *reciever;
        int slotIdx;
        QString name;
        QVector<int> parameterTypes;
    } signal_t;
    QVector<signal_t> m_signalList;
    QHash<QString, int> m_signalHash;
    void *m_parameters[11]; // max 10 parameters + ret value
};

addSlot — создаёт новый виртуальный слот с именем slotName и подключает его к сигналу signal объекта object. Посмотрим как тут всё устроено:

bool DynamicQObject::addSlot(QObject *object,
                             const char *signal,
                             const QString &slotName)
{
    if (containsSlot(slotName))
        return false;
    if (signal[0] != '2') {
        qWarning() << "Use SIGNAL() macro";
        return false;
    }

Проверяем нет ли уже слота с таким же именем, а также наличие символа '2' в начале его имени, этот символ добавляет макрос SIGNAL().

    QByteArray theSignal = QMetaObject::normalizedSignature(&signal[1]);
    int signalId = object->metaObject()->indexOfSignal(theSignal);
    if (signalId < 0) {
        qWarning() << "signal" << signal << "doesn't exist";
        return false;
    }

    QVector<int> parameterTypes;
    QMetaMethod signalMethod = object->metaObject()->method(signalId);
    for (int i = 0; i < signalMethod.parameterCount(); ++i)
        parameterTypes.push_back(signalMethod.parameterType(i));

Также как и в прошлый раз, получаем индекс сигнала, а далее получаем по этому индексу QMetaMethod. Из которого мы по одному получаем аргументы сигнала, создаём из них вектор. Далее мы будем приводить полученные аргументы именно к этим типам.

    int slotIdx = -1;
    for (int i = 0; i < m_slotList.count(); ++i) {
        if (m_slotList[i].isEmpty == true) {
            slotIdx = i;
            break;
        }
    }
    bool addEntry = false;
    if (slotIdx == -1) {
        addEntry = true;
        slotIdx = m_slotList.count();
    }

Здесь всё совсем просто, находим пустую запись (либо создаём новую) в m_slotList, куда мы сохраним информацию о созданном слоте. Пустые записи образуются после удаления слотов ( removeSlot() ), т.к. индексы у нас привязаны к слотам, их сдвиг привёл бы к вызову неверных слотов. Можно было бы применить здесь QHash или QMap, но я посчитал, что удаляют слоты гораздо реже, чем создают, а вызывают очень часто, так что вектор явно эффективнее, т.к. у него доступ по индексу выполняется за O(1), а у QMap и QHash в худшем случае за O(logn) и O(n) соответственно.
Собственно осталось только подключиться к сигналу:

    if (!QMetaObject::connect(object, signalId,
                                this, slotIdx + metaObject()->methodCount())) {
        qWarning() << "can't connect" << signal << "signal to virtual slot";
        return false;
    }

    if (addEntry) {
        m_slotList.push_back({false, object, signalId, slotName, parameterTypes});
    } else {
        slot_t &slot = m_slotList[slotIdx];
        slot.isEmpty = false;
        slot.object = object;
        slot.signalIdx = signalId;
        slot.name = slotName;
        slot.parameterTypes = parameterTypes;
    }

    return true;
}

И сохранить всю необходимую информацию о нём.

Вызов слота

Тут всё похоже на фокус со свойствами, создаём свою реализацию qt_metacall, только у нас теперь аргументов может быть больше:

int DynamicQObject::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_slotList.size());

Проверяем, что вызывают действительно слот (метаметод), а также что индекс корректный.

    const slot_t &slotInfo = m_slotList[id];
    QVariantList parameters;
    for (int i = 0; i < slotInfo.parameterTypes.count(); ++i) {
        void *parameter = arguments[i + 1];
        parameters.append(QVariant(slotInfo.parameterTypes[i], parameter));
    }

Достаём ранее сохранённую информацию о слоте. И дальше создаём список QVariant-ов из полученный аргументов и сохранённых типов.

    QMetaObject::invokeMethod(m_mapTo, m_catchMethod,
                              Q_ARG(QString, slotInfo.name),
                              Q_ARG(QVariantList, parameters));
    return -1;
}

Осталось только вызвать указанный в конструкторе метод с именем слота, и аргументами.

В removeSlot() нет ничего интересного, так что посмотрим сразу на пример использования:

Reciever reciever;
DynamicQObject dynamic(&reciever, "signalCatched");

Tester tester;
dynamic.addSlot(&tester, SIGNAL(signal1(int,int,QString)), "myslot1");
dynamic.addSlot(&tester, SIGNAL(signal2(QPoint)), "myslot2");

tester.emitSignal1();
tester.emitSingal2();

В конструктор DynamicQObject передаем указатель на любого наследника QObject и имя метода (Q_INVOKABLE), который будет вызывать при получении любого сигнала.
В классе Reciever у нас есть для имеется есть следующий метод: Q_INVOKABLE void signalCatched(const QString &signalName, const QVariantList &args), который просто выводить в консоль свои аргументы.
А в Tester есть два сигнала с указанными аргументами, и две функции генерирующие их, думаю всё должно быть понятно. Запускаем:

«myslot1» (QVariant(int, 123), QVariant(int, 456), QVariant(QString, «str») )
«myslot2» (QVariant(QPoint, QPoint(3,4) ) )

Видим, что всё работает:) Можно использовать абсолютно любые типы известные метасистеме Qt.

2. Слоты

Т.е. создание виртуальных сигналов и подключение их к обычным слотам, тут легко перепутать одно с другим… За это отвечают 3 функции: addSignal(), removeSignal() и activate().
Рассмотрим самое интересное. Создание сигнала:

bool DynamicQObject::addSignal(const QString &name, QObject *object, const char *slot)
{
    if (slot[0] != '1') {
        qWarning() << "Use SLOT() macro";
        return false;
    }
    int slotIdx = object->metaObject()->
            indexOfSlot(&slot[1]); // without 1 added by SLOT() macro
    if (slotIdx < 0) {
        qWarning() << slot << "slot didn't exist";
        return false;
    }

Как обычно, проверяем, что всё идёт как надо.

    QVector<int> parameterTypes;
    QMetaMethod slotMethod = object->metaObject()->method(slotIdx);
    for (int i = 0; i < slotMethod.parameterCount(); ++i)
        parameterTypes.push_back(slotMethod.parameterType(i));

    int signalIdx = -1;
    for (int i = 0; i < m_slotList.count(); ++i) {
        if (m_slotList[i].isEmpty == true) {
            signalIdx = i;
            break;
        }
    }
    bool addEntry = false;
    if (signalIdx == -1) {
        addEntry = true;
        signalIdx = m_signalList.count();
    }

Аналогично создаём вектор типов аргументов.

    if (!QMetaObject::connect(this, signalIdx + metaObject()->methodCount(),
                              object, slotIdx)) {
        qWarning() << "can't connect virtual signal" << name << "to slot" << slot;
        return false;
    }

    if (addEntry) {
        m_signalList.append({false, object, slotIdx, name, parameterTypes});
    } else {
        signal_t &signal = m_signalList[signalIdx];
        signal.isEmpty = false;
        signal.reciever = object;
        signal.slotIdx = slotIdx;
        signal.name = name;
        signal.parameterTypes = parameterTypes;
    }
    m_signalHash.insert(name, signalIdx);

    return true;
}

И снова всё почти также — подключаем наш виртуальный сигнал к слоту, а также сохраняем в m_signalHash пару имя — индекс сигнала. Таким образом при активации сигнала, мы получим его индекс из имени.
Удаление рассматривать не будем, а то и так очень много кода…

Активация сигнала

Тут уже есть новые моменты, а именно приведение типов.

bool DynamicQObject::activate(const QString &signalName, const QVariantList &args)
{
    int signalIdx = m_signalHash.value(signalName, -1);
    if (signalIdx == -1) {
        qWarning() << "signal" << signalName << "doesn't exist";
        return false;
    }

    signal_t &signal = m_signalList[signalIdx];

Проверяем, что сигнал существует и извлекаем сохранённую ранее информацию.

    if (args.count() < signal.parameterTypes.count()) {
        qWarning() << "parameters count mismatch:" << signalName
                   << "provided:" << args.count()
                   << "need >=:" << signal.parameterTypes.count();
        return false;
    }

Проверяем, что аргументов достаточно, лишние просто отбросим…

    QVariantList argsCopy = args;

    for (int i = 0; i < signal.parameterTypes.count(); ++i) {
        if (!argsCopy[i].convert(signal.parameterTypes[i])) {
            qWarning() << "can't cast parameter" << i << signalName;
            return false;
        }
        m_parameters[i + 1] = argsCopy[i].data();
    }

Создаём копию списка аргументов, т.к. он константный, а нам нужно приводить типы, т.е. менять его. А далее пробуем привести каждый аргумент к нужному типу. Так, например, можно вызвать слот с аргументом типа int, передав activate аргумент типа QString, главное чтобы строка содержала число.
Пример:

    DynamicQObject dynamic(&reciever, "signalCatched");

    Reciever reciever;
    dynamic.addSignal("virtual_signal", &reciever, SLOT(slot1(int,QString)));

    dynamic.activate("virtual_signal", QVariantList() << "123" << QString("qwerty") << 2 << 3);

Соответственно Reciever содержит обычный слот. Мы создаём виртуальный сигнал и вызываем его со списком аргументов.
Вот что получается:

slot1_call 123 «qwerty»

Два лишних аргумента отбросились, а для первого было выполнено преобразование типов, не уверен, что это очень нужная вещь, но тут она получается сама по себе, хотя можно и запретить подобное поведение…

Пока всё. Далее разберёмся с методами, и прикрутим простенький сетевой интерфейс…

P.S. В написании данного класса, очень помогла статья: Dynamic Signals and Slots, из которой можно узнать много всего интересного.

Автор: romixlab

Источник

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


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