简体   繁体   English

QObject通用信号处理程序

[英]QObject generic signal handler

(With "signal handler" I mean slots, not handlers for POSIX signals.) (“信号处理程序”是指插槽,而不是POSIX信号的处理程序。)

I need to "connect" ( probably not using QObject::connect directly) all signals from an instance of a (not yet known) subclass of QObject to one single slot of another QObject. 我需要将来自QObject::connect的一个(尚未知道的)子类的实例的所有信号 “连接”( 可能不直接使用QObject::connect )到另一个QObject的单个插槽 I need this in order to send the signal (with arguments) over network (for an own RPC system with support for signals). 我需要这个以便通过网络发送信号(带参数)(对于支持信号的自己的RPC系统)。

(With "not yet known" I mean that my code should be as generic as possible. So it souldn't contain a connect statement for each signal in each class I'm using with my RPC system, but provide something like RPC::connectAllSignals(QObject*); , which then scanns all signals during runtime and connects them.) (“还不知道”我的意思是我的代码应该尽可能通用。所以它不能包含我在RPC系统中使用的每个类中的每个信号的connect语句,但提供类似RPC::connectAllSignals(QObject*);然后在运行时扫描所有信号并连接它们。)

What I'd like to achieve is: Handle all signals and serialise them (signal name + arguments). 我想要实现的是:处理所有信号并将它们串行化(信号名称+参数)。 I already can serialise the arguments, but I don't know how to get the signal name. 我已经可以序列化参数,但我不知道如何获取信号名称。 After googling, it seems to be impossible to use something similar like there is sender() for the QObject instance. 在谷歌搜索之后,似乎不可能使用类似于QObject实例的sender()类的东西。 So I need to do something far more complicated. 所以我需要做一些更复杂的事情。

My current type system for passing the arguments to a target function on the remote end is restricted to some types anyway. 我当前用于将参数传递给远程端目标函数的类型系统无论如何都限于某些类型。 (That's because I need qt_metacall , which excepts the arguments to be of type void* with the "correct types" behind them. My RPC system uses QVariants with only a couple of types internally and I convert them to void* of the correct types using custom methods. I heard about QVariant::constData too late to use it, and it probably won't fit anyway; so I will stick to my type conversion if there is no drawback.) (那是因为我需要qt_metacall ,它除了参数类型为void* ,后面有“正确的类型”。我的RPC系统在内部只使用几种类型的QVariants,并使用正确的类型将它们转换为void*自定义方法。我听说QVariant::constData使用起来太迟了,反正可能也不适合;所以如果没有缺点,我会坚持我的类型转换。)

The target slot, where all signals should be mapped to, should look similar to this: 应将所有信号映射到的目标槽应类似于:

void handleSignal(QByteArray signalName, QVariantList arguments);

It would be best if the solution is supported by C++03, so I only want to use variadic templates if it is a big drawback to not use them. 最好是C ++ 03支持该解决方案,所以我只想使用可变参数模板,如果不使用它们是一个很大的缺点。 In this case C++11 is OK, so I'm also happy about answers using C++11. 在这种情况下,C ++ 11没问题,所以我也对使用C ++ 11的答案感到满意。


Now my possible solution to the question I'm thinking about: 现在我可以解决我正在考虑的问题:

I could scan all signals of the object using its QMetaObject and then creating a QSignalMapper (or something similar which passes all arguments) for each signal. 我可以使用其QMetaObject扫描对象的所有信号,然后为每个信号创建一个QSignalMapper (或类似的传递所有参数的东西)。 This is easy, and I need no help on this part. 这很容易,我不需要这方面的帮助。 As mentioned before, I'm already restricted to some types for arguments, and I can also live with a restriction on the argument count. 如前所述,我已经限制了一些类型的参数,我也可以对参数计数有限制。

It sounds like a dirty hack, but I could use some sort of custom, template-based signal mappers like this (in this example for three arguments): 这听起来像是一个肮脏的黑客,但我可以使用某种自定义的,基于模板的信号映射器(在这个例子中有三个参数):

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

