信号和槽
解释
当某个控件或某个事件发生时,系统中“关心”该信号的模块,执行一个Action,来响应该Signal。
机制
类似于设计模式:观察者模式。 信号发出类似于广播,通过广播的方式传递出去。 遍历“链”上的内容,如果对此信号感兴趣,就响应该消息。
signals 关键字
- 信号必须有signals关键字来声明
- 信号没有返回值,但可以有参数
- 信号就是函数的声明,只需声明,无需定义
- 使用:emit mySignal();
- 信号可以重载
signals:
void mySignal();
void mySignal(int, QString);
简单示例(Qt5 语法)
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("Quit"); //定义一个Button 控件
QObject::connect(&button, &QPushButton::clicked, &app, &QApplication::quit); //信号和槽绑定
button.show(); //显示button控件
return app.exec();
}
信号和槽的第5个参数
Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。
Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。
Signal和Slot一般形式
connect(sender, signal, receiver, slot);
- sender:发出信号的对象
- signal:发送对象发出的信号
- receiver:接收信号的对象
- slot:接收对象在接收到信号之后所需要调用的函数
Demo
#ifndef MAINWIDGET_H
#define MAINWIDGET_H
#include <QWidget>
#include <QPushButton>
#include "subwidget.h" //子窗口头文件
class MainWidget : public QWidget
{
Q_OBJECT
public:
MainWidget(QWidget *parent = 0);
~MainWidget();
public slots: //槽函数定义
void mySlot();
void changeWin();
void dealSub();
void dealSlot(int, QString);
private:
QPushButton b1;
QPushButton *b2;
QPushButton b3;
SubWidget subWin;
};
#endif // MAINWIDGET_H
#ifndef SUBWIDGET_H
#define SUBWIDGET_H
#include <QWidget>
#include <QPushButton>
class SubWidget : public QWidget
{
Q_OBJECT
public:
explicit SubWidget(QWidget *parent = 0);
void sendSlot();
signals:
/* 信号必须有signals关键字来声明
* 信号没有返回值,但可以有参数
* 信号就是函数的声明,只需声明,无需定义
* 使用:emit mySignal();
* 信号可以重载
*/
void mySignal();
void mySignal(int, QString);
public slots:
private:
QPushButton b;
};
#endif // SUBWIDGET_H
#include "mainwidget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWidget w;//执行MainWidget的构造函数
w.show();
return a.exec();
}
#include "mainwidget.h"
#include <QPushButton>
#include <QDebug> //打印
MainWidget::MainWidget(QWidget *parent)
: QWidget(parent)
{
b1.setParent(this);
b1.setText("close");
b1.move(100, 100);
b2 = new QPushButton(this);
b2->setText("abc");
connect(&b1, &QPushButton::pressed, this, &MainWidget::close);
/* &b1: 信号发出者,指针类型
* &QPushButton::pressed:处理的信号, &发送者的类名::信号名字
* this: 信号接收者
* &MainWidget::close: 槽函数,信号处理函数 &接收的类名::槽函数名字
*/
/* 自定义槽,普通函数的用法
* Qt5:任意的成员函数,普通全局函数,静态函数
* 槽函数需要和信号一致(参数,返回值)
* 由于信号都是没有返回值,所以,槽函数一定没有返回值
*/
connect(b2, &QPushButton::released, this, &MainWidget::mySlot);
connect(b2, &QPushButton::released, &b1, &QPushButton::hide);
/* 信号:短信
* 槽函数:接收短信的手机
*/
setWindowTitle("老大");
//this->setWindowTitle("老大");
b3.setParent(this);
b3.setText("切换到子窗口");
b3.move(50, 50);
//显示子窗口
//subWin.show();
connect(&b3, &QPushButton::released, this, &MainWidget::changeWin);
//处理子窗口的信号
// void (SubWidget::*funSignal)() = &SubWidget::mySignal;
// connect(&subWin, funSignal, this, &MainWidget::dealSub);
// void (SubWidget::*testSignal)(int, QString) = &SubWidget::mySignal;
// connect(&subWin, testSignal, this, &MainWidget::dealSlot);
//Qt4信号连接
//Qt4槽函数必须有slots关键字来修饰
connect(&subWin, SIGNAL(mySignal()), this, SLOT(dealSub()) );
connect(&subWin, SIGNAL(mySignal(int,QString)),
this, SLOT(dealSlot(int,QString)) );
// SIGNAL SLOT 将函数名字 -> 字符串 不进行错误检查
//Lambda表达式, 匿名函数对象
//C++11增加的新特性, 项目文件: CONFIG += C++11
//Qt配合信号一起使用,非常方便
QPushButton *b4 = new QPushButton(this);
b4->setText("Lambda表达式");
b4->move(150, 150);
//int a = 10, b = 100;
connect(b4, &QPushButton::clicked,
// = :把外部所有局部变量、类中所有成员以值传递方式
// this: 类中所有成员以值传递方式
// & : 把外部所有局部变量, 引用符号
[=](bool isCheck)
{
qDebug() << isCheck;
}
);
resize(400, 300);
}
void MainWidget::dealSlot(int a, QString str)
{
// str.toUtf8() -> 字节数组QByteArray
// ……data() -> QByteArray -> char *
qDebug() << a << str.toUtf8().data();
}
void MainWidget::mySlot()
{
b2->setText("123");
}
void MainWidget::changeWin()
{
//子窗口显示
subWin.show();
//本窗口隐藏
this->hide();
}
void MainWidget::dealSub()
{
//子窗口隐藏
subWin.hide();
//本窗口显示
show();
}
MainWidget::~MainWidget()
{
}
#include "subwidget.h"
SubWidget::SubWidget(QWidget *parent) : QWidget(parent)
{
this->setWindowTitle("小弟");
b.setParent(this);
b.setText("切换到主窗口");
connect(&b, &QPushButton::clicked, this, &SubWidget::sendSlot);
resize(400, 300);
}
void SubWidget::sendSlot()
{
emit mySignal(); //释放信号量
emit mySignal(250, "我是子窗口"); //信号量函数重载
}
注意
- 信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。
- 允许槽函数的参数可以比信号的少,槽函数存在的参数的顺序也必须和信号的前面几个一致。
Qt4 语法的不稳定性
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton *button = new QPushButton("Quit");
connect(button, SIGNAL(clicked()), &a, SLOT(quit())); //SINGAL 和 SLOT宏, connect函数接受字符串
button.show(); //显示button控件
return app.exec();
}
- SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。
- 注意到connect()函数的 signal 和 slot 都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。
自定义信号和槽
有一个报纸类Newspaper,有一个订阅者类Subscriber。Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。
- NewPaper类
#include <QObject>
// newspaper.h //
class Newspaper : public QObject //QObject类才具有信号和槽功能
{
Q_OBJECT //凡是QObject类(不管是直接子类还是间接子类),都应该在第一行代码写上Q_OBJECT
public:
Newspaper(const QString & name) :m_name(name) //构造及初始化函数
{
}
void send() //发送信号函数
{
emit newPaper(m_name);
}
signals: //信号关键字
void newPaper(const QString &name);
private:
QString m_name;
};
- reader 类
// reader.h //
#include <QObject>
#include <QDebug>
class Reader : public QObject
{
Q_OBJECT
public:
Reader() {}
void receiveNewspaper(const QString & name)
{
qDebug() << "Receives Newspaper: " << name;
}
};
- test
// main.cpp //
#include <QCoreApplication>
#include "newspaper.h"
#include "reader.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Newspaper newspaper("Newspaper A"); //报纸对象
Reader reader; //读者
QObject::connect(&newspaper, &Newspaper::newPaper,
&reader, &Reader::receiveNewspaper);
newspaper.send();
return app.exec();
}
Signal函数定义
- 信号就是一个个的函数名,返回值是 void(因为无法获得信号的返回值,所以也就无需返回任何值)。
- 参数是该类需要让外界知道的数据(即槽接收到的数据)。
- 信号作为函数名,不需要在 cpp 函数中添加任何实现。
emit 关键字
emit 是 Qt 对 C++ 的扩展,是一个关键字(其实也是一个宏)。emit 的含义是发出,也就是发出newPaper()信号。
数据从发送者到接收者的转移
emit 发出newPaper()信号。感兴趣的接收者会关注这个信号,可能还需要知道是哪份报纸发出的信号?所以,我们将实际的报纸名字m_name当做参数传给这个信号。当接收者连接这个信号时,就可以通过槽函数获得实际值。这样就完成了数据从发出者到接收者的一个转移。
Slot函数
哪些函数可以作为槽函数
以下 函数都可以作为槽函数。
- 成员函数
- static 函数
- 全局函数
- Lambda 表达式
- 与信号函数不同,槽函数必须自己完成实现代码。
- 槽函数就是普通的成员函数,因此作为成员函数,也会受到 public、private 等访问控制符的影响。(如果信号是 private 的,这个信号就不能在类的外面连接,也就没有任何意义。)
自定义信号和槽函数注意事项
1.发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
2.使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;
3.槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
4.使用 emit 在恰当的位置发送信号;
5.使用QObject::connect()函数连接信号和槽。
6.任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数
信号和槽的更多用法
一个信号可以和多个槽相连
如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。
多个信号可以连接到一个槽
只要任意一个信号发出,这个槽就会被调用。
一个信号可以连接到另外的一个信号
当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。
槽可以被取消链接
这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。
信号和槽中使用Lambda 表达式
在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。
我们的代码可以写成下面这样:
QObject::connect(&newspaper, static_cast<void (Newspaper:: *)
(const QString &)>(&Newspaper::newPaper),
[=](const QString &name)
{
/* Your code here. */
}
);