将QML信号连接到C++11 lambda槽(Qt 5)

20

将 QML 信号连接到常规的 C++ 槽很容易:

// QML
Rectangle { signal foo(); }

// C++ old-style
QObject::connect(some_qml_container, SIGNAL(foo()), some_qobject, SLOT(fooSlot()); // works!
无论我尝试什么,似乎都无法连接到一个C++11 lambda函数槽。
// C++11
QObject::connect(some_qml_container, SIGNAL(foo()), [=]() { /* response */ }); // fails...
QObject::connect(some_qml_container, "foo()", [=]() { /* response */ }); // fails...

两次尝试都因函数签名错误而失败 (没有QObject::connect重载能够接受这些参数)。然而,Qt 5文档暗示这应该是可能的。

不幸的是,Qt 5示例总是将C++信号连接到C++ lambda槽:

// C++11
QObject::connect(some_qml_container, &QMLContainer::foo, [=]() { /* response */ }); // works!

由于在编译时不知道QMLContainer::foo的签名(而手动声明QMLContainer::foo会失去使用QML的目的),因此此语法无法用于QML信号。

我正在尝试的事情是否可能?如果是,QObject::connect调用的正确语法是什么?

3个回答

6

你可以使用一个助手函数:

class LambdaHelper : public QObject {
  Q_OBJECT
  std::function<void()> m_fun;
public:
  LambdaHelper(std::function<void()> && fun, QObject * parent = {}) :
    QObject(parent),
    m_fun(std::move(fun)) {}
   Q_SLOT void call() { m_fun(); }
   static QMetaObject::Connection connect(
     QObject * sender, const char * signal, std::function<void()> && fun) 
   {
     if (!sender) return {};
     return connect(sender, signal, 
                    new LambdaHelper(std::move(fun), sender), SLOT(call()));
   }
};

然后:

LambdaHelper::connect(sender, SIGNAL(mySignal()), [] { ... });
< p >发送者拥有帮助对象,并将在其销毁时清理它。


...现在你有一个内存泄漏。 - Teimpz
1
@Teimpz 一点也不! - Kuba hasn't forgotten Monica

6
Lambdas等仅适用于新语法。如果您找不到将QML信号作为指针传递的方法,那么我认为这是不可能的。如果是这样,您可以使用变通方法:创建一个虚拟信号路由的QObject子类,该类只有信号,每个信号对应您需要路由的QML信号。然后,使用旧的connect语法将QML信号连接到此虚拟类的相应信号上的实例。现在您有了C++信号,可以使用新语法并连接到lambda表达式。该类还可以具有辅助方法,以自动连接从QML到类信号的连接,该方法将利用QMetaObject反射机制和适当的信号命名方案,使用与QMetaObject :: connectSlotsByName相同的原理。或者,您可以只是硬编码QML路由器信号连接,但仍将其隐藏在路由器类的方法中。未经测试...

谢谢您的回答,这给了我一个新的方向来寻找答案:是否可能获得指向QML信号的C++指针?如果可以,我可以将std::function绑定到信号上,将lambda绑定到槽上。不幸的是,将每个QML信号镜像到C++ QObject中(可以说)比仅在QObjects中定义槽(即老派方法)更糟糕。我想做的是避免完全使用QObjects,利用新的Qt 5接口(可能或可能不可能)。 - The Fiddler
拥有信号-信号连接自动发生可能意味着只需要一行额外的代码,例如在声明QML查看器后添加此行:MyQMLSignalRouter qmlSignals(&myQmlView.rootObject());,然后在新样式的connect调用中使用qmlSignals。 QML信号不存在于C ++函数中,它们无法(它们是动态的,C ++是静态的),因此我理解,直接获取它们的方法指针甚至在理论上也不可能。 - hyde
我对这种方法持怀疑态度,因为它引入了QML信号和C++代码之间的紧密耦合,以及单一的“超级类”方法(一个类在任何地方声明所有信号)。这闻起来不太好!您完全正确,QML信号在静态情况下无法提供给C++。但是可能存在动态解决方案:QQuickItem::metaObject()->indexOfSignal("foo()")可以正确返回该信号的索引。据我所知,获取可调用包装器的管道也存在,但隐藏在QtPrivate命名空间中。遗憾。 - The Fiddler
好的,你需要编写静态C++代码来调用connect以连接lambda。此时,使用autoconnect,如果您第一次连接该信号,则只需将该信号添加到路由器类中(在.h文件中添加一行)。如果在编译时不知道信号名称,则需要在路由器类中使用占位符信号,并进行两个连接(从QML到占位符信号的动态旧式连接,从占位符信号到lambda的静态新式连接)。我同意这有点恶心,但是lambda具有闭包的好处,如果适用于您的代码,那么我会说去试试。 - hyde
1
经过更多的思考,我最终实现了你建议的大部分内容。它可以工作,并且让 QML 端对 C++ 的细节无需关心(更加清晰和易于测试)。通过一些努力,也许可以创建一个多态的 SignalRouter 类来避免事先指定每个 slot。谢谢! - The Fiddler
@The-Fiddler,您可以编辑答案并发布示例代码吗? - Ross Rogers

2

如果不想为处理不同的信号创建lambda函数,您可以考虑使用QSignalMapper来拦截信号并将其发送到具有依赖于源的参数的静态定义的插槽。这个函数的行为完全取决于原始信号的来源。

QSignalMapper的折衷是,您获得了关于信号来源的信息,但失去了原始参数。如果您不能承受丢失原始参数,或者如果您不知道信号的来源(如QDBusConnection::connect()信号的情况),那么使用QSignalMapper就没有意义。

hyde的例子需要更多的工作,但可以让您实现一个更好的版本的QSignalMapper,其中在将信号连接到槽函数时可以向参数添加关于源信号的信息。

QSignalMapper类引用:http://qt-project.org/doc/qt-5.0/qtcore/qsignalmapper.html
示例:http://eli.thegreenplace.net/2011/07/09/passing-extra-arguments-to-qt-slots/

以下是一个示例,通过连接到具有"app_window"objectName的顶级ApplicationWindow实例的QSignalMapper实例来涟漪信号:

for (auto app_window: engine.rootObjects()) {
  if ("app_window" != app_window->objectName()) {
    continue;
  }
  auto signal_mapper = new QSignalMapper(&app);

  QObject::connect(
    app_window,
    SIGNAL(pressureTesterSetup()),
    signal_mapper,
    SLOT(map()));

  signal_mapper->setMapping(app_window, -1);

  QObject::connect(
    signal_mapper,
    // for next arg casting incantation, see https://dev59.com/jYfca4cB1Zd3GeqPnci1
    static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped),
    [](int /*ignored in this case*/) {
      FooSingleton::Inst().Bar();
    });
  break;
}

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