Then I could connect a QMetaMethod called method (which is known to be a signal) of a QObject called obj like this (which might be generated using some sort of script for all supported types and argument counts... yeah... it's getting dirty! ): 然后我可以连接一个名为obj的QMetaMethod调用method (已知是一个信号),这个可以使用某种类型的脚本为所有支持的类型和参数计数生成...是的... 它正在脏! ):

    // ...
}
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 == ...)
{
    // ...

As this may work , it really is a dirty solution. 由于这可能有效 ,它确实是一个肮脏的解决方案。 I'd need either a lot of macros to cover all combinations of types for at most N arguments (where N is about 3 to 5, not yet known), or a simple script generating the code for all cases. 我需要很多宏来覆盖最多N参数的所有类型组合(其中N大约是3到5,尚不知道),或者是为所有情况生成代码的简单脚本。 The problem is that this will be a lot of cases, as I'm supporting about 70 different types per argument (10 primitive types + nested lists and maps with depth 2 for every type of them). 问题是这将是很多情况,因为我支持每个参数约70种不同的类型(10种基本类型+嵌套列表和深度为2的每种类型的映射)。 So for an argument count limit of N there are N ^ 70 cases to cover! 因此,对于N的参数计数限制,有N ^ 70个案例要覆盖!

Is there a completely different approach for this objective, which I'm overlooking? 这个目标有一个完全不同的方法,我忽略了吗?


UPDATE: 更新:

I solved the problem on my own (see answer). 我自己解决了这个问题(见答案)。 If you are interested in the full source code, see my repository on bitbucket of my RPC system, which I have just published: bitbucket.org/leemes/qtsimplerpc 如果您对完整的源代码感兴趣,请参阅我刚刚发布的RPC系统的bitbucket上的我的存储库:bitbucket.org/leemes/qtsimplerpc

I found a solution for my question, after looking into the code of Conan as suggested by HostileFork in the question's comments: 在查看了HostileFork在问题评论中建议的柯南代码后,我找到了一个问题的解决方案:

I wrote a customized qt_static_metacall for a helper QObject by using a customized moc output file (by moving the generated file into my sources and removing the class' header from my .pro file afterwards). 我通过使用自定义的moc输出文件(通过将生成的文件移动到我的源中并随后从我的.pro文件中删除类'标题)为辅助QObject编写了一个自定义的qt_static_metacall I need to be careful, but it seems to be far less dirty than my suggested solution in the question. 我需要小心,但它似乎远不如我在问题中建议的解决方案那么脏。

For a class with some slots, here for example the two slots exampleA(int) and exampleB(bool) , it is defined like this: 对于具有一些插槽的类,这里例如两个插槽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: ;
        }
    }
}

As you can see, it redirects the call to the "real" method on the object pointer provided by the callee. 如您所见,它将调用重定向到被调用者提供的对象指针上的“真实”方法。

I made a class with some slot without any arguments, which will be used as the target of the signal we want to inspect. 我创建了一个没有任何参数的插槽的类,它将用作我们想要检查的信号的目标。

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

The slot map() never gets called in real, because we step in this calling process by putting our own method in the qt_static_metacall (note that the meta method with ID 0 is another signal I explain in the next section, so the modified method is the case 1 ): 插槽map()永远不会被实际调用,因为我们通过将自己的方法放在qt_static_metacall中来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: ;
        }
    }
}

What we do is: We just pass the uninterpreted argument array to our own handler, because we can't be specific about its types (or even the count). 我们所做的是:我们只是将未解释的参数数组传递给我们自己的处理程序,因为我们不能具体说明它的类型(甚至是计数)。 I defined this handler as follows: 我将此处理程序定义如下:

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

Finally, some other class may connect to the mapped signal, which will provide the sender object, the signal as a QMetaMethod (from which we can read the name) and the arguments as QVariants. 最后,其他一些类可以连接到mapped信号,它将提供发送方对象,信号为QMetaMethod(我们可以从中读取名称),参数为QVariants。

