信号和槽(signals and slots)用于对象之间的通信。信号和槽机制是Qt的核心特性,可能也是与其他框架提供的特性最与众不同的部分。Qt的元对象系统(meta-object system)使信号和插槽的实现成为可能。
在GUI编程中,当我们更改一个小部件(widget)时,我们通常希望通知(notified)另一个小部件(widget)。更一般地说,我们希望任何类型的对象能够相互通信。例如,如果用户单击了Close按钮,我们可能希望调用窗口的Close()
函数。
其他库使用回调(callbacks)来实现这种通信。回调函数是一个指向函数的指针,所以如果你想要一个处理函数通知你一些事件,你可以将一个指向另一个函数(回调函数)的指针传递给处理函数。处理函数然后在适当的时候调用回调函数。虽然使用这种方法的成功框架确实存在,但回调可能不太直观,而且在确保回调参数的类型正确性(type-correctness)方面可能会遇到问题。
在Qt中,我们有一个回调技术的替代方案:信号和槽。
当特定事件发生时发出(emit)信号。Qt的小部件(widgets)有许多预定义的信号,但是我们总是可以子类化(继承)小部件,给它们添加我们自己的信号。槽(slot)是响应特定信号而调用的函数。Qt的小部件有许多预定义的槽,但是通常的做法是子类化小部件并添加自己的槽,这样您就可以处理感兴趣的信号。
信号的签名(signature)必须与接收槽的签名匹配。 (事实上,槽的签名可能比它接收到的信号短,因为它可以忽略额外的参数。)由于签名是兼容(compatible)的,所以在使用基于函数指针(function pointer-based)的语法时,编译器可以帮助我们检测(detect)类型不匹配。基于字符串的信号(SIGNAL)和槽(SLOT)语法将在运行时(runtime)检测类型不匹配。(注:优先使用基于函数指针的语法)信号和插槽是松散耦合的: 发出信号的类既不知道也不关心哪个槽接收信号。 Qt的信号和槽机制确保,如果你将一个信号连接到槽,槽将在 恰当的时间 用信号的参数调用。信号和槽可以接受任何类型的任意数量的参数。它们是完全类型安全(type-safe)的。
所有从QObject或它的一个子类(例如,QWidget)继承的类都可以包含信号和槽。当对象以一种其他对象可能感兴趣的方式改变其状态时,就会发出信号。这就是对象通信所做的一切。它不知道或不关心是否有任何东西正在接收它发出的信号。这是真正的信息封装(information encapsulation),并确保对象可以作为软件组件使用。
槽可以用来接收信号,但它们也是普通的成员函数(member functions)。就像一个对象不知道是否有任何东西接收到它的信号一样,槽也不知道是否有任何信号连接到它。这确保了Qt可以创建真正独立的组件,可以:
class MyWidget : public QWidget
{
Q_OBJECT
public:
MyWidget();
signals:
void buttonClicked();
private:
QPushButton *myButton;
};
MyWidget::MyWidget()
{
myButton = new QPushButton(this);
connect(myButton, SIGNAL(clicked()),this, SIGNAL(buttonClicked()));
}
在这个例子中,MyWidget
的构造函数中转发了一个私有成员变量的信号, 并使它在MyWidget
中有了一个新的名称。
信号和插槽一起构成了一个强大的组件编程机制。
当对象的内部状态(internal state)以对象的客户端(client)或所有者(owner)感兴趣的某种方式发生变化时,对象就会发出信号。信号是public函数,可以从任何地方发出,但我们建议只从定义信号及其子类的类发出信号。
当信号发出时,连接到它的插槽通常立即执行,就像普通的函数调用一样。当这种情况发生时,信号和槽机制完全独立于任何GUI事件循环(event loop)。一旦所有的槽都返回,emit
语句之后的代码就会执行。当使用队列(queued)连接时,情况略有不同; 在这种情况下,emit关键字后面的代码将立即继续,插槽将稍后执行。
如果多个槽连接到一个信号,当信号发出时,槽的调用顺序将按照它们连接的顺序依次执行。信号是由moc自动生成的,不能在.cpp文件中实现。它们永远不能有返回类型(即使用void
)。如果成功地将信号连接到槽,则该函数返回一个QMetaObject::Connection
,它表示连接的句柄。如果连接句柄不能创建连接,那么它将是无效的,例如,如果QObject
不能验证信号或方法的存在,或者它们的签名(signature)不兼容。您可以通过将其转换为bool
类型来检查句柄是否有效。
关于参数的注意事项: 我们的经验表明,如果信号和槽不使用特殊类型,它们将更易于重用。如果QScrollBar::valueChanged()
使用一种特殊类型,比如假设的QScrollBar::Range
,那么它只能连接到专门为QScrollBar
设计的插槽。将不同的输入部件连接在一起是不可能的。
默认情况下,你建立的每一个连接都会发出一个信号; 重复连接会发出两个信号。你可以通过一个disconnect()
调用断开所有这些连接。
当发出连接到插槽的信号时,槽将被调用。slot是普通的c++函数,可以以正常的方式调用; 它们唯一的特点是可以连接信号。
因为槽是普通的成员函数,所以当直接调用时,它们遵循普通的c++规则。然而,作为槽,它们可以被任何组件调用,而不管其访问级别,通过信号-槽连接。这意味着从任意类的实例发出的信号可能导致在不相关类的实例中调用私有槽。
还可以将插槽定义为虚拟的,我们发现这在实际使用过程中非常有用。
与回调相比,信号和槽稍微慢一些,因为它们提供了更大的灵活性,尽管实际应用程序的差异并不大。一般来说,当槽函数为非虚函数时,发射一个连接到一些槽的信号,大约比直接调用接收函数慢十倍。这是定位连接对象、安全遍历所有连接(即检查在发送过程中后续接收方是否被销毁)以及以通用方式编组任何参数所需的额外开销。虽然十倍非虚函数调用可能听起来很多,但它的开销比任何new或delete操作都要小得多。一旦在后台执行需要new或delete的string、vector或list操作,信号和槽的开销只占整个函数调用开销的很小一部分。当你在槽中执行系统调用(system call)时,情况也是一样的或间接调用十多个函数。
信号和槽机制带来的的简单性和灵活性是非常值得的,用户甚至不会注意到这些开销。
请注意,当与基于qt的应用程序一起编译时,定义了称为信号或槽的变量的其他库可能会导致编译器警告和错误。要解决这个问题,#undef出错的预处理器符号。
在 Qt 5 中,QObject::connect()
有五个重载:
QMetaObject::Connection connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, PointerToMemberFunction,
Qt::ConnectionType)
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
Functor);
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, Functor ,
Qt::ConnectionType);
// !!! Qt 5
connect(sender, signal,receiver, slot);
这是我们最常用的形式。connect()
一般会使用前面四个参数,第一个是发出信号的对象(QObject及其子类),第二个是发送对象发出的信号,第三个是接收信号的对象(QObject及其子类),第四个是接收对象在接收到信号之后所需要调用的函数。也就是说,当 sender 发出了 signal 信号之后,会自动调用 receiver 的 slot 函数。
enum ConnectionType
{
AutoConnection,
DirectConnection,
QueuedConnection,
BlockingQueuedConnection,
UniqueConnection = 0x80
};
这个enum
描述了signal和slot之间的连接类型。具体来说,它确定特定的信号是立即发送到槽,还是排队等待稍后的时间发送。
Qt::AutoConnection
(默认的)如果receiver存在与信号发射的线程,Qt::DirectConnection
将被使用。其他情况Qt::QueuedConnection
将被使用。连接类型是在信号发出时确定的。Qt::DirectConnection
信号发射后槽将被立即调用。槽将执行在信号发射的线程。Qt::QueuedConnection
当控制权返回到接收方线程的事件循环(event loop)时将调用槽。槽在接收方的线程中执行。Qt::BlockingQueuedConnection
和Qt::QueuedConnection
类似,但是信号线程将阻塞到槽函数返回。因此接受方不能存活在于信号发射的线程。如果存在,则将会导致死锁。Qt::UniqueConnection
这是一个标记,可以使用位操作(OR)与上述任何一种连接类型结合使用。当Qt::UniqueConnection
被设置,如果连接已经存在(同一对对象的相同信号已经连接到相同的槽),再次使用QObject::connect()
将会失败。连接将返回一个无效的QMetaObject::Connection
注意:
Qt::UniqueConnections
不适用于的 lambdas
,非成员函数和functor
;它们只适用于连接到成员函数。QObject::connect: Cannot queue arguments of type 'MyType'
(Make sure 'MyType' is registered using qRegisterMetaType().)
在建立连接前,调用 qRegisterMetaType()
注册这一数据类型,在多线程中使用singals 和 slots 的细节信息参考Threads-and-Qobjects
QMetaObject::Connection connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
sender 类型是const QObject *
,signal 的类型是const char *
,receiver 类型是const QObject *
,slot 类型是const char *
。这个函数将 signal 和 slot 作为字符串处理。例子:
QLabel *label = new QLabel;
QScrollBar *scrollBar = new QScrollBar;
QObject::connect(scrollBar, SIGNAL(valueChanged(int)),label, SLOT(setNum(int)));
这个例子确保 label 总是在显示当前滚动条的值。注意!信号和槽的参数不能包含任何变量的名称。例如,下面的例子将不能工作,并返回错误。
// WRONG
QObject::connect(scrollBar, SIGNAL(valueChanged(int value)),label, SLOT(setNum(int value)));
QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
sender 和 receiver 同样是const QObject *
,但是 signal 和 slot 都是const QMetaMethod &
。我们可以将每个函数看做是QMetaMethod
的子类。因此,这种写法可以使用QMetaMethod
进行类型比对。
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, PointerToMemberFunction,
Qt::ConnectionType)
sender 和 receiver 也都存在,都是const QObject *
,但是 signal 和 slot 类型则是PointerToMemberFunction
。看这个名字就应该知道,这是指向成员函数的指针, 例子:
QLabel *label = new QLabel;
QLineEdit *lineEdit = new QLineEdit;
QObject::connect(lineEdit, &QLineEdit::textChanged, label, &QLabel::setText);
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
Functor);
前面两个参数没有什么不同,最后一个参数是Functor类型。这个类型可以接受 static 函数、全局函数以及 Lambda 表达式。
全局函数:
void someFunction();
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, someFunction);
Lambda 表达式:
QByteArray page = ...;
QTcpSocket *socket = new QTcpSocket;
socket->connectToHost("qt-project.org", 80);
QObject::connect(socket, &QTcpSocket::connected, [=] () {
socket->write("GET " + page + "\r\n");
});
如果sender被销毁,连接将自动断开。但是,应该注意在发出信号时,函数内使用的任何对象仍然是存活的。
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction ,
const QObject *, Functor ,
Qt::ConnectionType )
Creates a connection of a given type from signal in sender object to functor to be placed in a specific event loop of context, and returns a handle to the connection.
创建一个指定类型的signal连接,将functor放置在receiver的事件循环中。
注意: Qt::UniqueConnections
不能在lambdas,非成员函数和functor 上使用;它们只适用于连接到成员函数。信号必须是在头文件中声明为信号的函数。槽函数可以是任何可以连接到信号的函数或仿函数(functor)。信号和槽中相应参数的类型之间必须存在隐式转换。
// function:
void someFunction();
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, this, someFunction, Qt::QueuedConnection);
// Lambda:
QByteArray page = ...;
QTcpSocket *socket = new QTcpSocket;
socket->connectToHost("qt-project.org", 80);
QObject::connect(socket, &QTcpSocket::connected, this, [=] () {
socket->write("GET " + page + "\r\n");
}, Qt::AutoConnection);
由此我们可以看出,connect()函数,sender 和 receiver 没有什么区别,都是QObject指针;主要是 signal 和 slot 形式的区别。具体到我们的示例,我们的connect()函数显然是使用的第五个重载,最后一个参数是QApplication的 static 函数quit()。也就是说,当我们的 button 发出了clicked()信号时,会调用QApplication的quit()函数,使程序退出。
如果信号槽不符合,或者根本找不到这个信号或者槽函数的话,比如我们改成:
QObject::connect(&button, &QPushButton::clicked, &QApplication::quit2);
由于 QApplication 没有 quit2 这样的函数的,因此在编译时,会有编译错误:
'quit2' is not a member of QApplication
这样,使用成员函数指针,我们就不会担心在编写信号槽的时候会出现函数错误。
信号和槽的签名可以包含参数,参数可以有默认值。考虑QObject::destroyed()
:
void destroyed(QObject* = 0);
当一个QObject被删除时,它会发出QObject::destroyed()
信号。
We want to catch this signal, wherever we might have a dangling reference to the deleted QObject, so we can clean it up.
我们希望捕捉这个信号,无论在哪里,只要有一个指向已删除QObject的悬空引用,我们就可以清除它。
一个合适的槽可能是:
void objectDestroyed(QObject* obj = 0);
为了将信号连接到插槽,我们使用QObject::connect()
。有几种方法连接信号和插槽。
第一种方法是使用函数指针:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
将QObject::connect()与函数指针一起使用有几个优点:
connect(sender, &QObject::destroyed, [=](){
this; m_objects.remove(sender);
});
另一种将信号连接到槽的方法是使用 字符串风格的QObject::connect()
。关于在SIGNAL()和SLOT()宏中是否包含参数,如果参数有默认值,传递给SIGNAL()宏的签名的参数必须不少于传递给SLOT()宏的签名的参数。
所有这些都可以:
connect(sender, SIGNAL(destroyed(QObject*)),
this, SLOT(objectDestroyed(QObject*)));
connect(sender, SIGNAL(destroyed(QObject*)),
this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed())),
this, SLOT(objectDestroyed()));
但这个行不通:
connect(sender, SIGNAL(destroyed()),
this, SLOT(objectDestroyed(QObject*)));
因为槽需要一个QObject*
作为参数将但是信号没有发送。此连接将报告一个运行时错误。
注意!当使用字符串风格的QObject::connect()
重载时,编译器不会检查信号和槽参数。
字符串风格 | 仿函数风格 | |
---|---|---|
类型检查发生时刻 | 运行时 | 编译时 |
执行隐式类型转换 | 否 | 是 |
连接信号至lambda表达式 | 否 | 是 |
signal的参数可以少于slot(有默认参数) | 是 | 否 |
连接C++函数和QML函数 | 是 | 否 |
下面几节将详细解释这些区别,并演示如何使用每种连接语法特有的特性。
基于字符串的连接类型检查,在运行时比较字符串。这种方法有三个限制:
限制2和3的存在是因为string comparator不能访问c++类型信息,所以它依赖于精确的字符串匹配。
相反,基于仿函数的连接由编译器检查。编译器在编译时捕获错误,启用兼容类型之间的隐式转换,并识别同一类型的不同名称。
例如,只有基于仿函数的语法可以用于将携带int
的信号连接到接受double
的槽。QSlider
保存一个int
值,而QDoubleSpinBox
保存一个double
值。下面的代码片段展示了如何保持它们同步:
auto slider = new QSlider(this);
auto doubleSpinBox = new QDoubleSpinBox(this);
// OK: The compiler can convert an int into a double
connect(slider, &QSlider::valueChanged,
doubleSpinBox, &QDoubleSpinBox::setValue);
// ERROR: The string table doesn't contain conversion information
connect(slider, SIGNAL(valueChanged(int)),
doubleSpinBox, SLOT(setValue(double)));
下面的示例说明了缺少名称解析的原因。QAudioInput::stateChanged()
用一个类型为QAudio::State
的参数声明。因此,基于字符串的连接还必须指定QAudio::State
,即使State
已经可见。这个问题不会发生于基于仿函数的连接,因为实参类型不是连接的一部分。
auto audioInput = new QAudioInput(QAudioFormat(), this);
auto widget = new QWidget(this);
// OK
connect(audioInput, SIGNAL(stateChanged(QAudio::State)),
widget, SLOT(show()));
// ERROR: The strings "State" and "QAudio::State" don't match
using namespace QAudio;
connect(audioInput, SIGNAL(stateChanged(State)),
widget, SLOT(show()));
基于仿函数的连接语法可以将信号连接到c++ 11 lambda表达式,这是一种有效的内联槽。此特性不适用于基于字符串的语法。
在下面的示例中,TextSender
类发出一个带有QString
参数的textCompleted()
信号。
class TextSender : public QWidget {
Q_OBJECT
QLineEdit *lineEdit;
QPushButton *button;
signals:
void textCompleted(const QString& text) const;
public:
TextSender(QWidget *parent = nullptr)
: QWidget(parent)
{
line Edit = new QLineEdit(this);
button = new QPushButton("Send", this);
connect(button, &QPushButton::clicked, [=]{
emit textCompleted(lineEdit->text());
})
// ...
}
};
在本例中,尽管QPushButton::clicked()
和TextSender::textCompleted()
的参数不兼容,lambda函数还是使连接变得简单。相反,基于字符串的实现则需要额外的代码。
注意:基于仿函数的连接语法接受指向所有函数的指针,包括独立函数和常规成员函数。然而,为了便于阅读,信号应该只连接到插槽、lambda表达式和其他信号。
基于字符串的语法可以将c++对象连接到QML对象,但是基于仿函数的语法不能。这是因为QML类型是在运行时解析的,所以c++编译器无法使用它们。
在下面的示例中,单击QML对象将使c++对象打印一条消息,反之亦然。下面是QML类型(在QmlGui.qml中)
Rectangle {
width: 100; height: 100
signal qmlSignal(string sentMsg)
function qmlSlot(receivedMsg) {
console.log("QML received: " + receivedMsg)
}
MouseArea {
anchors.fill: parent
onClicked: qmlSignal("Hello from QML!")
}
}
下面是 C++ 代码:
class CppGui : public QWidget {
Q_OBJECT
QPushButton *button;
signals:
void cppSignal(const QVariant& sentMsg) const;
public slots:
void cppSlot(const QString& receivedMsg) const {
qDebug() << "C++ received:" << receivedMsg;
}
public:
CppGui(QWidget *parent = nullptr) : QWidget(parent) {
button = new QPushButton("Click Me!", this);
connect(button, &QPushButton::clicked, [=] {
emit cppSignal("Hello from C++!");
});
}
};
下面的代码展示了如何连接signal-slot:
auto cppObj = new CppGui(this);
auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this);
auto qmlObj = quickWidget->rootObject();
// Connect QML signal to C++ slot
connect(qmlObj, SIGNAL(qmlSignal(QString)),
cppObj, SLOT(cppSlot(QString)));
// Connect C++ signal to QML slot
connect(cppObj, SIGNAL(cppSignal(QVariant)),
qmlObj, SLOT(qmlSlot(QVariant)));
注意: QML中的所有JavaScript函数都接受var类型的参数,它映射到c++中的QVariant
类型。
当点击QPushButton
时,控制台打印:'QML received: ’ Hello from c++ ! ’ ‘。同样,当单击矩形时,控制台打印:’ c++ received: ’ Hello from QML! ’ '。
通常,只有当槽的参数数量与信号相同(或更少),并且所有参数类型都兼容时,才能建立连接。
基于字符串的连接语法为该规则提供了一个解决方案:如果槽有默认参数,这些参数可以从信号中省略。当发出的信号的参数比槽少时,Qt使用默认参数值运行槽。
基于仿函数的连接不支持此功能。
假设有一个名为DemoWidget
的类,它有一个槽printNumber()
,它有一个默认参数:
public slots:
void printNumber(int number = 42)
{
qDebug() << "Lucky number" << number;
}
使用基于字符串的连接,DemoWidget::printNumber()
可以连接到QApplication::aboutToQuit()
,即使后者没有参数。
基于仿函数的连接将产生一个编译时错误:
DemoWidget::DemoWidget(QWidget *parent)
: QWidget(parent) {
// OK: printNumber() will be called with a default value of 42
connect(qApp, SIGNAL(aboutToQuit()),
this, SLOT(printNumber()));
// ERROR: Compiler requires compatible arguments
connect(qApp, &QCoreApplication::aboutToQuit,
this, &DemoWidget::printNumber);
}
使用基于字符串的语法,参数类型是显式指定的。因此,重载信号或槽的实例是明确的。相反,在基于仿函数的语法中,必须对重载信号或槽进行类型转换,以告诉编译器使用哪个实例。
例如,QLCDNumber有三个版本的display()槽位:
QLCDNumber::display(int)
QLCDNumber::display(double)
QLCDNumber::display(QString)
要将int版本连接到QSlider::valueChanged()
,有以下两种语法:
auto slider = new QSlider(this);
auto lcd = new QLCDNumber(this);
// String-based syntax
connect(slider, SIGNAL(valueChanged(int)),
lcd, SLOT(display(int)));
// Functor-based syntax, first alternative
connect(slider, &QSlider::valueChanged,
lcd, static_cast(&QLCDNumber::display));
// Functor-based syntax, second alternative
void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display;
connect(slider, &QSlider::valueChanged,
lcd, mySlot);
// Functor-based syntax, third alternative
connect(slider, &QSlider::valueChanged,
lcd, QOverload::of(&QLCDNumber::display));
// Functor-based syntax, fourth alternative (requires C++14)
connect(slider, &QSlider::valueChanged,
lcd, qOverload(&QLCDNumber::display));