Qt5: одноразовое подключение к лямбде - PullRequest
2 голосов
/ 05 марта 2019

Как я могу создать одноразовое соединение (то есть соединение, которое автоматически отключается при первом включении) с Qt5.12? Я ищу элегантное решение, без излишнего многословия, в котором четко указано, что имеется в виду.

Я сейчас использую

QObject::connect(instance,Class::signal,this,[this](){
    QObject::disconnect(instance,Class::signal,this,0);
    /* ... */
});

, который работает только при отсутствии других подключенных сигналов.

Этот пост https://forum.qt.io/post/328402 предлагает

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

, который работает даже в присутствии других соединений, но, опять же, не очень элегантно.

В SO есть несколько вопросов на эту тему, но ни одно из решений, похоже, не работает для меня. Например, https://stackoverflow.com/a/42989833/761090 использует фиктивный объект:

QObject *dummy=new QObject(this);
QObject::connect(instance,Class::signal,[dummy](){
    dummy->deleteLater();
});

выдает предупреждение во время выполнения:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is ClassInstance(0x561c14ce3a60), parent's thread is QThread(0x561c14e1b050), current thread is QThread(0x561c14c2b530)

Шаблонное решение (вторая часть https://stackoverflow.com/a/26554206/761090) не компилируется с c ++ 17.

Есть лучшие предложения?

РЕДАКТИРОВАТЬ: Я подал это как трекер ошибок Qt: https://bugreports.qt.io/browse/QTBUG-74547.

1 Ответ

1 голос
/ 16 марта 2019

Я выбрал (для меня) наиболее многообещающий подход из вопроса ОП

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

и подумал о том, как обернуть его в функцию. На самом деле, это должна быть функция шаблона, чтобы сделать ее пригодной для любого сигнала Qt:

template <typename Sender, typename Emitter, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(
  Sender *pSender, void (Emitter::*pSignal)(Args ...args), Slot slot)
{
  QMetaObject::Connection *pConnection = new QMetaObject::Connection;
  *pConnection
    = QObject::connect(pSender, pSignal,
      [pConnection, slot](Args... args)
      {
        QObject::disconnect(*pConnection);
        delete pConnection;
        slot(args...);
      });
  return *pConnection;
}

Я пробовал это в MCVE :

#include <functional>

#include <QtWidgets>

template <typename Sender, typename Emitter, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(Sender *pSender, void (Emitter::*pSignal)(Args ...args), Slot slot)
{
  QMetaObject::Connection *pConnection = new QMetaObject::Connection;
  *pConnection
    = QObject::connect(pSender, pSignal,
      [pConnection, slot](Args... args)
      {
        QObject::disconnect(*pConnection);
        delete pConnection;
        slot(args...);
      });
  return *pConnection;
}

template <typename Sender, typename Emitter,
  typename Receiver, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(
  Sender *pSender, void (Emitter::*pSignal)(Args ...args),
  Receiver *pRecv, Slot slot)
{
  QMetaObject::Connection *pConnection = new QMetaObject::Connection;
  *pConnection
    = QObject::connect(pSender, pSignal,
      [pConnection, pRecv, slot](Args... args)
      {
        QObject::disconnect(*pConnection);
        delete pConnection;
        (pRecv->*slot)(args...);
      });
  return *pConnection;
}

void onBtnClicked(bool)
{
  static int i = 0;
  qDebug() << "onBtnClicked() called:" << ++i;
}

struct PushButton: public QPushButton {
  int i = 0;
  using QPushButton::QPushButton;
  virtual ~PushButton() = default;
  void onClicked(bool)
  {
    ++i;
    qDebug() << this << "PushButton::onClicked() called:" << i;
    setText(QString("Clicked %1.").arg(i));
  }
};

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup user interface
  QWidget qWinMain;
  QGridLayout qGrid;
  qGrid.addWidget(new QLabel("Multi Shot"), 0, 1);
  qGrid.addWidget(new QLabel("One Shot"), 0, 2);
  auto addRow
    = [](
      QGridLayout &qGrid, const QString &qText,
      QWidget &qWidgetMShot, QWidget &qWidgetOneShot)
    {
      const int i = qGrid.rowCount();
      qGrid.addWidget(new QLabel(qText), i, 0);
      qGrid.addWidget(&qWidgetMShot, i, 1);
      qGrid.addWidget(&qWidgetOneShot, i, 2);
    };
  QPushButton qBtnMShotFunc("Click me!");
  QPushButton qBtnOneShotFunc("Click me!");
  addRow(qGrid, "Function:", qBtnMShotFunc, qBtnOneShotFunc);
  PushButton qBtnMShotMemFunc("Click me!");
  PushButton qBtnOneShotMemFunc("Click me!");
  addRow(qGrid, "Member Function:", qBtnMShotMemFunc, qBtnOneShotMemFunc);
  QPushButton qBtnMShotLambda("Click me!");
  QPushButton qBtnOneShotLambda("Click me!");
  addRow(qGrid, "Lambda:", qBtnMShotLambda, qBtnOneShotLambda);
  QLineEdit qEditMShot("Edit me!");
  QLineEdit qEditOneShot("Edit me!");
  addRow(qGrid, "Lambda:", qEditMShot, qEditOneShot);
  qWinMain.setLayout(&qGrid);
  qWinMain.show();
  // install signal handlers
  QObject::connect(&qBtnMShotFunc, &QPushButton::clicked,
    &onBtnClicked);
  connectOneShot(&qBtnOneShotFunc, &QPushButton::clicked,
    &onBtnClicked);
  QObject::connect(&qBtnMShotMemFunc, &QPushButton::clicked,
    &qBtnMShotMemFunc, &PushButton::onClicked);
  connectOneShot(&qBtnOneShotMemFunc, &QPushButton::clicked,
    &qBtnOneShotMemFunc, &PushButton::onClicked);
  QObject::connect(&qBtnMShotLambda, &QPushButton::clicked,
    [&](bool) {
      qDebug() << "[&](bool) qBtnMShotLambda called.";
      static int i = 0;
      qBtnMShotLambda.setText(QString("Clicked %1.").arg(++i));
    });
  connectOneShot(&qBtnOneShotLambda, &QPushButton::clicked,
    [&](bool) {
      qDebug() << "[&](bool) for qBtnOneShotLambda called.";
      static int i = 0;
      qBtnOneShotLambda.setText(QString("Clicked %1.").arg(++i));
    });
  QObject::connect(&qEditMShot, &QLineEdit::editingFinished,
    [&]() {
      qDebug() << "[&]() for qEditMShot called. Input:" << qEditMShot.text();
      qEditMShot.setText("Well done.");
    });
  connectOneShot(&qEditOneShot, &QLineEdit::editingFinished,
    [&]() {
      qDebug() << "[&]() for qEditOneShot called. Input:" << qEditOneShot.text();
      qEditOneShot.setText("No more input accepted.");
      qEditOneShot.setEnabled(false);
    });
  // run
  return app.exec();
}

