QObject通用信号处理程序

7
(使用“信号处理程序”一词,我指的是槽,而不是用于POSIX信号的处理程序。)
我需要将未知子类的QObject实例的所有信号可能不直接使用QObject :: connect )连接到另一个QObject的单个槽上。 我需要这样做是为了通过网络发送信号及其参数(用于支持信号的自己的RPC系统)。
(通过“未知”我意思是我的代码应该尽可能通用。因此,它不应该包含对我在RPC系统中使用的每个类中的每个信号的connect语句,而是提供类似 RPC :: connectAllSignals(QObject *); 的内容,然后在运行时扫描所有信号并连接它们。)
我想要实现的是:处理所有信号并对其进行序列化(信号名称+参数)。 我已经可以对参数进行序列化,但我不知道如何获取信号名称。 经过谷歌搜索,似乎不可能像QObject实例那样使用类似的东西 sender()。 因此,我需要做一些更复杂的事情。
我当前用于将参数传递到远程端目标函数的类型系统是受限制的。 (那是因为我需要 qt_metacall ,它希望参数为 void * 类型,后面是“正确的类型”。我的RPC系统在内部使用只有几种类型的QVariants,并使用自定义方法将它们转换为 void * 的正确类型。我听说过 QVariant :: constData 太晚了,已经无法使用,而且它可能也不适合;因此,如果没有缺点,我将坚持自己的类型转换。)
所有信号应映射到的目标槽应类似于:
void handleSignal(QByteArray signalName, QVariantList arguments);

最好的解决方案是支持C++03,所以如果不使用可变参数模板,则只需使用C++11。在这种情况下,C++11也可以,因此我也很高兴看到使用C++11的答案。
现在我正在考虑的问题的可能解决方案:
我可以使用其QMetaObject扫描对象的所有信号,然后为每个信号创建一个QSignalMapper(或类似传递所有参数的东西)。这很容易,我不需要在这部分得到帮助。如前所述,我已经受到某些类型参数的限制,并且我也可以接受对参数计数的限制。
听起来像是一个肮脏的hack,但我可以使用一些自定义的基于模板的信号映射器,例如这样(在这个例子中有三个参数):
template<class T1, class T2, class T3>
class MySignalMapper : public QObject
{
    Q_OBJECT
public:
    void setSignalName(QByteArray signalName)
    {
        this->signalName = signalName;
    }
signals:
    void mapped(QByteArray signalName, QVariantList arguments);
public slots:
    void map(T1 arg1, T2 arg2, T3 arg3)
    {
        QVariantList args;
        // QVariant myTypeConverter<T>(T) already implemented:
        args << myTypeConverter(arg1);
        args << myTypeConverter(arg2);
        args << myTypeConverter(arg3);
        emit mapped(signalName, args);
    }
private:
    QByteArray signalName;
};

那么我可以像这样连接一个名为method(已知为信号)的QMetaMethod到一个名为obj的QObject上(可能是使用某种脚本生成所有支持的类型和参数计数的...是的...变得有点复杂了!):

    // ...
}
else if(type1 == "int" && type2 == "char" && type3 == "bool")
{
    MySignalMapper<int,char,bool> *sm = new MySignalMapper<int,char,bool>(this);
    QByteArray signalName = method.signature();
    signalName = signalName.left(signalName.indexOf('(')); // remove parameters
    sm->setMember(signalName);

    // prepend "2", like Qt's SIGNAL() macro does:
    QByteArray signalName = QByteArray("2") + method.signature();

    // connect the mapper:
    connect(obj, signalName.constData(),
            sm, SLOT(map(int,char,bool)));
    connect(sm, SIGNAL(mapped(int,char,bool)),
            this, SLOT(handleSignal(const char*,QVariantList)));
}
else if(type1 == ...)
{
    // ...

虽然这种方法可能行得通,但它确实是一个不太优雅的解决方案。我需要大量的宏来覆盖最多N个参数的所有类型组合(其中N约为3到5,尚未确定),或者一个简单的脚本为所有情况生成代码。问题在于,这将涉及很多情况,因为我支持每个参数大约70种不同的类型(10种基本类型+嵌套列表和深度为2的映射,针对每种类型都有)。因此,对于参数计数限制为N,需要覆盖N ^ 70个情况!

是否有完全不同的方法可以实现这个目标,而我却忽视了?


更新:

我自己解决了这个问题(请参见答案)。如果您对完整的源代码感兴趣,请查看我刚刚发布的RPC系统的bitbucket存储库:bitbucket.org/leemes/qtsimplerpc


1
首先,它不会起作用,因为您无法使 Q_OBJECT 类成为模板。 - Lol4t0
1
你可能需要查找的一个关键词是“Spy”(如Spy ++)......作为先例(尽管不一定是你所需的)......像QSignalSpy和http://qt-apps.org/content/show.php/Conan+-+Connection+analyzer+for+Qt?content=108330这样的东西。 - HostileFork says dont trust SE
@Lol4t0 哦天啊,我完全忘了(没有测试它)。但是如果我需要硬编码每种类型的组合,我可以通过信号处理程序这样做。肮脏肮脏肮脏... - leemes
@HostileFork 谢谢您提供的链接,这让我找到了一个可能的解决方案,正如我在这个问题的答案中所描述的那样。让我们看看它是否有效。 - leemes
@HostileFork 请查看我的更新答案,基于您的评论提供了解决方案。我希望我能在这方面给予您信誉值... - leemes
@leemes 很高兴能帮到你。别担心,你才是做工作的人... 我基本上忽略“分数”这个东西。 :) - HostileFork says dont trust SE
3个回答

