用最有趣的方式解释信号槽通讯机制
1 导语:信号槽的前世今生
信号槽机制创造的意义并非是在没有路的地方修建了路,而更像是增加可选的路径。每每提到QT,我们就不可避免的会谈论信号槽机制。这是一项伟大的发明,在GUI领域,它在对象的通讯方式中独树一帜——自由而安全,分散但灵活,这是我对于它的粗浅认识。
让我们来谈谈它的出身吧。遥想回调函数风光的那些年,总有那么一群程序员被它的指针弄得心烦意乱,他们开始在各大论坛上吐槽回调机制的不安全性、变量传递还有耦合太强的烦恼……
所谓时势造英雄,信号槽的机制很好的解决了高耦合与线程堵塞的问题——等等,高耦合?线程堵塞?这些高深的名词是怎么回事,一点也不有趣啦!不要着急,且听我慢慢道来。
(注:秉持负责的态度,还是要解释一下高耦合和线程堵塞的概念的。)
①高耦合指的软件系统结构中各模块间紧密联系的一种状态。
更学术一点的说法要从耦合性来说起——耦合性:也称块间联系,指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。
②线程阻塞通常是指一个线程在执行过程中暂停,以等待某个条件的触发。线程堵塞往往会造成线程死锁,在多任务系统下,当一个或多个进程等待系统资源,而资源又被进程本身或其它进程占用时,就形成了死锁。有个变种叫活锁。
用一个形象的例子说明:就拿我们日常生活中的限量版商品生产和消费来说,货物是有限的,商家会限制顾客数目。同理,当多线程的情况下,某个特定时间下,(峰值高并发)出现消费者速度远大于生产者速度,消费者必须阻塞来等待生产者,以保证生产者能够生产出新的数据;当生产者速度远大于消费者速度时,同样也是一个道理。这些情况都要程序员自己控制阻塞,同时又要线程安全和运行效率。
2 QT的信号槽
2.1什么是信号槽?
在信号槽中出现的概念“信号”和我们平时理解的“信号”非常相似。来一个生活中的场景:你坚持不懈地向你的老爸发短信——爸爸,这个月的生活费可以提高吗?老爸收到这个短信后要么果断拒绝,要么想想觉得你这个月比较努力,给出肯定答复。
在这个有点搞笑的例子里,你向老爸提出的“提高生活费”就是一个信号,你就是信号的发出者,而信号的另一端,也就是你的老爸,就是这个信号的接受者,他给出的选择即相当于槽函数的实现。而整个过程的联系是你与老爸的亲情关系和抚养义务,换而言之,即便你群发这个消息到整个世界,能响应你的也就是那些有联系的人罢了,至于其他人,他们并不会给你生活费……
图一 信号槽趣例
其实笔者给出的是一个很狡猾的例子,这里包含了很多东西:
- 信号被设计成什么都不做。
- 一个信号可以发送给多个槽,同样,多个信号也能发给一个槽。
- 信号与槽是独立分散的不同个体,由特定的连接实现通讯。
- 信号与槽能链接,是设立在槽函数对于信号是感兴趣的基础上的。
好了,小伙伴,看懂这个例子,恭喜你信号槽机制入门了!至于要不要继续学就看你自己的了^ ^
2.2 用严谨的语言解释信号槽
可能有同学看完这个例子对信号槽有了初步印象,那么乘热打铁,我用更加严谨的语言来解释一下什么是信号槽吧。
信号只是一个特殊的成员函数声明,函数的返回值是void类型,函数只能声明不能定义,信号必须使用signals关键字进行声明,函数的访问属性自动被设置为protected类型,只能通过emit关键字调用函数(发射信号)。
下面是一个信号的简单实例,在注释中我给出了一切需要注意的点:
图二 信号的定义
至于“槽”,我们其实更习惯于称呼它为“槽函数”。槽就是一个可以被调用处理特定信号的函数。需要注意的是:信号与槽的原型应该完全相同,槽函数的返回值必须是void类型,槽函数可以像普通成员函数一样被调用。
3 信号槽实战
3.1 信号槽实例
哈哈,没想到吧,我把我向老爸要生活费的例子写了出来!我几乎把所有要注意的点都融入进去了。
它的结果是这样的(请暂时忽略别的东西,这些在代码中用于提示注意要点)
点击ask for money 的按钮,会出现如下窗口:
点击give my daughter money,打印出这一段话:
这就完美的完成了从要求给生活费到打钱的响应过程,如果你想看源代码,下面就是。
//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 SlotEmployees();
void DealSub();
void DealSlot(int, QString);
private:
QPushButton b1;
QPushButton *b2;
QPushButton b3;
QPushButton *b4;
SubWidget w2;
};
#endif // MAINWIDGET_H
//Subwiget.h
#ifndef SUBWIDGET_H
#define SUBWIDGET_H
#include <QWidget>
#include <QPushButton>
class SubWidget : public QWidget
{
Q_OBJECT
public:
explicit SubWidget(QWidget *parent = 0);
private:
QPushButton b;
signals:
void mysignal();
void mysignal(int, QString);
public slots:
void SlotReturn();
};
#endif // SUBWIDGET_H
//Mainwidget.cpp
#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("^-^"); //->左边为指针
b2->move(100,200);
connect(&b1,SIGNAL(pressed()), this, SLOT(close()));
connect(b2,SIGNAL(released()),this,SLOT(MySlot()));
this->setWindowTitle("I am son");
b3.setParent(this);
b3.setText("send request to father");
b3.move(100,300);
b4 = new QPushButton(this);
b4->setText("^o^"); //->左边为指针
b4->move(200,200);
connect(b4,SIGNAL(released()),this,SLOT(MySlot()));//一个信号可以关联多个槽
//w2.show();
connect(&b3,SIGNAL(released()),this,SLOT(SlotEmployees()));
connect(&w2,SIGNAL(mysignal()),this,SLOT(DealSub()));
//SIGNAL,SLOT宏把函数转换为字符串,要是函数写错,它是不会报错的
connect(&w2,SIGNAL(mysignal(int,QString)),this,SLOT(DealSlot(int,QString)));
resize(500,500);
}
MainWidget::~MainWidget()
{
}
void MainWidget::MySlot()
{
b2->setText("123");
}
void MainWidget::SlotEmployees()
{
w2.show();
this->hide();
}
void MainWidget::DealSub()
{
w2.hide();
this->show();
}
void MainWidget::DealSlot(int a, QString str)
{
qDebug()<<a<<str.toUtf8(); //小写大写括号,你的qDebug()
}
//Subwidget.cpp
#include "SubWidget.h"
SubWidget::SubWidget(QWidget *parent) : QWidget(parent)
{
this->setWindowTitle(" your father");
b.setParent(this);
b.setText("give money");
b.move(200,200);
connect(&b,SIGNAL(released()),this,SLOT(SlotReturn()));
resize(500,500);
}
void SubWidget::SlotReturn()
{
emit mysignal();
emit mysignal(200, "na qu hui huo");//带参数的槽函数
}
4 信号槽plus
4.1 Lambda表达式
相比于QT4,QT5在信号槽上更进一步,它支持的ISO C++ 11 标准的一大亮点是引入Lambda表达式。而lambda表达式使用起来,至少是在形式上更简单方便。
“Lambda 表达式”(lambda expression)是一个匿名函数,
Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)
话不多说,直接用一个非常经典的实例来解释这一现象。
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 传统Qt是连接方式
// 传统Qt4连接方式为 信号发送者,信号,信号接受者,处理函数
QObject::connect(ui->pushButton,SIGNAL(clicked(bool)),this,SLOT(qT4_slot()));
//Qt5连接方式
//看起来和QT4的方式没有太大差别,只是在Qt4 中引用了信号槽,在简单的使用时没有问题,但是在庞大的工程中,信号和槽仅仅是宏替换,在编译的时候没有安全监测
//Qt5在编译的时候就会有监测 QObject::connect(ui->pushButton_2,&QPushButton::clicked,this,&Widget::qT5_slot);
//Qt5 Lambda表达式
//这里需要注意 Lambda表达式是C++ 11 的内容,所以,需要再Pro项目文件中加入 CONFIG += C++ 11
QObject::connect(ui->pushButton_3,&QPushButton::clicked,[=](){qDebug()<<"lambda 表达式";});
}
Widget::~Widget()
{
delete ui;
}
void Widget::qT4_slot()
{
qDebug()<< "This is Qt 4 Connect method";
}
void Widget::qT5_slot()
{
qDebug()<< "This is Qt 5 Connect method";
}
5 信号槽有什么优越性
5.1事件通讯与信号槽通讯的对比
终于到了对比产生彼此的优越的时候了,笔者结合了许多网络资料,进行了事件与信号槽机制的对比总结。
1.信号与槽,在同一线程,类型为直连时,信号发生,槽被调用,相当于直接的函数调用,是同步的;而事件,使用sendEvent时是同步的,使用postEvent则是异步,要等到下一轮事件循环才会被处理。
2.跨线程时,采用队列连接的信号与槽,是通过事件实现的。从这点讲,信号与槽等同于事件,只是信号与槽用起来更方便,事件稍微繁琐。
3.事件一方面是处理用户输入,比如按键,鼠标;一方面可以自定义事件用于对象间通讯。无论哪种,都会经过Qt的事件循环。而信号与槽则不一定会经过事件循环。
当然一切的方法只有相对的优越性,信号槽并非是完美的通讯方式plus,他打乱了程序的结构,看到为信号槽服务的源码时,总是有种使不上劲的感受,这是因为这些工作往往是分散的,让许多初学者(比如说我)糟心的是,即使信号槽关联出现问题,QT4往往不会报错,因为在它眼里,SIGNAL,SLOT内的仅仅是字符串而已……
注意,我无意将信号槽与其他通讯方式进行对比,一切解决方案只有在特定的环境下才能说优劣,总体而言,只有最适合的语言和解决方案,没有最完美的方案。
6 在其他语言中使用信号槽(以python为例)
信号槽功能强大,怎么不会引起其他语言的注意呢,笔者在学习QT之前学习了一段时间python GUI编程,了解到万能的python有个PYQT库可用。我们只需导入PyQt库就能在python中与信号槽进行愉快的玩耍啦!(P.S.现有PyQt4和PyQt5版本,个人感觉4版本的信号槽模式更接近于QT环境的操作,5版本的更符合python原生习惯。)
请看一个非常简单的例子,其中包含了关联信号槽,解除关联,再次连接等基本操作,其结果演示如下:
(1)对滑块进行拖动,下面即时显示数字。点击保存,保存状态显示为saved,点击运行,运行状态显示为running。
(2)解除关联之后,即使把滑块拖到极致,也没有改变,而一旦重新关联,移动滑块可以正常运行。
看完演示之后,咱们再来看看这个例子上的核心代码。
# 用connect进行关联
self.buttonRun.clicked.connect(self.buttonSave.clicked)
self.slider.valueChanged.connect(self.pBar.setValue)
self.slider.valueChanged.connect(self.lcdNumber.display)
self.buttonSave.clicked.connect(self.showMessage)
self.buttonRun.clicked.connect(self.showMessage)
self.buttonDisconnect.clicked.connect(self.unbindConnection)
self.buttonConnect.clicked.connect(self.bindConnection)
self.buttonStop.clicked.connect(self.stop)
# 用disconnect取消关联
def unbindConnection(self):
self.slider.valueChanged.disconnect()
# 用bindconnect再次关联信号槽
def bindConnection(self):
self.slider.valueChanged.connect(self.pBar.setValue)
self.slider.valueChanged.connect(self.lcdNumber.display)
# 用stop清空状态
def stop(self):
self.saveLabel.setText("")
self.runLabel.setText("")
我们可以发现,在其它语言中使用信号槽实现对象间的通讯和在QT环境下实现还是略有不同的,以上提供的是更接近于python语言习惯的信号槽机制。
虽然逻辑相近,但笔者所使用的这种方式确实在很多细节上和QT的展现形式不同。实际上,python的PYQT4库中也提供了用QT的方式进行对象之间的通讯,以上面滑块和数字的连接为例,改过来就是这样的:
# QT式信号槽响应机制
self.connect(slider, QtCore.SIGNAL('valueChanged(int)'),
lcdNumber, QtCore.SLOT('display(int)'))
这种写法完全复刻了QT4信号槽的标准写作方式,笔者认为,这种处理大抵是为了降低跨语言学习者在学习中的隔阂感。
PYQT5还提供了许多信号槽的高级玩法,笔者才疏学浅恐有遗漏之处,原创不易,欢迎指正和交流。
本文于2020/1/20 撰写,2020/7/30修改发布。
本文python用例有参考其他博主。