Соответствующий проект Qt:

SOURCES = testQSignalOneShot.cc

QT += widgets

Протестировано в VS2017 на Windows 10:

Snapshot of testQSignalOneShot in Windows 10

Протестировано с g ++ в cygwin64 (X11):

$ qmake-qt5 testQSignalOneShot.pro

$ make && ./testQSignalOneShot
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o testQSignalOneShot.o testQSignalOneShot.cc
g++  -o testQSignalOneShot.exe testQSignalOneShot.o   -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread 
Qt Version: 5.9.4
onBtnClicked() called: 1
onBtnClicked() called: 2
onBtnClicked() called: 3
QPushButton(0xffffcb60) PushButton::onClicked() called: 1
QPushButton(0xffffcb60) PushButton::onClicked() called: 2
QPushButton(0xffffcba0) PushButton::onClicked() called: 1
[&](bool) qBtnMShotLambda called.
[&](bool) qBtnMShotLambda called.
[&](bool) for qBtnOneShotLambda called.
[&]() for qEditMShot called. Input: "abc123"
[&]() for qEditMShot called. Input: "def456"
[&]() for qEditMShot called. Input: "Well done."
[&]() for qEditOneShot called. Input: "abc456"

Snapshot of testQSignalOneShot in cygwin64 & X11

Примечание:

Я должен признать, что это решение имеет небольшой недостаток. Если одноразовое соединение не «срабатывает», а отключается (например, из-за того, что объект отправителя удален), то QMetaObject::Connection не удаляется и становится утечкой памяти. Некоторое время я размышлял, как решить эту проблему, не имея хорошей идеи. Наконец, я решил отправить это как есть. Так что, пожалуйста, возьмите это с крошкой соли & ndash; это еще не "готово к производству". По крайней мере, это показывает идею.


Наконец-то я решил проблему с утечкой памяти.

Суть связи в том, что она должна передаваться во внутреннюю "батутную" лямбду.

Однако передача соединения по значению обеспечит правильное управление хранилищем, но на данный момент оно еще не инициализировано.

Передача по ссылке может решить эту проблему, но при этом будет зафиксирована ссылка на локальную переменную (фатальную).

Следовательно, решение с new QMetaObject::Connection кажется единственным работающим, так как указатель может быть передан по значению, но экземпляр может быть обновлен впоследствии. Из-за выделения с new приложение контролирует время жизни экземпляра QMetaObject::Connection.

Но что, если сигнал не излучается. Мое решение: в противном случае объект отправителя может быть ответственным за его удаление. Это может быть достигнуто в Qt путем «присоединения» QObject к другому (установка последнего как родителя первого).

На основе этого улучшенное решение, где я хранил QMetaObject::Connection в оболочке, полученной из QObject:

#include <functional>

#include <QtWidgets>

struct ConnectionWrapper: QObject {
  ConnectionWrapper(QObject *pQParent): QObject(pQParent) { }
  ConnectionWrapper(const ConnectionWrapper&) = delete;
  ConnectionWrapper& operator=(const ConnectionWrapper&) = delete;
  virtual ~ConnectionWrapper()
  {
    qDebug() << "ConnectionWrapper::~ConnectionWrapper()";
  }
  QMetaObject::Connection connection;
};

template <typename Sender, typename Emitter, typename Slot, typename... Args>
QMetaObject::Connection connectOneShot(Sender *pSender, void (Emitter::*pSignal)(Args ...args), Slot slot)
{
  ConnectionWrapper *pConn = new ConnectionWrapper(pSender);
  pConn->connection
    = QObject::connect(pSender, pSignal,
      [pConn, slot](Args... args)
      {
        QObject::disconnect(pConn->connection);
        delete pConn;
        slot(args...);
      });
  return pConn->connection;
}

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // setup user interface
  QWidget qWinMain;
  QGridLayout qGrid;
  auto addRow
    = [](QGridLayout &qGrid, const QString &qText, QWidget &qWidgetMShot, QWidget &qWidgetOneShot)
    {
      const int i = qGrid.rowCount();
      qGrid.addWidget(new QLabel(qText), i, 0);
      qGrid.addWidget(&qWidgetMShot, i, 1);
      qGrid.addWidget(&qWidgetOneShot, i, 2);
    };
  QPushButton qBtn("One Shot");
  QPushButton qBtnDisconnect("Disconnect");
  addRow(qGrid, "Disconnect Test:", qBtnDisconnect, qBtn);
  qWinMain.setLayout(&qGrid);
  qWinMain.show();
  // install signal handlers
  QMetaObject::Connection connectionBtn
    = connectOneShot(&qBtn, &QPushButton::clicked,
      [&](bool) {
        qDebug() << "[&](bool) for qBtn called.";
        static int i = 0;
        qBtn.setText(QString("Clicked %1.").arg(++i));
      });
  QObject::connect(&qBtnDisconnect, &QPushButton::clicked,
    [&](bool) {
      QObject::disconnect(connectionBtn);
      qDebug() << "qBtn disconnected.";
    });
  // run
  return app.exec();
}

Соответствующий проект Qt:

SOURCES = testQSignalOneShot2.cc

QT += widgets

Я снова протестировал в VS2017 (Windows 10 «родной») и на cygwin64 с X11. Ниже сеанс в последнем:

$ qmake-qt5 testQSignalOneShot2.pro

$ make && ./testQSignalOneShot2
/usr/bin/qmake-qt5 -o Makefile testQSignalOneShot2.pro
g++ -c -fno-keep-inline-dllexport -D_GNU_SOURCE -pipe -O2 -Wall -W -D_REENTRANT -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -isystem /usr/include/qt5 -isystem /usr/include/qt5/QtWidgets -isystem /usr/include/qt5/QtGui -isystem /usr/include/qt5/QtCore -I. -I/usr/lib/qt5/mkspecs/cygwin-g++ -o testQSignalOneShot2.o testQSignalOneShot2.cc
g++  -o testQSignalOneShot2.exe testQSignalOneShot2.o   -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread 
Qt Version: 5.9.4
ConnectionWrapper::~ConnectionWrapper()
[&](bool) for qBtn called.
qBtn disconnected.

В этом случае я сначала нажал Один выстрел , а второй Отключил . Проверено снова на противоположный порядок:

$ make && ./testQSignalOneShot2
make: Nothing to be done for 'first'.
Qt Version: 5.9.4
qBtn disconnected.
ConnectionWrapper::~ConnectionWrapper()

В этом случае вывод ConnectionWrapper::~ConnectionWrapper() не появился до того, как я вышел из приложения. (Имеет смысл - отправитель qBtn был удален при оставлении области действия main().)

Snapshot of testQSignalOneShot2 in cygwin64 & X11

...