【QT|趣谈】最详细的信号槽机制介绍!

1 导语:信号槽的前世今生

信号槽机制创造的意义并非是在没有路的地方修建了路,而更像是增加可选的路径。每每提到QT,我们就不可避免的会谈论信号槽机制。这是一项伟大的发明,在GUI领域,它在对象的通讯方式中独树一帜——自由而安全,分散但灵活,这是我对于它的粗浅认识。

让我们来谈谈它的出身吧。遥想回调函数风光的那些年,总有那么一群程序员被它的指针弄得心烦意乱,他们开始在各大论坛上吐槽回调机制的不安全性、变量传递还有耦合太强的烦恼……

所谓时势造英雄,信号槽的机制很好的解决了高耦合与线程堵塞的问题——等等,高耦合?线程堵塞?这些高深的名词是怎么回事,一点也不有趣啦!不要着急,且听我慢慢道来。

(注:秉持负责的态度,还是要解释一下高耦合和线程堵塞的概念的。)
①高耦合指的软件系统结构中各模块间紧密联系的一种状态。

更学术一点的说法要从耦合性来说起——耦合性:也称块间联系,指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。

②线程阻塞通常是指一个线程在执行过程中暂停,以等待某个条件的触发。线程堵塞往往会造成线程死锁,在多任务系统下,当一个或多个进程等待系统资源,而资源又被进程本身或其它进程占用时,就形成了死锁。有个变种叫活锁。

用一个形象的例子说明:就拿我们日常生活中的限量版商品生产和消费来说,货物是有限的,商家会限制顾客数目。同理,当多线程的情况下,某个特定时间下,(峰值高并发)出现消费者速度远大于生产者速度,消费者必须阻塞来等待生产者,以保证生产者能够生产出新的数据;当生产者速度远大于消费者速度时,同样也是一个道理。这些情况都要程序员自己控制阻塞,同时又要线程安全和运行效率。

2 QT的信号槽

2.1什么是信号槽?

在信号槽中出现的概念“信号”和我们平时理解的“信号”非常相似。来一个生活中的场景:你坚持不懈地向你的老爸发短信——爸爸,这个月的生活费可以提高吗?老爸收到这个短信后要么果断拒绝,要么想想觉得你这个月比较努力,给出肯定答复。

在这个有点搞笑的例子里,你向老爸提出的“提高生活费”就是一个信号,你就是信号的发出者,而信号的另一端,也就是你的老爸,就是这个信号的接受者,他给出的选择即相当于槽函数的实现。而整个过程的联系是你与老爸的亲情关系和抚养义务,换而言之,即便你群发这个消息到整个世界,能响应你的也就是那些有联系的人罢了,至于其他人,他们并不会给你生活费……

扫描二维码关注公众号,回复: 15019160 查看本文章

图一 信号槽趣例
在这里插入图片描述

其实笔者给出的是一个很狡猾的例子,这里包含了很多东西:

  1. 信号被设计成什么都不做。
  2. 一个信号可以发送给多个槽,同样,多个信号也能发给一个槽。
  3. 信号与槽是独立分散的不同个体,由特定的连接实现通讯。
  4. 信号与槽能链接,是设立在槽函数对于信号是感兴趣的基础上的。

好了,小伙伴,看懂这个例子,恭喜你信号槽机制入门了!至于要不要继续学就看你自己的了^ ^

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用例有参考其他博主。

猜你喜欢

转载自blog.csdn.net/qq_42671556/article/details/107701684