Qt 编程指南 3 信号和槽沟通

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 改成公有的,那会破坏类的封装性,不建议这么弄。在面对私有成员无法访问的情况下,使用信号接力是比较科学的方法。

原文地址:https://www.cnblogs.com/kekeoutlook/p/7468212.html