Qt信号和槽机制详解(新手必看)
在 GUI 编程中,当改变了一个部件时,总希望其他部件也能了解到该变化。更一般地说,我们希望任何对象都可以和其他对象进行通信。例如,用户单击了“关闭”按钮,则希望可以执行窗口的 close() 函数来关闭窗口。
为了实现对象间的通信,一些工具包中使用了回调(callback)机制,而在 Qt 中使用了信号和槽来进行对象间的通信。
当一个特殊的事情发生时,便可以发射一个信号,比如按钮被单击就发射 clicked() 信号;而槽就是一个函数,它在信号发射后被调用来响应这个信号。
在 Qt 的部件类中已经定义了一些信号和槽,但是更常用的做法是子类化部件,然后添加自定义的信号和槽来实现想要的功能。
Qt 支持一个信号关联到多个槽上,多个信号也可以关联到同一个槽上,甚至一个信号还可以关联到另一个信号上,如下图所示。

图 1 对象间信号和槽的关联图
如果存在多个槽与某个信号相关联,那么,当这个信号被发射时,这些槽将会一个接一个地执行,执行顺序与关联顺序相同。
程序的运行效果如下图所示:

图 2 信号和槽示例运行效果
新建 Qt Widgets 应用,项目名称为 mysignalslot,基类选择 QWidget,类名保持 Widget 不变。
项目建立完成后,向项目中添加新文件,模板选择 Qt 分类中的“Qt设计器界面类”,界面模板选择 Dialog without Buttons,类名设置为 MyDialog。完成后,在 mydialog.h 文件中添加代码来声明一个信号:
信号只用声明,不需要也不能对它进行定义实现。还要注意,信号没有返回值,只能是 void 类型的。
因为只有 QObject 类及其子类派生的类才能使用信号和槽机制,这里的 MyDialog 类继承自 QDialog 类,QDialog 类又继承自 QWidget 类,QWidget 类是 QObject 类的子类,所以这里可以使用信号和槽。另外,使用信号和槽还必须在类定义的最开始处添加 Q_OBJECT 宏。
双击 mydialog.ui 文件进入设计模式,在界面中添加一个 Spin Box 部件和一个 Push Button 部件,将 pushButton 的显示文本修改为“确定”。然后转到 pushButton 的单击信号 clicked() 对应的槽,更改如下:
然后到 widget.h 文件中添加自定义槽的声明:
下面打开 widget.ui 文件,向界面上拖入一个 Label 部件,更改其文本为“获取的值是:”。
然后进入 widget.cpp 文件中,添加头文件 #include "mydialog.h",再在构造函数中添加如下代码:
下面添加自定义槽的实现,这里只是简单地将参数传递来的数值显示在了标签上。
下面总结一下使用信号和槽应该注意的几点事项:
对于这种形式中的信号和槽,必须使用 SIGNAL() 和 SLOT() 宏,它们可以将其参数转化为 const char* 类型,另外,第四个参数指定的槽在声明时必须使用 slots 关键字。
connect() 函数的返回值为 QMetaObject:: Connection 类型,该返回值可以用于 QObject::disconnect(const QMetaObject::Connection &connection) 函数来断开该关联。
需要注意,在调用该 connect() 函数时,信号和槽的参数只能有类型,不能有变量名,例如写成 SLOT(showValue(int value)) 是不对的。
对于信号和槽的参数问题,基本原则是信号中的参数类型要和槽中的参数类型相对应,而且信号中的参数可以多于槽中的参数,但是不能反过来,如果信号中有多余的参数,那么它们将被忽略。
connect() 函数另一种常用的基于函数指针的重载形式如下:
要使一个成员函数可以和信号关联,那么这个函数的参数数目不能超过信号的参数数目,但是并不要求该函数拥有的参数类型与信号中对应的参数类型完全一致,只需要可以进行隐式转换即可。
使用这种重载形式,前面程序中的关联可以使用如下代码代替:
另外,这种方式还支持 C++11 中的 lambda 表达式,可以在关联时直接编写信号发射后要执行的代码,例如,前面示例程序中的关联可以写为:
如果函数还有 const 重载形式,那么还需要使用 QConstOverload 和 QNonConstOverload 类,例如:
新建 Qt Widgets 应用,项目名称为 mysignalslot2,基类选择 QWidget,类名保持 Widget 不变。完成后先在 widget.h 文件中进行函数声明:
因为在 setupUi() 函数中调用了 connectSlotsByName() 函数,所以使用自动关联的部件的定义都要放在 setupUi() 函数调用之前,而且必须使用 setObjectName() 指定它们的 objectName,只有这样才能正常使用自动关联。下面添加槽的定义:
可以看到,如果要使用信号和槽的自动关联,就必须在 connectSlotsByName() 函数之前进行部件的定义,而且要指定部件的 objectName。鉴于这些约束,虽然自动关联形式上很简单,但实际编写代码时却不常使用。
此外,在定义一个部件时,应明确地使用 connect() 函数来对其进行信号和槽的关联,这样当其他开发者看到这个部件定义时,就可以知道和它相关的信号和槽的关联了,而使用自动关联没有这么明了。
1) 断开与一个对象所有信号的所有关联:
2) 断开与一个指定信号的所有关联:
3) 断开与一个指定的receiver的所有关联:
4) 断开一个指定信号和槽的关联:
与connect()函数一样,disconnect()函数也有基于函数指针的重载形式:
为了实现对象间的通信,一些工具包中使用了回调(callback)机制,而在 Qt 中使用了信号和槽来进行对象间的通信。
当一个特殊的事情发生时,便可以发射一个信号,比如按钮被单击就发射 clicked() 信号;而槽就是一个函数,它在信号发射后被调用来响应这个信号。
在 Qt 的部件类中已经定义了一些信号和槽,但是更常用的做法是子类化部件,然后添加自定义的信号和槽来实现想要的功能。
Qt 支持一个信号关联到多个槽上,多个信号也可以关联到同一个槽上,甚至一个信号还可以关联到另一个信号上,如下图所示。

