Qt 6.x中的信号和槽介绍及示例
信号(signals)和槽(slots)用于对象之间的通信,Qt使用信号和槽完成了事件监听操作。信号和槽机制是Qt的核心特性,可能也是与其它框架提供的特性最大的不同之处。信号和槽是通过Qt的元对象系统(Meta-Object system)实现的,Qt的元对象系统使信号和槽成为可能。其它框架或工具包可能使用回调(callback)来实现这种通信。信号和槽类似于设计模式中的观察者模式。
在Qt中,我们有一个回调技术的替代方案:signals and slots。当特定事件发生时,发出信号(a signal is emitted).Qt的widgets(控件或部件)有许多预定义的信号,但我们总是可以subclass widgets,向它们添加我们自己的信号。槽是响应特定信号而调用的函数。Qt的widgets有许多预定义的槽,但通常的做法是subclass widgets并添加自己的槽,这样就可以处理感兴趣的信号。
信号和槽机制(mechanism)是类型安全的:信号的签名(signature)必须与接收槽的签名相匹配(事实上,一个槽的签名可能比它接收到的信号短,因为它可以忽略额外的参数)。因为签名是兼容的,所以在使用基于函数指针的语法时,编译器可以帮助我们检测类型不匹配。基于字符串的SIGNAL和SLOT语法将在运行时检测类型不匹配。信号和槽是松散耦合的(loosely coupled):发出信号的类既不知道也不关心哪个槽接收信号。Qt的信号和槽机制确保如果你将一个信号连接(connect)到一个槽,此槽将在正确的时间用信号的参数被调用。信号和槽可以接受任意数量的任意类型的参数。它们是完全类型安全的。
所有继承自QObject或它的子类(例如,QWidgets)的类都可以包含信号和槽。当对象以其他对象可能感兴趣的方式改变其状态时,就会发出信号(Signals are emitted by objects when they change their state in a way that may be interesting to other objects).这就是对象进行通信所做的全部工作。它不知道也不关心是否有任何东西在接收它发出的信号。
槽可以用于接收信号,但它们也是普通的成员函数。就像一个对象不知道是否有任何东西接收到它的信号一样,一个槽也不知道它是否有任何信号连接到它(Just as an object does not know if anything receives its signals, a slot does not know if it has any signals connected to it)。
你可以将任意数量的信号连接到单个槽(多个信号可以连接到一个槽),一个信号可以连接到任意数量的槽(一个信号可以和多个槽相连)。甚至可以将一个信号直接连接到另一个信号(这将在第一个信号发出(emit)时立即发出第二个信号)。如下图所示:https://doc.qt.io/qt-6/signalsandslots.html
信号:当对象(object)的内部状态(internal state)以对象的客户端或所有者可能感兴趣的某种方式发生变化时,对象就会发出信号。信号是公共访问函数,可以从任何地方发出,但我们建议只从定义信号及其子类的类发出。
当信号发出时,连接到它的槽通常立即执行,就像普通的函数调用一样。当这种情况发生时,信号和槽机制完全独立于任何GUI事件循环。一旦所有槽都返回,emit语句后面的代码就会执行。当使用队列连接(queued connections)时,情况略有不同;在这种情况下,emit关键字后面的代码将立即继续,槽将在稍后执行。
如果几个槽连接到一个信号,那么当信号发出时,这些槽将按照它们连接的顺序(in the order they have been connected)依次执行。
信号由moc(Meta-Object Compiler, 元对象编译器)自动生成,不能在.cpp文件中实现。它们永远不能有返回类型(使用void)。如果C++文件中的类声明中包含了宏Q_OBJECT,则编译时会生成一个新的cpp文件,默认文件名为moc_项目名称.cpp。信号也是一个函数,只是我们只管写信号声明,而信号实现Qt会帮助我们自动生成;槽函数不仅需要写函数声明,其实现也必须自己来实现。当Qt发现signals标志后,默认为我们是在定义信号,Qt则帮助我们生产了信号的实现体。当Qt发现slots标志时,Qt元对象系统是用来解析槽函数时用的。
关于参数的注意事项:如果信号和槽不使用特殊类型(special types),它们就更容易被重用。
槽:当连接到槽的信号发出时,槽被调用。槽是普通的C++函数,可以正常调用;它们唯一的特点是信号可以连接到它们。
因为槽是普通的成员函数,所以直接调用时遵循普通的C++规则。然而,作为槽,任何组件(component)都可以通过signal-slot连接调用它们,无论其访问级别如何。这意味着从任意类的实例发出的信号可能导致在不相关的类的实例中调用私有槽。
你还可以将槽定义为vritual,我们发现这在实践中非常有用。
与回调相比,信号和槽稍微慢一些,因为它们提供了更大的灵活性,尽管对于实际应用程序来说这种差异是微不足道的。
注意:当与基于Qt的应用程序一起编译时,其它定义名为信号或槽的变量的库可能会导致编译器警告和错误。若要解决此问题,请#undef有问题的预处理器符号。
所有包含信号或槽的类都必须在其声明的顶部提及(mention) Q_OBJECT.它们还必须(直接或间接)从QObject派生。QObject是所有类的基类。
槽函数:on_控件ID_控件函数(参数), 不需要通过connect函数将信号与槽关联起来
(1).必须是on_开头;
(2).控件ID是object name,是唯一的;
(3).控件函数:控件自身具有的函数,如按键的clicked()
自定义信号注意事项:
(1).只能是类的成员函数;
(2).返回值必须是void类型;
(3).需要使用signals关键字进行声明;
(4).只需要声明;
(5).通过emit发送信号,也可以不用emit直接调用;
(6).支持重载.
自定义槽函数注意事项:
(1).可以是类的成员函数、全局函数、静态函数、Lambda表达式;
(2).返回值必须是void类型;
(3).支持重载;
(4).参数是用来接收信号传递的数据,信号传递的数据就是信号的参数;
(5).使用slots关键字进行声明,也可以省略.
通过QObject::connect函数关联信号和槽函数。connect函数是在QObject中实现的,并不局限于GUI,因此,只要继承QObject类,就可以使用信号和槽机制。connect函数一般写在窗口的构造函数中,相当于在事件产生之前在Qt框架中先进行注册。
Qt5及后,不推荐使用SIGNAL/SLOT老式用法,如:connect(btn, SIGNAL(clicked()), this, SLOT(close())); 推荐使用:connect(btn, &QPushButton::clicked, this, &MainWindow::close); 老式写法,在编译的时候即使信号或槽不存在也不会报错,但是在执行的时候无效。新方法,如果编译的时候信号或槽不存在是无法编译通过的,相当于编译时检查,不容易出错。
注:以上内容来自于网络整理
以下为测试代码:
Widgets_Test.h:原名为mainwindow.h
#pragma once
#include <qstring.h>
#include <QtWidgets/QMainWindow>
#include "ui_Widgets_Test.h"
class Widgets_Test : public QMainWindow
{
Q_OBJECT
public:
Widgets_Test(QWidget *parent = nullptr);
~Widgets_Test();
signals:
void get_address(const QString& str);
private slots:
// 槽函数:on_<object name>_<signal name> : 自动关联,无需使用connect将信号与槽关联
// 在Qt Creator中,打开ui,然后选中按钮,点击右键,然后选择"转到槽...",双击clicked(),会自动生成槽函数
void on_csdn_address_clicked();
void on_github_address_clicked();
void set_address(const QString& str);
private:
Ui::Widgets_TestClass ui;
};
Widgets_Test.cpp:原名为mainwindow.cpp
#include "Widgets_Test.h"
#include <string>
#include <qdebug.h>
#include <qmessagebox.h>
Widgets_Test::Widgets_Test(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
//connect(this, SIGNAL(get_address(QString)), this, SLOT(set_address(QString))); // 不推荐使用SIGNAL, SLOT
connect(this, &Widgets_Test::get_address, this, &Widgets_Test::set_address);
}
Widgets_Test::~Widgets_Test()
{}
void Widgets_Test::on_csdn_address_clicked()
{
emit get_address("csdn");
//get_address("csdn"); // 可以省略emit
}
void Widgets_Test::on_github_address_clicked()
{
emit get_address("github");
}
void Widgets_Test::set_address(const QString& str)
{
// 试试std::string到QString的转换
const static std::string csdn = "https://blog.csdn.net/fengbingchun/", github = "https://github.com/fengbingchun";
QString ret = "^_^";
if (str == "csdn") {
ret = QString::fromStdString(csdn);
} else if (str == "github") {
ret = QString::fromStdString(github);
}
//qDebug() << ret;
QMessageBox::information(nullptr, "Result", ret, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
}
main.cpp:
#include "Widgets_Test.h"
#include <QtWidgets/QApplication>
#include <QDebug>
#include <qpushbutton.h>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widgets_Test w;
// 信号、槽:点击Close按钮后关闭窗口
QPushButton button("Close", &w);
button.move(QPoint(500, 350));
button.resize(50, 20);
// Qt预定义的信号和槽
QObject::connect(&button, &QPushButton::clicked, &w, &Widgets_Test::close);
w.show();
return a.exec();
}
执行结果如下图所示:点击csdn address按钮,会弹出对话框,显示csdn地址;点击github address按钮,会弹出对话框,显示github地址;点击Close按钮,关闭应用程序
GitHub:https://github.com/fengbingchun/Qt_Test