系列文章目录
文章目录
前言
信号的三要素
- 信号源:谁发的信号
- 信号的类型:哪种类别
- 信号的处理方式:注册信号处理函数,在信号被触发的时候自动调用执行
在QT中
- 信号源:哪个控件发出的信号
- 信号的类型:用户进行不同的操作,可能触发不同的信号
比如(1)点击按钮,触发点击信号(2) 输入框中移动光标:触发移动光标的信号
(3) 勾选复选框(4) 选择下拉框
GUI程序实际上就是让用户操作,程序和用户交互,整个过程需要关注用户当前的操作具体是什么。
信号的处理方式被称作槽函数。
QT中使用connect这样的函数,信号和槽关联起来,信号触发就会自动执行槽函数
槽函数就是回调函数,callback
提前把信号处理方式准备好了再触发信号
接下来我们讨论connect函数的相关问题
一、connect函数
该函数和linux中TCP 的socket没有直接关系,是QObject提供的静态成员函数
QT中提供这些类,本身是存在一定的继承关系的。 以下是connect函数的定义。
connect(const QObject*sender,//信号源:哪个控件发出的信号
const char* signal, //信号类型
const QObject* receiver,//如何处理 哪个对象负责处理
const char*method,//如何处理:整个对象该怎么处理
Qt::ConnectionType type = Qt::AutoConnection)//暂时不考虑,很少使用
二、例:点击界面按钮关闭窗口
按住ctrl + 鼠标左键可以跳转到定义
alt + ←可以跳转回去
connect(button,&QPushButton::clicked,this,&Widget::close);
上述connect函数其中的信号也是成员函数
click是slot函数(槽函数),调用的时候相当于点击了一下按钮
clicked,点完了之后触发点击信号
图标带锯齿是槽函数,WiFi图标的是信号
- 在connect函数中
connect(button,&QPushButton::clicked)
前两个参数必须是匹配的 - button的类型是QPushButton*,第二个参数必须是QPushButton内置的信号(父类的信号),不能是其他的类
connect(button,&QPushButton::clicked,this)
this是指父类为执行对象(widget),
connect(button,&QPushButton::clicked,this,&Widget::close)
close是QWidget内置的槽函数,Widget。- close槽函数功能已经内部实现好了,具体作用就是关闭窗口,点击后就会关闭。
两个问题
- 如何知道QPushButton有clicked信号
- QWidget有一个close槽
文档!
如果翻阅文档的时候,没有找到,可以看看父类
QT中有很多按钮,QAbstractButton中式各种按钮的共性内容
查阅文档的时候,关注信号的发送时机(用户进行什么样的操作产生这个信号)
2,4填写的是函数指针,
为什么函数定义和实现不一样?
connect(const QObject*sender,//信号源:哪个控件发出的信号
const char* signal, //信号类型
const QObject* receiver,//如何处理 哪个对象负责处理
const char*method,//如何处理:整个对象该怎么处理
Qt::ConnectionType type = Qt::AutoConnection)//暂时不考虑,很少使用
connect(button,&QPushButton::clicked,this,&Widget::close)
观察函数定义,在2,4位置定义是char*,而我们填入的是void(*)(),和bool(*)(),
- C++中是不允许不同的指针类相互赋值(函数传参),这是因为给出的是旧版本的。此时给信号参数传参要搭配signal宏,槽传参搭配slot宏,将传入的函数指针转成char*。
connect(button,SIGNAL(&QPushButton::clicked),this,SLOT(&Widget::close))
- QT5开始,对上述写法做出简化,不需要写两个宏,给connect提供重载版本,重载版本中,2,4参数成了泛型参数允许传入任意类型的函数指针了
定义如下:
template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection)
现在有了1->Func1,2->Func2类型萃取器
此时的connect带有了参数检查功能,如果第一,第二个参数不匹配,或者3,4个参数不匹配,代码就会编译出错。
三、自定义槽函数
自定义信号 -> QPushButton
自定义槽 -> 普通的成员函数(没区别)
按钮修改标题完整代码
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button ->move(100,100);
connect(button,&QPushButton::clicked,this,&Widget::handleClicked);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleClicked()
{
//按按钮,修改窗口标题
this -> setWindowTitle("按钮已经按下");
}
以前必须将槽函数放到public slots:
QT自己的扩展关键字(不是C++的标准语法)
QT广泛使用元编程技术,调用专门的扫描器,扫描代码中特定的关键字,基于关键字生成代码,因此槽函数设置为public即可。
(不能理解的可以看看c++的相关知识)
图形化的方式
图形化界面设置按钮,右键,转到槽
- QPushButton提供的所有信号都包含在这里(还包含了QPushButton父类的信号)
- 选择clicked就会看到生成好的函数(声明和定义),可以直接编写需要的代码。
- 该函数不用connect!!!
- 因为在QT中,除了通过connect连接信号槽之外,还可以通过函数名字的方式自动连接
void Widget::on_PushButton_clicked()//该名字包含了信息
当命名规则符合了上述规则,QT也能自动把信号和槽连接起来。
- 因为QT调用
QMwtaObject::connectSlotsByName:
调用该函数就会触发上述自动连接信号槽的规则,在自动生成的ui_Widget.h中调用
用哪种方式呢?
图形化界面创建控件,推荐快速方式
如果代码方式创建,手动connect,(自己的代码没有调用自动命名规则)
四、自定义信号
- 自定义槽函数是一件很关键的事情。(用户触发之后的业务逻辑)
- 自定义信号很少见,实际开发中很少,因为用户的操作可以穷举,内置信号基本覆盖到上述所有可能的用户操作。因此自定义信号本身代码比较简单。
自己的Widget虽然没有定义任何信号,但是由于继承自QWidget和Qobject,这俩类里面已经提供了一些信号了,可以直接使用 - QT信号本身就是一个函数,QT高版本中,槽函数和普通成员函数没区别,但是信号很特殊,
- 只要写出函数声明并且告诉QT是一个信号即可,函数定义在编译过程自动生成(该过程程序员无法干预)。
信号在QT中式特殊的机制,QT生成的信号函数的实现,要配合QT框架做很多既定的操作
- 作为信号函数,函数返回值必须是void,有没有参数都可以,甚至支持重载
siganls是QT自己扩展出来的关键字,qmake的时候,调用一些代码的分析/生成工具,扫描到类中包含signals这个关键字的时候,就会自动把下面声明认为是信号,给这些信号自动的生成函数定义
connect(this,&Widget::mySignal,this,&Widget::handleMySignal);
//建立连接不代表信号发出
}
如何触发自定义的信号呢?
- QT内置信号,都不需要手动通过代码来触发,用户在GUI进行某些操作,会自动触发信号(发射信号的代码已经内置到QT框架中了)
但是自己的信号,QT提供了emit mySignal();
完整代码如下:
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(this,&Widget::mySignal,this,&Widget::handleMySignal);//建立连接不代表信号发出
emit mySignal();
}
Widget::~Widget()
{
delete ui;
}
void Widget::handleMySignal()//注意别忘了写声明
{
this->setWindowTitle("处理自定义信号");//设置窗口标题
}
发送的自定义信号,可以在任意合适的代码中,不一定在构造函数里面也可以自定义点击的时候发送信号。
比如可以在点击按钮的时候发送信号
void Widget::on_pushButton_clicked()
{
emit mySignal();
}
其实在QT5中emit啥都没做,真正操作都包含在mySignal内部生成的函数定义中了,即使不写emit也能把信号发射出去,写了之后代码可读性更高
五、带参数的信号和槽
当信号带参数,槽的参数必须和信号一致,发射信号的时候就可以给信号函数传递实参,与之对应的参数就会被传递到对应的槽函数中(此时起到了传递参数的作用)
signals:
void mySignal(const QString&);//自动生成定义
public:
void handleMySignal(const QString&);
- 参数一致要求的主要是类型,个数不一致也可以,不一致的时候要求信号的参数的个数必须比槽的参数个数更多。
- 形参名字可以不写,只写类型(C++基础语法中的)
void Widget::handleMySignal(const QString& text)
{
this->setWindowTitle(text);//槽
}
void Widget::on_pushButton_clicked()
{
emit mySignal("带参数的信号");//信号
}
传参起到复用代码的效果,比如有多个逻辑,逻辑整体一致但是涉及的数据不同,可以通过函数-参数来复用代码,并且在不同的场景中传入不同的参数。
void Widget::handleMySignal(const QString& text)
{
this->setWindowTitle(text);//槽
}
void Widget::on_pushButton_clicked()
{
emit mySignal("信号1");
}
void Widget::on_pushButton_2_clicked()
{
emit mySignal("信号2");
}
QT很多内置信号也带有参数,但不是自己传递,比如clicked,有两种版本,bool表示当前按钮是否处于选中状态,对于QCheckBox很有用
参数个数不一致的情况
- 在上述基础上给信号函数增加参量,不影响实现(信号2,槽1)
- 信号个数少于槽函数参数个数(信号1槽2),会报错
一个槽函数有可能会绑定多个信号,如果严格的要求个数一致,就意味着信号绑定到槽的要求变高了。当前情况下绑定灵活,更多信号可以绑定到槽函数,个数不一致,槽函数会按照参数顺序拿到钱N个参数,至少要确保槽函数到每个参数都有值。(不能少)
参数类型不一致
信号和槽参数类型不一致会导致无法编译
使用信号槽的前提
QT中如果某个类能够使用信号槽,必须在最开始写下Q_OBJECT的宏,该宏能展开成很多额外的代码。之后的宏还能进一步展开,最终会得到很多复杂的代码,如果不加宏会编译出错
信号和槽存在的意义
- 信号槽解决的是响应用户的操作
信号槽在GUI开发的各种框架中,比较有特色(其他GUI更简洁,比如网页开发js+dom api,主要是挂回调函数,处理函数就像控件的一个属性/成员一样~~,大部分GUI都这样)
QT的connect这个机制设想是
- 解耦合,触发用户操作的控件 和 处理对应用户的操作逻辑 解耦合
- 多对多效果(和数据库非常相似) 学生表 课程表 学生-课程表
以下类似于数据库中关联表的
connect(this,&Widget::mySignal1,this,&Widget::mySlot1);
connect(this,&Widget::mySignal1,this,&Widget::mySlot2);
connect(this,&Widget::mySignal1,this,&Widget::mySlot3);
其实在GUI开发中,多对多是一个伪需求,绝大多数一对一就够用了,新出现的框架,很少再支持多对多的了。
六、信号和槽断开连接
- 使用disconnect来断开信号槽的连接(使用很少,大多数连上了就不用管了),主动断开是把信号绑定在另一个槽函数上
void Widget::handleClick()
{
this->setWindowTitle("修改窗口的标题1");
}
void Widget::handleClick2()
{
this->setWindowTitle("修改窗口的标题2");
}
void Widget::on_pushButton_2_clicked()
{
//1.先断开原来的信号槽
disconnect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleClick);
//2.重新绑定
connect(ui->pushButton,&QPushButton::clicked,this,&Widget::handleClick2);
}
如果没有disconnect,就会出现一个信号绑定了两个槽函数,两个槽函数都会执行。
- 上述结果为点击顺序按钮1->按钮2(切换槽函数)->按钮三的打印结果。
- 也就是说,不disconnect,切换槽函数的作用变成了一个信号对应两个信号槽,handleclick1和handleClick2同时执行
七、 使用lambda表达式定义槽函数
lamda本质上是一个回调函数
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->move(200,200);
connect(button,&QPushButton::clicked,this,[](){
qDebug()<<"lanmda 被执行了";
});
这样简化了信号槽的写法。但是当前参数什么也没干,如果像点击按钮时移动按钮位置,button找不到定义怎么办?
- lanbda表达式本质上是回调函数,这个函数无法直接获取到上层作用域中的变量。解决问题引入变量捕获的语法,通过变量捕获,获取到外层作用域中的变量
connect(button,&QPushButton::clicked,this,[button,this](){//[=]
qDebug()<<"lanmda 被执行了";
button->move(300,300);
this->move(100,100);
});
- 如果当前lanmda里面想使用更多的外层变量,写作
[=]
,这个写法就是把上层作用域中所有变量名捕获进来。 - 槽函数简单而且一次性使用,就会写作lanmbda的形式,回调函数执行时机是不确定的,无论何时用户点击按钮,捕获到的变量都能正确使用
- button是new出来的变量,生命周期挂到整个窗口(对象树上,窗口关闭才会释放),这个东西可以随时使用。(53)
- lanmda除了值的方式捕获[=],还有引用的方式[&],(QT中很少写,捕获的是控件指针,指针变量按值还是引用来传递都无所谓,&方式还要关注本身的周期。因此按值捕获就够了!)
该语法为C++ 11中引入的,QT5以及更高版本,默认C++11编译的,QT4或者更老的版本,需要手动添加C++11的编译选项
CONFIG +=c++11 //入乡随俗
总结
- 信号槽。信号源,信号的类型,信号处理方式
- 信号槽的使用。新版本
- 如何查阅文档(控件内置信号,何时触发,槽的作用【找父类!】)
- 自定义槽函数(普通成员函数)图形化界面也可以自动生成,函数名特定规则完成
- 自定义信号 本质是成员函数(定义自动生成,只用写声明)signals自定义关键字中emit完成发射
- 信号和槽带参数,参数一致(类型,信号个数可以多)
- 信号槽意义,解耦合,多对多
高内聚,低耦合
耦合:两者之间影响很大
内聚:某个功能点集中放在一起
- lambda表达式,简化槽函数定义