【问题标题】:QObject generic signal handlerQObject 通用信号处理程序
【发布时间】:2012-06-04 00:46:35
【问题描述】:

(“信号处理程序”是指插槽,而不是 POSIX 信号的处理程序。)

我需要“连接”(可能不是直接使用QObject::connect所有信号从 QObject 的(未知)子类的实例到 另一个 QObject 的单个插槽。我需要这个来通过网络发送信号(带参数)(对于支持信号的自己的 RPC 系统)。

(对于“未知”,我的意思是我的代码应该尽可能通用。所以它不包含connect 声明,用于我在 RPC 系统中使用的每个类中的每个信号,但提供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 得到答案。


现在我对我正在考虑的问题的可能的解决方案

我可以使用它的QMetaObject 扫描对象的所有信号,然后为 each 信号创建一个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;
};

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

    // ...
}
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

【问题讨论】:

  • 首先它不会起作用,因为你不能让Q_OBJECT class template'd
  • 你可能会为这种能力寻找的关键字是“间谍”(如在 Spy++ 中)......先例(尽管不一定是你需要的)......像QSignalSpyqt-apps.org/content/show.php/…
  • @Lol4t0 天哪,我完全忘记了(没有测试)。但是如果我需要对每种类型的组合进行硬编码,我可以使用信号处理程序来完成。脏脏脏脏的……
  • @HostileFork 感谢您提供此链接,这使我找到了一个可能的解决方案,如我自己对这个问题的回答中所述。让我们看看它是否有效。
  • @HostileFork 根据您的评论查看我的更新答案以获取解决方案。我希望我能给你这方面的声誉......

标签: c++ qt metaprogramming rpc


【解决方案1】:

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

我通过使用自定义的moc 输出文件(通过将生成的文件移动到我的源中并随后从我的 .pro 文件中删除类的标题)为助手 QObject 编写了自定义的qt_static_metacall。我需要小心,但它似乎远没有我在问题中建议的解决方案那么脏。

对于一个有一些槽的类,这里以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 连接到对象并映射到最终的接收器,然后接收器能够处理(和区分)源对象发出的所有信号。

我仍然无法将 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 中。其他类型的工作就像一个魅力。)

【讨论】:

【解决方案2】:

您可以为每个参数进行通用调度,关于 SLOT/SIGNAL 它们只是字符串,因此伪造它们不是问题。这一切都是关于制作一个模板函数,它将每个参数传递给调度并合并所有结果。如果你使用 c++11,它甚至可以有无限数量的参数。

【讨论】:

  • 我没有完全理解你。 “通用调度”是什么意思?这是一个常用术语,您可以提供我可以阅读的链接吗?我想这与如何实现可变参数模板有关,但我可能错了:)
  • 最好支持C++03,所以我可能会坚持参数计数限制。有什么好的解决方案吗?
  • @leemes:为每个参数类型创建一个具有重载的函数,该函数将序列化类型信息和特定参数,并在模板函数中为每个参数调用该函数,它将按类型选择。
  • 这就是我在信号映射器的插槽中所做的。但是如果我没记错的话,我需要在插槽的签名中写类型(或使用模板)。
  • @leemes:当它在两个方向上仍然是一系列 void* 时,您可以拦截它。您还可以使用模板并为每种类型组合实例化它们,您可以有一个脚本来生成它,只需一行排列。
【解决方案3】:

出于同样的原因,我一直在寻找通用信号处理程序,即通过 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;
};

神奇的 capture-all-slot 只是在基类(此处为 void handleRegisteredObjectSignal())中定义的一个虚拟方法,它什么也不做。我在构造函数中查询它的 meta-method-id 并将其存储为 static int 以避免每次都搜索它。

在此自定义元调用处理程序中,您拦截对您的 magic-capture-all 插槽的调用并检查发送者对象和信号。这提供了将 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;
}

我只是在这个方法中处理了所有事情,但您也可以重新发送在另一个信号中收集的所有信息并在运行时附加到该信号。

如果有人感兴趣,我还用我的解决方案here 建立了一个存储库。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-23
    • 2017-11-13
    • 1970-01-01
    相关资源
    最近更新 更多