This is not a full solution, but the final step is easy: For each signal of the class to be inspected, we create a GenericSignalMapper providing the meta method of the signal. 这不是一个完整的解决方案,但最后一步很简单:对于要检查的类的每个信号,我们创建一个GenericSignalMapper,提供信号的元方法。 We connect map to the object and mapped to the final receiver, which is then able to handle (and distinguish) all emitted signals by the source object. 我们将map连接到对象并映射到最终接收器,然后能够处理(并区分)源对象发出的所有信号。

I still have problems converting the void* arguments to QVariants. 我仍然无法将void*参数转换为QVariants。 Fixed. 固定。 _a also includes a placeholder for the return value at index 0 , so arguments start at index 1 . _a还包含索引0处返回值的占位符,因此参数从索引1开始。


Example: 例:

In this example, the "final step" (create and connect mappers for each signal) is done manually. 在此示例中,“最后一步”(为每个信号创建和连接映射器)是手动完成的。

The class to be inspected: 要检查的课程:

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

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

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

The final handler class receiving all signals via the mappers: 最终的处理程序类通过映射器接收所有信号:

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

The code where we create the objects and connect them: 我们创建对象并连接它们的代码:

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

Output: 输出:

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

(The char argument doesn't get printed correctly, but it is stored in the QVariant correctly. Other types work like a charm.) char参数无法正确打印,但它正确存储在QVariant中。其他类型的工作方式类似于魅力。)

You can make a generic dispatch per argument, and about the SLOT/SIGNAL they are just strings so it's not problem to forge them. 你可以为每个参数做一个通用的调度,关于SLOT / SIGNAL,它们只是字符串,所以伪造它们不是问题。 It's all about making one template function that will pass per argument into the the dispatch and merge all the results. 这是关于制作一个模板函数,它将每个参数传递到调度并合并所有结果。 This can even have unlimited number of arguments if you use c++11. 如果你使用c ++ 11,这甚至可以有无限数量的参数。

I was looking for a generic signal handler for the same reason, ie forwarding signal calls via RPC. 我出于同样的原因寻找通用信号处理程序,即通过RPC转发信号调用。 There is a very interesting and detailed description of the QObject-QMetaObject magic in a QtDevDays presentation . QtDevDays演示文稿中有一个非常有趣和详细的QObject-QMetaObject魔术描述。 In particular, they also describe the desire to inspect generic signals for debugging or interfacing with scripting languages - so this is a perfect read. 特别是,它们还描述了检查通用信号以进行调试或与脚本语言接口的愿望 - 所以这是一个完美的阅读。

Long story short: Your solution was to modify qt_static_metacall in the moc code. 长话短说:您的解决方案是修改moc代码中的qt_static_metacall (Now in Qt5?) The same thing can be achieved by subclassing your QObject based class and overriding qt_metacall , for example: (现在在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;
};

The magic capture-all-slot is just a dummy method defined in the base class (here void handleRegisteredObjectSignal() ) that takes nothing and does nothing. 神奇的capture-all-slot只是在基类(这里是void handleRegisteredObjectSignal() )中定义的虚拟方法,它什么也不做,什么都不做。 I query its meta-method-id in the constructor and store it as static int to avoid searching for it every time. 我在构造函数中查询其meta-method-id并将其存储为static int以避免每次都搜索它。

Within this custom metacall handler you intercept the calls to your magic-capture-all slot and inspect the sender object and signal. 在此自定义元调用处理程序中,您可以拦截对magic-capture-all插槽的调用,并检查发送方对象和信号。 This provides all the type information required to convert the void** arguments to a QVariant list 这提供了将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;
}

I just handled everything within this method, but you could also re-emit all the information that was gathered in another signal and attach to that at runtime. 我刚刚处理了这个方法中的所有内容,但您也可以重新发出在另一个信号中收集的所有信息,并在运行时附加到该信息中。

If anyone is interested, I also set up a repository with my solution here . 如果有人有兴趣,我也在这里建立一个包含我的解决方案的存储库。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM