Qt5:一次性连接到 lambda
Qt5: one-shot connection to lambda
如何使用 Qt5.12 创建一次性连接(即第一次激活时自动断开连接)?我正在寻找一个优雅的解决方案,不过分冗长,它清楚地传达了意思。
我目前正在使用
QObject::connect(instance,Class::signal,this,[this](){
QObject::disconnect(instance,Class::signal,this,0);
/* ... */
});
只有在没有连接其他信号时才有效。
这个posthttps://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 上有几个关于此主题的问题,但没有一个解决方案似乎对我有用。例如, 使用虚拟对象:
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)
模板化解决方案( 的第二部分)不能用 c++17 编译。
有更好的建议吗?
编辑: 我将此作为请求提交 Qt 错误跟踪器:https://bugreports.qt.io/browse/QTBUG-74547 .
我从 OP 问题中采用了(对我来说)最有希望的方法
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
在 Windows 10:
的 VS2017 中测试
在 cygwin64 (X11) 中使用 g++ 测试:
$ 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"
注:
我必须承认这个解决方案有一点缺陷。如果一次性连接不是 "fired" 而是断开连接(例如,因为发送者对象被删除),那么 QMetaObject::Connection
不会被删除并成为内存泄漏。我想了一会儿如何在没有好主意的情况下解决这个问题。最后,我决定按原样发送。所以,请对此持保留态度——它还没有 "production-ready"。至少,它表明了这个想法。
最后,我解决了内存泄漏的问题。
连接的症结在于它必须传递给内部 "trampoline" lambda。
但是,按值传递连接将确保正确的存储管理,但此时它尚未初始化。
通过引用传递它可以解决这个问题,但这会捕获对局部变量的引用(致命)。
因此,new QMetaObject::Connection
的解决方案似乎是唯一有效的解决方案,因为指针可以按值传递,但实例可以在之后更新。由于使用 new
进行分配,应用程序可以控制 QMetaObject::Connection
个实例的生命周期。
但是信号没有发出怎么办。我的解决方案:否则,发件人对象可能负责删除它。这可以在 Qt 中通过 "attaching" a 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 "native") 和带有 X11 的 cygwin64 上再次测试。在后者的会话下方:
$ 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.
在这种情况下,我先按了 One Shot,然后按了第二个 Disconnect。再次测试相反的顺序:
$ make && ./testQSignalOneShot2
make: Nothing to be done for 'first'.
Qt Version: 5.9.4
qBtn disconnected.
ConnectionWrapper::~ConnectionWrapper()
在这种情况下,ConnectionWrapper::~ConnectionWrapper()
的输出在我退出应用程序之前没有出现。 (有道理——发件人 qBtn
在 main()
的范围被保留时被删除。)
前段时间我为单次连接写了一个包装器。似乎工作正常,也许对某些人有用。
template <typename SenderFunc, typename ReceiverFunc>
void singleShotConnect(const typename QtPrivate::FunctionPointer<SenderFunc>::Object* sender, SenderFunc signal, const QObject* context, ReceiverFunc receiverFunc){
auto connection = std::make_shared<QMetaObject::Connection>();
connection = QObject::connect(sender, signal, context, [connection, receiverFunc](auto ...args){
QObject::disconnect(*connection);
std::invoke(receiverFunc, args...);
});
};
用法示例:
auto* senderObj = new SenderObj();
auto* receiverObj = new ReceiverObj();
singleShotConnect(senderObj, &SenderObj:someSignal, receiverObj, &ReceiverObj::someSlot);
有一个更简单的方法:删除接收器以破坏连接。
auto receiver = new QObject(this);
connect(_textFadeOutAnimation, &QPropertyAnimation::finished, receiver, [this, text, receiver](){
receiver.deleteLater();
});
(我认为所有其他答案)的小问题是它无法处理发送方和接收方处于不同线程的情况。如果接收器正忙(例如处理另一个信号),则信号可能会在断开连接之前多次触发。断开信号不会删除任何已排队的连接。
解决方案是用两个连接做一个稍微变体。第一个连接到预期的接收器(并在接收器的线程上运行)。第二个在发件人的线程上运行并断开连接。
template <typename SenderT, typename Func1T, typename ...Args>
QMetaObject::Connection connectOneShot(SenderT* sender, const Func1T& signal, Args&&...args) {
auto connections = std::make_shared<std::pair<QMetaObject::Connection, QMetaObject::Connection>>();
// The order of connections is important here - the target slot must be connected first (so that it's called before the disconnection has happened)
connections->first = QObject::connect(sender, signal, std::forward<Args>(args)...);
// This gets run as a direct connection on the sender's thread, thus ensuring that's it's called immediately (before the signal has the change to repeat)
connections->second = QObject::connect(sender, signal, sender, [connections](){
QObject::disconnect(connections->first);
QObject::disconnect(connections->second);
});
return connections->first;
}
与原来相比的另一个细微变化是我使用 std::shared_ptr
来确保 heap-allocated 连接被清除 - 就像在 .
中一样
如何使用 Qt5.12 创建一次性连接(即第一次激活时自动断开连接)?我正在寻找一个优雅的解决方案,不过分冗长,它清楚地传达了意思。
我目前正在使用
QObject::connect(instance,Class::signal,this,[this](){
QObject::disconnect(instance,Class::signal,this,0);
/* ... */
});
只有在没有连接其他信号时才有效。
这个posthttps://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 上有几个关于此主题的问题,但没有一个解决方案似乎对我有用。例如, 使用虚拟对象:
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)
模板化解决方案( 的第二部分)不能用 c++17 编译。
有更好的建议吗?
编辑: 我将此作为请求提交 Qt 错误跟踪器:https://bugreports.qt.io/browse/QTBUG-74547 .
我从 OP 问题中采用了(对我来说)最有希望的方法
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
在 Windows 10:
的 VS2017 中测试在 cygwin64 (X11) 中使用 g++ 测试:
$ 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"
注:
我必须承认这个解决方案有一点缺陷。如果一次性连接不是 "fired" 而是断开连接(例如,因为发送者对象被删除),那么 QMetaObject::Connection
不会被删除并成为内存泄漏。我想了一会儿如何在没有好主意的情况下解决这个问题。最后,我决定按原样发送。所以,请对此持保留态度——它还没有 "production-ready"。至少,它表明了这个想法。
最后,我解决了内存泄漏的问题。
连接的症结在于它必须传递给内部 "trampoline" lambda。
但是,按值传递连接将确保正确的存储管理,但此时它尚未初始化。
通过引用传递它可以解决这个问题,但这会捕获对局部变量的引用(致命)。
因此,new QMetaObject::Connection
的解决方案似乎是唯一有效的解决方案,因为指针可以按值传递,但实例可以在之后更新。由于使用 new
进行分配,应用程序可以控制 QMetaObject::Connection
个实例的生命周期。
但是信号没有发出怎么办。我的解决方案:否则,发件人对象可能负责删除它。这可以在 Qt 中通过 "attaching" a 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 "native") 和带有 X11 的 cygwin64 上再次测试。在后者的会话下方:
$ 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.
在这种情况下,我先按了 One Shot,然后按了第二个 Disconnect。再次测试相反的顺序:
$ make && ./testQSignalOneShot2
make: Nothing to be done for 'first'.
Qt Version: 5.9.4
qBtn disconnected.
ConnectionWrapper::~ConnectionWrapper()
在这种情况下,ConnectionWrapper::~ConnectionWrapper()
的输出在我退出应用程序之前没有出现。 (有道理——发件人 qBtn
在 main()
的范围被保留时被删除。)
前段时间我为单次连接写了一个包装器。似乎工作正常,也许对某些人有用。
template <typename SenderFunc, typename ReceiverFunc>
void singleShotConnect(const typename QtPrivate::FunctionPointer<SenderFunc>::Object* sender, SenderFunc signal, const QObject* context, ReceiverFunc receiverFunc){
auto connection = std::make_shared<QMetaObject::Connection>();
connection = QObject::connect(sender, signal, context, [connection, receiverFunc](auto ...args){
QObject::disconnect(*connection);
std::invoke(receiverFunc, args...);
});
};
用法示例:
auto* senderObj = new SenderObj();
auto* receiverObj = new ReceiverObj();
singleShotConnect(senderObj, &SenderObj:someSignal, receiverObj, &ReceiverObj::someSlot);
有一个更简单的方法:删除接收器以破坏连接。
auto receiver = new QObject(this);
connect(_textFadeOutAnimation, &QPropertyAnimation::finished, receiver, [this, text, receiver](){
receiver.deleteLater();
});
解决方案是用两个连接做一个稍微变体。第一个连接到预期的接收器(并在接收器的线程上运行)。第二个在发件人的线程上运行并断开连接。
template <typename SenderT, typename Func1T, typename ...Args>
QMetaObject::Connection connectOneShot(SenderT* sender, const Func1T& signal, Args&&...args) {
auto connections = std::make_shared<std::pair<QMetaObject::Connection, QMetaObject::Connection>>();
// The order of connections is important here - the target slot must be connected first (so that it's called before the disconnection has happened)
connections->first = QObject::connect(sender, signal, std::forward<Args>(args)...);
// This gets run as a direct connection on the sender's thread, thus ensuring that's it's called immediately (before the signal has the change to repeat)
connections->second = QObject::connect(sender, signal, sender, [connections](){
QObject::disconnect(connections->first);
QObject::disconnect(connections->second);
});
return connections->first;
}
与原来相比的另一个细微变化是我使用 std::shared_ptr
来确保 heap-allocated 连接被清除 - 就像在