Connect signal to slot, but call the slot only once and then auto-disconnect them
Asked Answered
J

3

6

Consider this JS code:

function handleSig() {
    emitter.someSig.disconnect(handleSig);
    // do some work here
}

emitter.someSig.connect(handleSig);

Can it be written without the explicit disconnect and the named function?

Ideally, I'd like something like this:

emitter.someSig.connect(
    function() {
        // do some work here
    },
    Qt.SingleShotConnection
);

Near-duplicate: Automatically disconnect after first signal emission - but that question is about Python and mine is about QML and C++.

Joacima answered 10/7, 2017 at 18:15 Comment(2)
What a problem to call to disconnect()?Excurvate
@folibis: 1. Then the handleSig name is triplicated, 2. It's harder to read, and 3. Can't define the handler inside the connect call.Joacima
P
8

You can create a small helper function, which does the disconnecting for you, like such:

function connectOnce(sig, slot) {
    var f = function() {
        slot.apply(this, arguments)
        sig.disconnect(f)
    }
    sig.connect(f)
}

As demonstration of the usage:

import QtQuick 2.7
import QtQuick.Controls 2.0

ApplicationWindow {
    id: myWindow
    visible: true
    width: 600
    height: 600
    color: 'white'

    signal action(string name)

    function slot(name) {
        console.log(name)
    }

    Button {
        text: 'connect'
        onClicked: {
            connectOnce(action, slot)
        }
    }

    Button {
        y: 80
        text: 'action'
        onClicked: {
            action('test')
        }
    }

    function connectOnce(sig, slot) {
        var f = function() {
            slot.apply(this, arguments)
            sig.disconnect(f)
        }
        sig.connect(f)
    }
}

The upper two Buttons will connect slot and slot2 in single-shot mode to the signal action. The Button action will fire the signal action which will execute the slots as many times as they are connected. Then they will be immediately disconnected.

You might put the function connectOnce into a library to have it to your avail where ever you need it.


This solution is easily extended to a more general form, that will connect a function to be executed n times by introducing a counter in the closure:

function connectN(sig, slot, n) {
    if (n <= 0) return
    var f = function() {
        slot.apply(this, arguments)
        n--
        if (n <= 0) sig.disconnect(f)
    }
    sig.connect(f)
}
Provocation answered 11/7, 2017 at 7:35 Comment(1)
Why didn't I think of this! :)Joacima
J
6

I found an answer for C++ here, though it's a bit inelegant:

https://forum.qt.io/topic/67272/how-to-create-a-single-shot-one-time-connection-to-a-lambda/2

Code from that link:

QMetaObject::Connection * const connection = new QMetaObject::Connection;
*connection = connect(_textFadeOutAnimation, &QPropertyAnimation::finished, [this, text, connection](){
    QObject::disconnect(*connection);
    delete connection;
});

I'm still holding out for better C++ answers, and for a QML answer.

Joacima answered 10/7, 2017 at 18:26 Comment(1)
you could use a shared_ptr for the connection, then you don't have to delete, because the lifetime is managed in the capture clauseVivianne
N
0

It would probably most convenient to make a template implementing the same syntax as a regular QObject::connect call.

I've made these two templates below, using Stefan Monov's own answer. The first is method pointer based, the second one is lambda based.

#ifndef  CONNECT_ONCE_H
# define CONNECT_ONCE_H

# include <QObject>
# include <memory>

template<typename EMITTER, typename SIGNAL, typename RECEIVER, typename... ARGS>
void connectOnce(EMITTER* emitter, SIGNAL signal, RECEIVER* receiver, void (RECEIVER::*slot)(ARGS...), Qt::ConnectionType connectionType = Qt::AutoConnection)
{
  auto connection = std::make_shared<QMetaObject::Connection>();
  auto onTriggered = [connection, receiver, slot](ARGS... arguments){
    (receiver->*slot)(arguments...);
    QObject::disconnect(*connection);
  };

  *connection = QObject::connect(emitter, signal, receiver, onTriggered, connectionType);
}

template<typename EMITTER, typename SIGNAL, typename RECEIVER, typename SLOT, typename... ARGS>
void connectOnce(EMITTER* emitter, SIGNAL signal, RECEIVER* receiver, SLOT slot, Qt::ConnectionType connectionType = Qt::AutoConnection)
{
  std::function<void (ARGS...)> callback = slot;
  auto connection = std::make_shared<QMetaObject::Connection>();
  auto onTriggered = [connection, callback](ARGS... arguments) {
    callback(arguments...);
    QObject::disconnect(*connection);
  };

  *connection = QObject::connect(emitter, signal, receiver, onTriggered, connectionType);
}

#endif

About the lambda based template, I couldn't get it to compile when directly declaring std::function as a parameter, as the compiler couldn't recognize a lambda parameter as a valid candidate... which is why I'm declaring SLOT as a template parameter, and then casting it to std::function. It's not as concise as I would've liked, but it works.

Natasha answered 15/12, 2021 at 14:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.