5

在问题评论中,HostileFork建议我查看Conan的代码后,我找到了我的问题的解决方案:

我编写了一个自定义的qt_static_metacall来帮助QObject,并使用自定义的moc输出文件(将生成的文件移动到我的源文件中,并在之后从我的.pro文件中删除类的头文件)。我需要小心,但它似乎比我在问题中提出的解决方案要好得多。

对于一个具有一些槽的类,例如这里的两个槽exampleA(int)exampleB(bool),定义如下:

void ClassName::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Q_ASSERT(staticMetaObject.cast(_o));
        ClassName *_t = static_cast<ClassName *>(_o);
        switch (_id) {
        case 0: _t->exampleA((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 1: _t->exampleB((*reinterpret_cast< bool(*)>(_a[1]))); break;
        default: ;
        }
    }
}

正如您所看到的,它将调用重定向到由被调用者提供的对象指针上的“真实”方法。
我创建了一个没有任何参数的带有一些插槽的类,它将被用作我们想要检查的信号的目标。
class GenericSignalMapper : public QObject
{
    Q_OBJECT
public:
    explicit GenericSignalMapper(QMetaMethod mappedMethod, QObject *parent = 0);
signals:
    void mapped(QObject *sender, QMetaMethod signal, QVariantList arguments);
public slots:
    void map();
private:
    void internalSignalHandler(void **arguments);
    QMetaMethod method;
};

插槽map()在实际中从未被调用,因为我们通过将自己的方法放入qt_static_metacall中来进入此调用过程(请注意,ID为0的元方法是我在下一节中解释的另一个信号,因此修改后的方法是case 1)。
void GenericSignalMapper::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        Q_ASSERT(staticMetaObject.cast(_o));
        GenericSignalMapper *_t = static_cast<GenericSignalMapper *>(_o);
        switch (_id) {
        case 0: _t->mapped((*reinterpret_cast< QObject*(*)>(_a[1])),(*reinterpret_cast< QMetaMethod(*)>(_a[2])),(*reinterpret_cast< QVariantList(*)>(_a[3]))); break;
        case 1: _t->internalSignalHandler(_a); break;
        default: ;
        }
    }
}

我们所做的是:我们只是将未经解释的参数数组传递给我们自己的处理程序,因为我们无法确定其类型(甚至数量)。我定义了以下处理程序:
void GenericSignalMapper::internalSignalHandler(void **_a)
{
    QVariantList args;
    int i = 0;
    foreach(QByteArray typeName, method.parameterTypes())
    {
        int type = QMetaType::type(typeName.constData());

        QVariant arg(type, _a[++i]); // preincrement: start with 1
                                     // (_a[0] is return value)
        args << arg;
    }
    emit mapped(sender(), method, args);
}

最后,其他类可能会连接到mapped信号,该信号将提供发送者对象、信号作为QMetaMethod(从中我们可以读取名称)和参数作为QVariants。
这不是一个完整的解决方案,但最后一步很容易:对于要检查的类的每个信号,我们创建一个GenericSignalMapper,提供信号的元方法。我们将map连接到对象,并将mapped连接到最终接收器,然后能够通过源对象处理(和区分)所有发射的信号。 我仍然有问题将void*参数转换为QVariants。已修复。_a还包括索引0处的返回值占位符,因此参数从索引1开始。

示例:

在此示例中,“最终步骤”(为每个信号创建和连接映射器)是手动完成的。

要检查的类:

class Test : public QObject
{
    Q_OBJECT
public:
    explicit Test(QObject *parent = 0);

    void emitTestSignal() {
        emit test(1, 'x');
    }

signals:
    void test(int, char);
};

最终的处理程序类通过映射接收所有信号:

class CommonHandler : public QObject
{
    Q_OBJECT
public:
    explicit CommonHandler(QObject *parent = 0);

signals:

public slots:
    void handleSignal(QObject *sender, QMetaMethod signal, QVariantList arguments)
    {
        qDebug() << "Signal emitted:";
        qDebug() << "  sender:" << sender;
        qDebug() << "  signal:" << signal.signature();
        qDebug() << "  arguments:" << arguments;
    }
};

我们创建对象并将它们连接的代码如下:
CommonHandler handler;

// In my scenario, it is easy to get the meta objects since I loop over them.
// Here, 4 is the index of SIGNAL(test(int,char))
QMetaMethod signal = Test::staticMetaObject.method(4);

Test test1;
test1.setObjectName("test1");
Test test2;
test2.setObjectName("test2");

GenericSignalMapper mapper1(signal);
QObject::connect(&test1, SIGNAL(test(int,char)), &mapper1, SLOT(map()));
QObject::connect(&mapper1, SIGNAL(mapped(QObject*,QMetaMethod,QVariantList)), &handler, SLOT(handleSignal(QObject*,QMetaMethod,QVariantList)));

GenericSignalMapper mapper2(signal);
QObject::connect(&test2, SIGNAL(test(int,char)), &mapper2, SLOT(map()));
QObject::connect(&mapper2, SIGNAL(mapped(QObject*,QMetaMethod,QVariantList)), &handler, SLOT(handleSignal(QObject*,QMetaMethod,QVariantList)));

test1.emitTestSignal();
test2.emitTestSignal();

输出:

Signal emitted: 
  sender: Test(0xbf955d70, name = "test1") 
  signal: test(int,char) 
  arguments: (QVariant(int, 1) ,  QVariant(char, ) )  
Signal emitted: 
  sender: Test(0xbf955d68, name = "test2") 
  signal: test(int,char) 
  arguments: (QVariant(int, 1) ,  QVariant(char, ) ) 

(char参数无法正确打印,但是它在QVariant中存储正确。其他类型都可以正常工作。)

如果您对完整的源代码感兴趣,请查看我刚刚发布在Bitbucket上的RPC系统存储库:https://bitbucket.org/leemes/qtsimplerpc - leemes

1
我正在寻找一个通用的信号处理程序,原因是通过RPC转发信号调用。在QtDevDays presentation中有一个非常有趣和详细的QObject-QMetaObject魔法的描述。特别是,他们还描述了检查通用信号以进行调试或与脚本语言进行接口的愿望-因此这是一篇完美的阅读材料。
长话短说:您的解决方案是修改moc代码中的qt_static_metacall。(现在在Qt5中?)同样的事情可以通过子类化基于QObject的类并覆盖qt_metacall来实现,例如:
class QRpcService : public QRpcServiceBase
{
public:
    explicit QRpcService(QTcpServer* server, QObject *parent = 0);
    virtual ~QRpcService();

    virtual int qt_metacall(QMetaObject::Call, int, void**);
private:
    static int s_id_handleRegisteredObjectSignal;
};

魔法捕获所有插槽是在基类中定义的虚拟方法(这里是void handleRegisteredObjectSignal()),它什么都不做。我在构造函数中查询其元方法ID,并将其存储为static int,以避免每次搜索。
在自定义元调用处理程序中,您拦截对魔法捕获所有插槽的调用,并检查发送方对象和信号。这提供了转换void**参数为QVariant列表所需的所有类型信息。
int QRpcService::qt_metacall(QMetaObject::Call c, int id, void **a)
{
    // only handle calls to handleRegisteredObjectSignal
    // let parent qt_metacall do the rest
    if (id != QRpcService::s_id_handleRegisteredObjectSignal)
        return QRpcServiceBase::qt_metacall(c, id, a);

    // inspect sender and signal
    QObject* o = sender();
    QMetaMethod signal = o->metaObject()->method(senderSignalIndex());
    QString signal_name(signal.name());

    // convert signal args to QVariantList
    QVariantList args;
    for (int i = 0; i < signal.parameterCount(); ++i)
        args << QVariant(signal.parameterType(i), a[i+1]);

    // ...
    // do whatever you want with the signal name and arguments
    // (inspect, send via RPC, push to scripting environment, etc.)
    // ...

    return -1;
}

我只在这个方法中处理了所有内容,但你也可以将收集到的所有信息重新发出到另一个信号,并在运行时附加到该信号上。
如果有人感兴趣,我也在这里设置了一个存储库来存放我的解决方案。

1

您可以通过参数进行通用调度,而SLOT/SIGNAL只是字符串,因此伪造它们并不成问题。关键在于创建一个模板函数,将每个参数传递到调度中并合并所有结果。如果使用c++11,甚至可以具有无限数量的参数。


我没有完全理解你的意思。你所说的“通用分派”是什么意思?这是一个常见的术语吗?你能给我提供一个链接让我可以阅读一下吗?我猜它可能与如何实现可变参数模板有关,但我也可能错了 :) - leemes
最好支持C++03,因此我可能会坚持参数计数限制。这方面有好的解决方案吗? - leemes
@leemes:创建一个函数,对于每个参数类型都进行重载,该函数将序列化类型信息和特定参数,并在模板函数中为每个参数调用该函数,它将根据类型进行选择。 - Daniel
这就是我在信号映射器的插槽中所做的。但如果我没记错的话,我需要在插槽的签名中编写类型(或使用模板)。 - leemes
如果我找到了另一个解决方案,如果我的解决方案不起作用,我会来找你的。到目前为止,谢谢。 - leemes
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接