Connect QML signal to C++11 lambda slot (Qt 5)
Asked Answered
A

3

20

Connecting a QML signal to a regular C++ slot is easy:

// QML
Rectangle { signal foo(); }

// C++ old-style
QObject::connect(some_qml_container, SIGNAL(foo()), some_qobject, SLOT(fooSlot()); // works!

However, no matter what I try, I cannot seem to be able to connect to a C++11 lambda function slot.

// C++11
QObject::connect(some_qml_container, SIGNAL(foo()), [=]() { /* response */ }); // fails...
QObject::connect(some_qml_container, "foo()", [=]() { /* response */ }); // fails...

Both attempts fail with a function signature error (no QObject::connect overload can accept these parameters). However, the Qt 5 documentation implies that this should be possible.

Unfortunately, Qt 5 examples always connect a C++ signal to a C++ lambda slot:

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

This syntax cannot work for a QML signal, as the QMLContainer::foo signature is not known at compile-time (and declaring QMLContainer::foo by hand defeats the purpose of using QML in the first place.)

Is what I'm trying to do possible? If so, what is the correct syntax for the QObject::connect call?

Arthur answered 25/3, 2013 at 21:8 Comment(0)
T
6

Lambdas etc only work with new syntax. If you can't find a way to give QML signal as a pointer, then I think it is not directly possible.

If so, you have a workaround: create a dummy signal-routing QObject subclass, which only has signals, one for every QML signal you need to route. Then connect QML signals to corresponding signals of an instance of this dummy class, using the old connect syntax.

Now you have C++ signals you can use with the new syntax, and connect to lambdas.

The class could also have a helper method, to automate connections from QML to signals of the class, which would utilize QMetaObject reflection mechanisms and a suitable signal naming scheme, using same principle as QMetaObject::connectSlotsByName uses. Alternatively you can just hard-code the QML-router signal connections but still hide them inside a method of the router class.

Untested...

Tivoli answered 25/3, 2013 at 21:32 Comment(6)
Thanks for the answer, this gives me a new direction to look for an answer: is it possible to get a C++ pointer to a QML signal? If so, I can bind a std::function to the signal and a lambda to the slot. Unfortunately, mirroring every QML signal into a C++ QObject is (arguably) a worse design than defining just the slots in QObjects (i.e. the old-school approach). What I would like to do is avoid using QObjects altogether, taking advantage of the new Qt 5 interfaces (which might or might not be possible).Arthur
Well, having the signal-signal connections happen automatically could mean it's just one extra line of code, something like this line after declaring QML viewer: MyQMLSignalRouter qmlSignals(&myQmlView.rootObject()); and then use qmlSignals in new-style connect calls. The QML signals do not exist as C++ functions, they can't (they're dynamic, C++ is static) so getting a direct method pointer to them is not even theoretically possible, as far as I understand it.Tivoli
My skepticism for this approach lies in the tight-coupling between QML signals and C++ code it introduces, as well as the single "super-class" approach (one class to declare all signals, everywhere). It smells bad! You are completely right that QML signals are not available to C++ statically. However a dynamic solution might exist: QQuickItem::metaObject()->indexOfSignal("foo()") correctly returns the index of that signal. AFAICT, the plumbing for getting a callable wrapper also exists, but is hidden inside the QtPrivate namespace. Bummer.Arthur
well, you need to write the static C++ code to call connect to connect the lambda. At that point, with autoconnect, you'd just need to add that signal to the router class as well (one line to .h file), if it is first time you connect that signal. If signal name is not known at compile time, then you'd need a placeholder signal in the router class, and two connects (dynamic old-style connect from QML to placeholder signal, static new-style connect from that to lambda). I agree it is a bit nasty, but lambdas have benefits of closures, and if that applies to your code, then I'd say go for it.Tivoli
After some more deliberation, I ended up implementing pretty much what you suggest. It works and it keeps the QML side blissfully ignorant of the C++ details (cleaner and more testable). With a little work, it might be possible to create a polymorphic SingalRouter class to avoid specifying every slot beforehand. Thanks!Arthur
@The-Fiddler, Can you edit the answer and post example code?Karleenkarlen
S
6

You can use a helper:

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

Then:

LambdaHelper::connect(sender, SIGNAL(mySignal()), [] { ... });

The sender owns the helper object and will clean it up upon its destruction.

Sphinx answered 21/7, 2017 at 18:10 Comment(2)
...and now you have a memleakLimpet
@Limpet Not at all!Antisocial
L
2

Instead of creating lambda functions on the fly to deal with different signals, you may want to consider using a QSignalMapper to intercept the signals and send them to a statically-defined slot with an argument dependent on the source. The behavior of the function would then depend entirely on the source of the original signal.

The trade-off with QSignalMapper is that you gain information about the source of the signal, but you lose the original arguments. If you can't afford to lose the original arguments, or if you don't know the source of the signals (as is the case with QDBusConnection::connect() signals), then it doesn't really make sense to use a QSignalMapper.

hyde's example would require a little more work, but would allow you to implement a better version of QSignalMapper where you can add information about the source signal to the arguments when connecting the signal to your slot function.

QSignalMapper class reference: http://qt-project.org/doc/qt-5.0/qtcore/qsignalmapper.html
Example: http://eli.thegreenplace.net/2011/07/09/passing-extra-arguments-to-qt-slots/

Here is an example rippling a signal through a QSignalMapper instance connecting to a top ApplicationWindow instance with an objectName of "app_window":

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 http://stackoverflow.com/questions/28465862
    static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped),
    [](int /*ignored in this case*/) {
      FooSingleton::Inst().Bar();
    });
  break;
}
Liquorish answered 7/6, 2013 at 7:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.