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?

Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
The Fiddler
  • 2,726
  • 22
  • 28

3 Answers3

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

hyde
  • 60,639
  • 21
  • 115
  • 176
  • 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). – The Fiddler Mar 26 '13 at 08:49
  • 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. – hyde Mar 26 '13 at 09:04
  • 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. – The Fiddler Mar 26 '13 at 10:07
  • 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. – hyde Mar 26 '13 at 11:19
  • 1
    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! – The Fiddler Mar 26 '13 at 12:10
  • @The-Fiddler, Can you edit the answer and post example code? – Ross Rogers Nov 17 '16 at 19:21
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.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
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;
}
Ross Rogers
  • 23,523
  • 27
  • 108
  • 164
dmiller309
  • 385
  • 5
  • 8