https://qtguide.ustclug.org/
1 信号和槽
所谓信号槽,简单来说,就像是插销一样:一个插头和一个插座。怎么说呢?当某种事件发生之后,比如,点击了一下鼠标,或者按了某个按键,这时,这个组件就会发出一个信号。就像是广播一样,如果有了事件,它就漫天发声。这时,如果有一个槽,正好对应上这个信号,那么,这个槽的函数就会执行,也就是回调。
#include <QtGui/QApplication> #include <QtGui/QPushButton> int main(int argc, char *argv[]) { QApplication a(argc, argv); QPushButton *button = new QPushButton("Quit"); // QApplication 的实例 a 说,如果button 发出了 clicked 信号,你就去执行我的 quit 函数。 QObject::connect(button, SIGNAL(clicked()), &a, SLOT(quit())); button->show(); return a.exec(); }
QObject 是所有类的根。Qt 使用这个 QObject 实现了一个单根继承的 C++。它里面有一个 connect静态函数,用于连接信号槽。
clicked()就是一个信号,而 quit()就是槽
2 应用实例
- 忽略自动补全报的错
- 在图形界面修改过后,自动补全未必及时读取新加入的控件的信息
1 在主窗口头文件Qt_tset1.h里声明这个函数FoodIsComing()
2 在主窗口函数文件Qt_tset1.cpp里实现这个函数体FoodIsComing()
3 创建链接执行函数。控件动作触发事件,然后调用函数执行
例如: 按键 的 单击动作 触发 主窗体 中的 FoodIsComing() 函数,并执行。
3 自定义信号和槽沟通
通过信号和槽机制通信,通信的源头和接收端之间是松耦合的:
- 源头只需要顾自己发信号就行,不用管谁会接收信号;
- 接收端只需要关联自己感兴趣的信号,其他的信号都不管;
- 只要源头发了信号,关联该信号的接收端全都会收到该信号,并执行相应的槽函数。
为何不用回掉函数
- 回调函数机制是很常见的,Windows 消息机制本身也是回调函数的应用,多线程编程也使用回调函数作为新线程里的任务函数。
- 我们上一节示范的三个例子,信号与槽函数可以一对一关联,一对多关联,多对一关联,如 果用回调函数实现这些复杂的映射,那会是非常头疼的事。比如希望 theSrc 同时把数据传递给 A、B、C 三个目标对象,那 SendDataTo 函数必须手动执行三次。
- 回调函数难以实现同时一发多收、多发一收,而信号和槽机制是完全可以的,并且代码非常简洁明了。
- 另外信号与槽函数可以在运行时解除关联关系,这也是回调函数不好实现的特性。
源头和接收端是非常自由的,connect 函数决定源头和接收端的关联关系,并会自动根据信号里的参数传递给接收端的槽函数。
因为源头是不关心谁接收信号的,所以 connect 函数一般放在接收端类的代码中,或者放在能同时访问源端和接收端对象的代码位置。
创建信号源
1 在窗体上创建一个按钮 显示 “发送自定义” 引用名 pushButton
2 在Qt_tset1.h 中 ,添加信号 SendMsg1(QString str) 和槽函数 ButtonClicked() 声明
#pragma once #include <QtWidgets/QMainWindow> #include "ui_Qt_tset1.h" class Qt_tset1 : public QMainWindow { Q_OBJECT public: Qt_tset1(QWidget *parent = Q_NULLPTR); //添加这一段代码 public slots: //槽函数声明标志 void FoodIsComing(); //槽函数 void PrintText(const QString& text); void ButtonClicked(); // 接收按钮信号的槽函数 需要实体代码 signals: //添加自定义的信号 void SendMsg1(QString str); //信号只需要声明,不要给信号写实体代码,因为使用了关键字 emit发信号
private: Ui::Qt_tset1Class ui; };
3 在Qt_tset1.cpp 中 ,添加信号 SendMsg1(QString str) 的关键字 和槽函数 ButtonClicked() 实体
void Qt_tset1::ButtonClicked() { //用 emit 发信号 //emit 是发信号的关键字,然后接下来就与调用函数是一样的格式,SendMsg 里面放置我们想传递的字符串参数。除了 emit 字样,触发信号就与函数调用一样。 emit SendMsg1(tr("This is the message!")); }
emit 是发信号的关键字,然后接下来就与调用函数是一样的格式,SendMsg 里面放置我们想传递的字符串参数。除了 emit 字样,触发信号就与函数调用一样。这样简单一句就实现了触发信号的过程,同之前所说的,源端就顾自己发信号,至于谁接收 SendMsg 信号,源端是不管的。
Widget 窗体代码就是上面那么多,发送我们自定义的 SendMsg 信号的过程如下图所示:
4在Qt_tset1.cpp 中 关联 信号 和 槽函数 。 按键动clicked(),触发执行ButtonClicked()函数。该函数 内部 执行 发射信号动作。
//关联 connect(ui.pushButton, SIGNAL(clicked()), this, SLOT(ButtonClicked()));
创建接收槽
添加一个新的类(新的窗口或者其他界面),里面创建接受函数接收上一个窗口发来的数据
这里创建一个QT Class新的类ShowMsg ,(也可以是 Qt GUI Class)
依赖于QObject 基类
然后可以看到新创建的ShowMsg.h和ShowMsg.cpp
接下来,我们编辑 showmsg.h ,声明接收 SendMsg 信号的槽函数 RecvMsg:
#pragma once #include <QObject> class ShowMsg : public QObject { Q_OBJECT public: //ShowMsg(QObject *parent); explicit ShowMsg(QObject *parent = 0); //构造函数 ~ShowMsg(); public slots: //接收 SendMsg 信号的槽函数 void RecvMsg(QString str); };
RecvMsg 槽函数声明的参数类型和返回类型要与 SendMsg 信号保持一致,所以参数是 QString,返回 void。
然后我们编辑 showmsg.cpp,实现 RecvMsg 槽函数:
#include "ShowMsg.h" #include <QMessageBox> ShowMsg::ShowMsg(QObject *parent) : QObject(parent) { } ShowMsg::~ShowMsg() { } //str 就是从信号里发过来的字符串 void ShowMsg::RecvMsg(QString str) { QMessageBox::information(NULL, tr("Show"), str); }
添加头文件 <QMessageBox> 包含之后,我们添加槽函数 RecvMsg 的实体代码,里面就是一句弹窗的代码,显示收到的字符串。QMessageBox::information 函数第一个参数是父窗口指针,设置为 NULL,代表没有父窗口,就是在系统桌面直接弹窗的意思。
信号和槽机制有三步,一是有源头对象发信号,我们完成了;第二步是要有接收对象和槽函数,注意,上面只是类的声明,并没有定义对象。我们必须定义一个接收端的对 象,然后才能进行第三步 connect。
编辑项目里 main.cpp,向其中添加代码,定义接收端对象,然后进行 connect:
#include "Qt_tset1.h" // 主窗体 #include "ShowMsg.h" // 接收窗体 #include <QtWidgets/QApplication> #include <QtWidgets/QLabel> //#include <iostream> //using namespace std; int main(int argc, char *argv[]) { //cout << 123 << endl; QApplication a(argc, argv); Qt_tset1 w; // ①主窗体对象,内部会发送 SendMsg 信号 ShowMsg s; //②接收端对象,有槽函数 RecvMsg //③关联,信号里的字符串参数会自动传递给槽函数 QObject::connect(&w, SIGNAL(SendMsg1(QString)), &s, SLOT(RecvMsg(QString))); // QLabel label(QLabel::tr("Hello Qt!")); //label.show(); //QPushButton *button = new QPushButton("Quit"); // QObject::connect(button, SIGNAL(clicked()), &a, SLOT(quit())); //button->show(); //显示主界面 w.show(); return a.exec(); //cout << 123 << endl; }
首先添加 "showmsg.h" 头文件包含,然后在主窗体对象 w 定义之后,定义了接收端对象 s。
主窗体对象 w 会发 SendMsg 信号,接收端 s 有对应的槽函数 RecvMsg,这样完成了信号和槽机制的头两步。
接下来第三步就是调用关联函数 QObject::connect,将源头对象、信号、接收端对象、槽函数关联。
connect 函数是通用基类 QObject 里面定义的,之前用 connect 函数都没有加类前缀,是因为在 QObject 派生类里面自动继承了 connect 函数,不需要额外的前缀。
在 main 函数里,需要手动加 QObject:: 前缀来调用 connect 函数。
关联完成之后,一旦用户点击主窗体里的按钮,我们自定义的 SendMsg 信号就会发出去,然后 接收端对象 s 里的槽函数就会执行,并且信号里的字符串也会自动传递给 RecvMsg 槽函数,然后会出现弹窗显示传递的字符串。
这个例子完整的执行流程如下图所示:
本小节需要大家学习的就是右半段的部分,我们在主窗体 ButtonClicked 函数里触发自定义的信号 SendMsg,然后通过 connect 函数关联,自动调用了接收端对象 s 的槽函数 RecvMsg,并弹窗显示了传递的字符串。
也许有读者会问,费这么大劲,为什么不直接在 ButtonClicked 里面弹窗?那不简单多了?
因为本小节的目的不是弹窗,而是为了展现自定义信号和槽函数的代码写法,理解信号和槽机制的运行流程。以后遇到复杂多窗口的界面程序,在多个窗体对象之间就可以用 上图示范的流程,来进行通信、传递数据。
4 信号关联到信号示例
信号除了可以关联到槽函数,还可以关联到类型匹配的信号,实现信号的接力触发。上个示例中因为 clicked 信号没有参数,而 SendMsg 信号有参数,所以不方便直接关联。本小节示范一个信号到信号的关联,将按钮的 clicked 信号关联到一个参数匹配的 SendVoid 信号。
重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
4.1 发射信号
1创建新按钮
Qt_tset1.h添加我们自定义的信号SendVoid():
2新添加的 SendVoid 信号声明,没有参数,所以能和按钮的 clicked 信号匹配,实现信号到信号的关联。
添加关联函数调用:
仅在构造函数里加了一句 connect 调用,注意 connect 函数第四个参数是 SIGNAL(SendVoid()),这就是关联到信号的用法。以前都是关联到槽函数,这里直接关联到自定义的信号,而不需要槽函数中转。
关联之后,一旦按钮的 clicked 信号触发,主窗体的信号 SendVoid() 紧跟着自动触发,实现信号触发的接力过程。
4.2 接收信号
自定义信号的触发过程编完之后,下面为项目添加新的ShowMsg 类 也是从 QObject 派生
然后我们声明自定义的槽函数,用于接收 SendVoid() 信号,打开 showvoid.h,编辑如下:
#pragma once #include <QObject> class ShowMsg : public QObject { Q_OBJECT public: //ShowMsg(QObject *parent); explicit ShowMsg(QObject *parent = 0); //构造函数 ~ShowMsg(); public slots: //接收 SendMsg 信号的槽函数 void RecvMsg(QString str); //接收 SendVoid() 信号的槽函数 void RecvVoid(); };
头文件增加了与 SendVoid() 信号匹配的槽函数 RecvVoid() 声明。
然后我们编辑 ShowMsg.cpp,添加槽函数实体代码:
#include "ShowMsg.h" #include <QMessageBox> ShowMsg::ShowMsg(QObject *parent) : QObject(parent) { } ShowMsg::~ShowMsg() { } //str 就是从信号里发过来的字符串 void ShowMsg::RecvMsg(QString str) { QMessageBox::information(NULL, tr("Show"), str); } //槽函数,弹窗 void ShowMsg::RecvVoid() { QMessageBox::information(NULL, tr("Show"), tr("Just void.")); }
有了 ShowVoid 类声明是不够的,接收信号需要一个对象实体,然后才能关联,所以同样地,编辑 main.cpp 文件,添加代码如下:
#include "Qt_tset1.h" // 主窗体 #include "ShowMsg.h" // 接收窗体 #include <QtWidgets/QApplication> #include <QtWidgets/QLabel> //#include <iostream> //using namespace std; int main(int argc, char *argv[]) { //cout << 123 << endl; QApplication a(argc, argv); Qt_tset1 w; // ①主窗体对象,内部会发送 SendMsg 信号 ShowMsg s; //②接收端对象,有槽函数 RecvMsg //③关联,信号里的字符串参数会自动传递给槽函数 QObject::connect(&w, SIGNAL(SendMsg1(QString)), &s, SLOT(RecvMsg(QString))); //关联源头的信号和接收端的槽函数 QObject::connect(&w, SIGNAL(SendVoid()), &s, SLOT(RecvVoid())); // QLabel label(QLabel::tr("Hello Qt!")); //label.show(); //QPushButton *button = new QPushButton("Quit"); // QObject::connect(button, SIGNAL(clicked()), &a, SLOT(quit())); //button->show(); //显示主界面 w.show(); return a.exec(); //cout << 123 << endl; }
执行流程如下图所示:
主窗体里将信号关联到信号,是需要大家学会用的。也许有读者会问,为什么不直接将 ui->pushButton 的信号关联到最终的目的端 s 呢?
因为 ui 是主窗体对象 w 的私有成员变量,在类外不可访问,无论是 main 函数还是 ShowVoid 类的代码里,都是看不到 ui->pushButton 这个按钮的,源头都找不到,是没法关联的。
如果把私有变量 ui 改成公有的,那会破坏类的封装性,不建议这么弄。在面对私有成员无法访问的情况下,使用信号接力是比较科学的方法。