图 1 对象间信号和槽的关联图
如果存在多个槽与某个信号相关联,那么,当这个信号被发射时,这些槽将会一个接一个地执行,执行顺序与关联顺序相同。
Qt信号和槽典型应用示例
下面通过一个简单的例子来进一步讲解信号和槽的相关知识。这个例子实现的效果是:在主界面中创建一个对话框,在这个对话框中可以输入数值,当单击“确定”按钮时,关闭对话框,然后将输入的数值通过信号发射出去,最后在主界面中接收该信号并且显示数值。程序的运行效果如下图所示:

图 2 信号和槽示例运行效果
新建 Qt Widgets 应用,项目名称为 mysignalslot,基类选择 QWidget,类名保持 Widget 不变。
项目建立完成后,向项目中添加新文件,模板选择 Qt 分类中的“Qt设计器界面类”,界面模板选择 Dialog without Buttons,类名设置为 MyDialog。完成后,在 mydialog.h 文件中添加代码来声明一个信号:
class MyDialog:public QDialog{ //Q_OBJECT 是一个宏,添加它才能正常使用 Qt 的信号和槽机制 Q_OBJECT //修饰信号函数的关键字 signals: //自定义的信号函数 void dlgReturn(int message); };声明一个信号要使用 signals 关键字,在 signals 前面不能用 public、private 或 protected 等关键字,因为信号默认是 public 函数,可以从任何地方进行发射,但是建议只在声明该信号的类及其子类中发射该信号。
信号只用声明,不需要也不能对它进行定义实现。还要注意,信号没有返回值,只能是 void 类型的。
因为只有 QObject 类及其子类派生的类才能使用信号和槽机制,这里的 MyDialog 类继承自 QDialog 类,QDialog 类又继承自 QWidget 类,QWidget 类是 QObject 类的子类,所以这里可以使用信号和槽。另外,使用信号和槽还必须在类定义的最开始处添加 Q_OBJECT 宏。
双击 mydialog.ui 文件进入设计模式,在界面中添加一个 Spin Box 部件和一个 Push Button 部件,将 pushButton 的显示文本修改为“确定”。然后转到 pushButton 的单击信号 clicked() 对应的槽,更改如下:
void MyDialog::on_pushButton_clicked() { int value = ui->spinBox->value(); // 获取输入的数值 emit dlgReturn(value); // 发射信号 qDebug() << "signal is emitted"; }单击“确定”按钮便可以获取 spinBox 部件中的数值,然后使用自定义的信号将其作为参数发射出去。发射一个信号要使用 emit 关键字,例如这里发射了 dlgReturn() 信号。
然后到 widget.h 文件中添加自定义槽的声明:
private slots: void showValue(int value);槽就是普通的 C++ 函数,可以像一般的函数那样使用。声明槽建议使用 slots 关键字,一个槽可以是 private、public 或者 protected 类型的,槽也可以被声明为虚函数,这与普通的成员函数是一样的。
下面打开 widget.ui 文件,向界面上拖入一个 Label 部件,更改其文本为“获取的值是:”。
然后进入 widget.cpp 文件中,添加头文件 #include "mydialog.h",再在构造函数中添加如下代码:
MyDialog *dlg = new MyDialog(this); // 将对话框中的自定义信号与主界面中的自定义槽进行关联 connect(dlg, SIGNAL(dlgReturn(int)), this, SLOT(showValue(int))); dlg->show();这里创建了一个 MyDialog 实例 dlg,并且使用 Widget 作为父部件。然后将 MyDialog 类的 dlgReturn() 信号与 Widget 类的 showValue() 槽进行关联。
下面添加自定义槽的实现,这里只是简单地将参数传递来的数值显示在了标签上。
void Widget::showValue(int value) // 自定义槽 { ui->label->setText(tr("获取的值是:%1").arg(value)); qDebug() << "setText: " << value; }现在运行程序查看效果。这个程序自定义了信号和槽,可以看到它们使用起来很简单,只需要进行关联,然后在适当的时候发射信号即可。
下面总结一下使用信号和槽应该注意的几点事项:
- 需要继承自 QObject 类或其子类;
- 在类定义的最开始处私有部分添加 Q_OBJECT 宏;
- 槽中参数的类型要和信号参数的类型相对应,且不能比信号的参数多;
- 信号只用声明,没有定义,且返回值为 void 类型。
Qt信号和槽的关联
信号和槽的关联使用的是 QObject 类的 connect() 函数,该函数的其中一个原型如下:[static] QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)这是 Qt 5 之前默认使用的形式:
- 第一个参数为发射信号的对象,例如前面例子中的 dlg;
- 第二个参数是要发射的信号,比如 SIGNAL(dlgReturn(int));
- 第三个参数是接收信号的对象,在前面例子中是 this,表明是本部件,即 Widget(当这个参数为 this 时,也可以将这个参数省略,因为 connect() 函数还有另一个重载形式,其参数默认为 this);
- 第四个参数是要执行的槽,比如 SLOT(showValue(int)),其实该参数也可以指定一个信号,实现信号与信号的关联;
- 最后一个参数 type 表明了关联的方式,由 Qt::ConnectionType 枚举类型指定,其默认值是 Qt::AutoConnection,还有其他几个选择,具体功能如下表所示。在编程中一般使用默认值。
常量 | 描述 |
---|---|
Qt::AutoConnection | 自动关联,默认值。如果receiver存在于(lives in)发射信号的线程,使用Qt::DirectConnection,否则使用Qt::QueuedConnection。在信号被发射时决定使用哪种关联类型 |
Qt::DirectConnection | 直接关联。发射完信号后立即调用槽,只有槽执行完成返回后,发射信号处后面的代码才可以执行 |
Qt::QueuedConnection | 队列关联。当控制返回receiver所在线程的事件循环后再执行槽,无论槽执行与否,发射信号处后面的代码都会立即执行 |
Qt::BlockingQueuedConnection | 阻塞队列关联。类似Qt::QueuedConnection,不过,信号线程会一直阻塞,直到槽返回。当receiver存在于信号线程时不能使用该类型,不然程序会死锁 |
Qt::UniqueConnection | 唯一关联。这是一个标志,可以结合其他几种连接类型,使用按位或操作。这时两个对象间的相同的信号和槽只能有唯一的关联。使用这个标志主要为了防止重复关联 |
Qt::SingleShotConnection | 单射关联。这是一个标志,可以结合其他几种连接类型,使用按位或操作。当信号发射后连接会自动断开,槽只被调用一次(Qt 6.0中加入) |
对于这种形式中的信号和槽,必须使用 SIGNAL() 和 SLOT() 宏,它们可以将其参数转化为 const char* 类型,另外,第四个参数指定的槽在声明时必须使用 slots 关键字。
connect() 函数的返回值为 QMetaObject:: Connection 类型,该返回值可以用于 QObject::disconnect(const QMetaObject::Connection &connection) 函数来断开该关联。
需要注意,在调用该 connect() 函数时,信号和槽的参数只能有类型,不能有变量名,例如写成 SLOT(showValue(int value)) 是不对的。
对于信号和槽的参数问题,基本原则是信号中的参数类型要和槽中的参数类型相对应,而且信号中的参数可以多于槽中的参数,但是不能反过来,如果信号中有多余的参数,那么它们将被忽略。
connect() 函数另一种常用的基于函数指针的重载形式如下:
QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)这是 Qt 5 中加入的一种重载形式,与前者最大的不同就是,指定信号和槽两个参数时不用再使用 SIGNAL() 和 SLOT() 宏,并且槽函数不再必须使用 slots 关键字声明的函数,而可以使用任意能和信号关联的成员函数。
要使一个成员函数可以和信号关联,那么这个函数的参数数目不能超过信号的参数数目,但是并不要求该函数拥有的参数类型与信号中对应的参数类型完全一致,只需要可以进行隐式转换即可。
使用这种重载形式,前面程序中的关联可以使用如下代码代替:
connect(dlg, &MyDialog::dlgReturn, this, &Widget::showValue);使用这种方式还有一个好处就是可以在编译时进行检查,信号或槽的拼写错误、槽函数参数数目多于信号的参数数目等错误在编译时就能够被发现。所以建议在编写代码时使用这种关联方式。
另外,这种方式还支持 C++11 中的 lambda 表达式,可以在关联时直接编写信号发射后要执行的代码,例如,前面示例程序中的关联可以写为:
connect(dlg, &MyDialog::dlgReturn, this, [=](int value){ ui->label->setText(tr("获取的值是:%1").arg(value)); });这样就不再需要声明定义槽函数了。不过,当信号或者槽有重载的时候,使用这种方式就会出现问题,例如 QWidget 类中包含多种重载形式的 update() 函数:
update() update(int x, int y, int w, int h) update(const QRect &rect) update(const QRegion &rgn)直接使用 &Widget::update 无法确定要使用哪个重载形式。这种情况下,可以使用 QOverload 类来确定要使用的重载形式,其格式为:
QOverload<要使用的重载形式的参数列表,只保留类型>::of(PointerToMemberFunction);例如:
connect(timer, &QTimer::timeout, this, QOverload<>::of(&Widget::update));
如果函数还有 const 重载形式,那么还需要使用 QConstOverload 和 QNonConstOverload 类,例如:
struct Foo { void overloadedFunction(int, const QString &); void overloadedFunction(int, const QString &) const; }; ... QConstOverload<int, const QString &>::of(&Foo::overloadedFunction) ... QNonConstOverload<int, const QString &>::of(&Foo::overloadedFunction)
Qt信号和槽的自动关联
信号和槽还有一种自动关联方式,在前面已经提到过了,这里再通过例子深入讲解一下。新建 Qt Widgets 应用,项目名称为 mysignalslot2,基类选择 QWidget,类名保持 Widget 不变。完成后先在 widget.h 文件中进行函数声明:
private slots: void on_myButton_clicked();这里声明了一个槽,它使用自动关联。然后在 widget.cpp 文件中添加头文件 #include <QPushButton>,再将构造函数的内容更改如下:
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { QPushButton *button = new QPushButton(this); // 创建按钮 button->setObjectName("myButton"); // 指定按钮的对象名 button->setText(tr("关闭窗口")); ui->setupUi(this); // 要在定义了部件以后再调用这个函数 }
因为在 setupUi() 函数中调用了 connectSlotsByName() 函数,所以使用自动关联的部件的定义都要放在 setupUi() 函数调用之前,而且必须使用 setObjectName() 指定它们的 objectName,只有这样才能正常使用自动关联。下面添加槽的定义:
void Widget::on_myButton_clicked() // 使用自动关联 { close(); }这里进行了关闭部件的操作。对于槽的函数名,中间要使用前面指定的 objectName,这里是 myButton。现在运行程序,单击按钮,发现可以正常关闭窗口。
可以看到,如果要使用信号和槽的自动关联,就必须在 connectSlotsByName() 函数之前进行部件的定义,而且要指定部件的 objectName。鉴于这些约束,虽然自动关联形式上很简单,但实际编写代码时却不常使用。
此外,在定义一个部件时,应明确地使用 connect() 函数来对其进行信号和槽的关联,这样当其他开发者看到这个部件定义时,就可以知道和它相关的信号和槽的关联了,而使用自动关联没有这么明了。
Qt信号和槽断开关联
可以通过 disconnect() 函数来断开信号和槽的关联,其原型如下:bool QObject::disconnect(const char *signal, const QObject *receiver, const char *slot = 0, Qt::ConnectionType type = Qt::AutoConnection);该函数一般有下面几种用法:
1) 断开与一个对象所有信号的所有关联:
disconnect(myObject, nullptr, nullptr, nullptr);等价于:
myObject->disconnect();
2) 断开与一个指定信号的所有关联:
disconnect(myObject, SIGNAL(mySignal()), nullptr, nullptr);等价于:
myObject->disconnect(SIGNAL(mySignal()));
3) 断开与一个指定的receiver的所有关联:
disconnect(myObject, nullptr, myReceiver, nullptr);等价于:
myObject->disconnect(myReceiver);
4) 断开一个指定信号和槽的关联:
disconnect(myObject, SIGNAL(mySignal()),myReceiver, SLOT(mySlot()));等价于:
myObject->disconnect(SIGNAL(mySignal()),myReceiver, SLOT(mySlot())); disconnect(myConnection);// myConnection是进行关联时connect()的返回值
与connect()函数一样,disconnect()函数也有基于函数指针的重载形式:
bool QObject::disconnect(const char *signal, const QObject *receiver, const char *slot);其用法类似,只是其中信号、槽的参数需要使用函数指针 &MyObject::mySignal、&MyReceiver::mySlot 等形式。这个函数并不能断开信号与一般函数或者 lambda 表达式之间的关联,如果有这方面需要,可以使用 connect() 返回值进行断开。