信号与槽
信号介绍
Qt 中,信号会涉及三个要素:
-
信号源:每一个控件,都会独属于自己的信号,这个信号源通常由控件发出
-
信号的类型:用户进行不同的操作,就会触发不同的信号
例如:点击一个按钮,会触发点击的信号;在输入框中移动光标,就会触发光标移动的信号;选择下拉框,也会触发不同的信号。 -
信号的处理方式:槽(slot),槽是一个回调函数,用于处理控件发出的信号
connect 函数使用
Qt 中,一般使用 connect
函数,把信号和槽关联起来。当使用了这个函数,信号被触发后会自动去调用执行槽函数。
前提是,槽函数要先实现了才能处理控件发出的信号。顺序不能颠倒,单纯有信号没有信号的处理方式是办不了事的!
学 Linux 老铁,不要将这里提到的
connect
函数和 Linux 的 TCP socket 中 connect 建立链接的函数弄混淆了。它们两个之间没有联系,没有联系,没有联系!只是名字恰好相同而已。
Qt 中 connect
函数 是 QObject类提供的一个静态成员函数。
Qt 中很多的类,都是存在一定的继承关系
例如,我们悉知的 Qwidget 这个类的父类就是 QObject。而 Qwidget 类又是诸多控件的父类,QPushButton、QLineEdit等等都是 Qwidget 的子类。可以说 QObject 就是QT中内置类中的祖宗类!因为这些内置类都会间接去继承 QOject 类。
因此,在任何类中都可以直接使用 connect
这个函数!
语法:
connect(const QObject* sender,
const char* signal,
const QObject* receiver,
const char* method,
Qt::ConnectionType type = Qt::AutoConnection )
看到这个函数的参数,我相信会劝退很多人。其实不然,这个函数没有想象的很难使用。
最常用的只是前 4个参数,最后一个参数很少用到。
- sender 参数:传入的信号是哪个控件发出来的
- signal 参数:传入这个信号的类型(点击、键盘输入、拖动鼠标… ),信号也是控件的成员
- receiver 参数:传入负责处理信号的控件
- method 参数:传入负责处理信号的控件的内部实现的成员函数
使用 connect 函数的时候,联想一下 Qt 中的信号三要素,再加上处理信号的控件就很容易记下来了。
下面来举个示例:
实现一个功能按钮,当用户点击这个按钮时,将整个窗口都关闭
- 在 Widget 构造函数内部,实例化一个 QPushButton 对象(记得包含头文件!)设置这个按钮的文本,并且移动到特定位置:
实现效果如下:
当然现在点击这个按钮是没有任何反应的,没有编写 connect 函数去处理信号对应的效果。- 调用 conncet 函数,实现点击按钮关闭整个窗口的功能。
实现效果如下:
connect 函数传参问题
下面来说一下,connect
函数的两个参数,分别是:
- const char* singal;
- const char* method;
上面代码中,使用这两个参数时,我们是这样传参的:
connect(mybutton, &QPushButton::clicked, this, &Widget::close);
传参时,针对函数取地址得到的是一个函数指针! 但是,connect
函数参数所能接收的是 char*
类型的指针。
在 C/C++ 中,指针也是分类型的。例如:int*
、 char*
、 float*
… 在这里对 QPushButton::clicked
和 对Widget::close
函数取地址,应该分别用 void (*)()
类型指针 和 bool (*)()
类型指针来接收。
上面提到的 connect
函数,这不指针类型不匹配吗?在C++中,是不允许使用两个不同类型指针相互赋值的!但是,Qt 中却没有报错。
其实上面提到的 connect
函数的声明是很老旧版本的,没想到吧。
以前在使用老版本的 connect 函数时,是需要对这两个传入的参数搭配两个宏来使用的!
- 给信号参数传参要搭配:
SIGNAL
宏- 给槽函数传参需要搭配:
SLOT
宏
拿上面代码举例:
connect(mybutton, SIGNAL( &QPushButton::clicked), this, SLOT(&Widget::close));
使用这两个宏,会将取到的函数指针类型转换成 char*
类型的指针
上面 connect
函数写法在 Qt 5版本后就不再使用了。本身 connect
函数的参数又多,在加上这两个宏,实在是太繁琐。
Qt 5 版本后,给 connect
函数提供了一个重载版本。针对第二个参数和第四个参数提供了泛型参数,允许传入任何的指针类型。
重载版本的 connect
函数声明如下:
template<typename Func1, typename Func2> //Func1 和 Func2 是泛型参数
static inline QMetaObject::Connection connect(
const typename QtPrivate::FuncitionPointer<Funcl>::Object* sender,
Func1 signal,
const typename QtPrivate::FunctionPointer<Func2>:: Object* receiver,
Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection)
)
使用了新版的 connect
函数重载后,connect
函数就带有了一定的 参数检查 功能。
如果传入的第一个参数和第二个参数不匹配(这里的不匹配是指:参数二的指针不是参数一的成员函数)第三个参数和第四个参数不匹配,此时就会编译出错!
定义槽(solt)函数
定义槽函数在开发中是非常重要的,所谓的槽函数其实是一个普通的函数,和在类中定义一个成员函数没有多大的区别,槽函数主要用于处理接收信号。当用户触发到某个操作时,要进行的业务逻辑。
自定义槽函数的方式有两种。
下面举个例子,实现一个按钮控件,当按下后更改窗口的标题:
方法一
- 实例化一个控件对象,手动在 Widget 类中定义槽函数,通过调用 connect 函数来关联信号和槽;
- 实例化一个 QPushButton 对象,设置这个按钮的文本,并且移动到特定位置:
- 在QWidget 类中编写 headleClicked 槽函数;调用 connect 函数,将mybutton 对象发出的信号 和 headleClicked 槽函数关联起来。实现对应的功能:
实现效果如下:
方法二
- 通过 ui 文件,直接编写槽函数
- 找到 widget.ui 文件,双击 widget.ui 文件,跳转到可视化界面:
- 找到 Buttons 模块下的 Push Button 控件,拖拽到右图的可视化编辑页面,编辑和调整按钮的文本和大小:
- 鼠标右击刚刚拖拽的按钮控件,选择转到槽函数:
此时,会出现关于这个控件的所有信号选项。这个时候,选择我们需要实现功能的信号即可:
- 点击 ok 选项后,会直接跳转到槽函数实现上。此时,只需要对槽函数进行编写功能即可:
- 上述操作都做完后,我们可以直接编译程序,生成我们想要的效果:
可以看到,在使用第二种方法是比较方便的,并不需要用户直接去定义槽函数的函数声明 和 函数名,只需要编写对应的功能即可。更甚至可以不用直接去调用 connect 函数来关联 信号 和 槽函数。
问题来了,没有调用 connect 函数,那么信号和槽是怎么关联的?
此时,就要谈一下方法二直接形成的槽函数的名字了:
void Widget::on_pushButton_clicked();
这个槽函数的名字是系统直接默认生成的!仔细观察这个函数名字会发现一些规律。
on_pushButton_clicked
由以下几部分组成:
on
前缀、pushButton
是按钮控件的 objectName、clicked
是这个按钮控件的信号
当槽函数名称都符合这样的排序规则,Qt 就会自动将信号和槽函数给建立联系。
在
ui_widget.h
文件中的setupUi
成员函数内部会调用connectSlotsByName
这个函数,这个函数就会根据上面提到的槽函数命名规则,自动去关联对应控件的信号
但是,如果没有按照对应要求去实现对应的名称,Qt 就关联不上对应的信号和槽。一般让编译器实现就行,并不需要我们刻意去关心。
- 如果通过图形界面创建控件,那么推荐使用第二种方法来实现槽函数,从而达到快速链接信号和槽
- 如果使用代码的方式创建控件,那么就要像方法一那般实现槽函数后,再调用 connect 函数链接信号和槽
定义信号
Qt 中是允许自定义信号的,信号对应的是用户的操作。
信号是一个特殊的函数。与普通函数和槽函数不同,我们只需要写出函数声明,并且告诉编译器这是一个信号即可!
编译过中编译器会自行生成信号的定义,我们不做干预,也干预不了。
定义信号需要注意以下几点:
1. 信号是个特殊的函数,这个函数不需要手动定义内容,编译器会自动完成
2. 信号不需要返回值,直接设置为 void
3. 信号也没有参数都可以,也支持函数重载
关键字 signals、emit
定义一个信号需要用到一个 Qt 内置的关键字:signals
。这个关键字不是 C++ 标准,是Qt自己扩展出来的。
当编译器进行编译的时候,如果扫描到 signals
关键字,就会将关键字下面的函数声明识别为信号,并且对这些函数自动生成函数定义。
与 Qt 内置信号不同的是,我们自定义的信号需要被触发的才能发送信号。触发信号需要用到关键字:emit
下面举个示例:
- 在 Widget 类中,定义 mysignal 信号、定义触发 mysignal 信号所实现的槽函数
- 在GUI 界面拖拽一个 PushButton 控件,当按下按钮,按钮控件发射信号调用槽函数,再通过槽函数内部发射 mysignal 信号,再去调用 headleMysignal 槽函数:
由于是 ui 文件拖拽的控件实现的槽函数,因此 pushbutton 不用手动调用 connect 函数。在这里只需要链接widget发射的信号和处理这个信号的 headleMysignal 槽函数即可:
效果如下:
定义带参数的信号和槽
定义信号和槽函数时,是可以带参数的。
- 当信号带有参数的时候,使用槽的参数必须和信号的参数一致
当然,这里指代的一致性,是信号和槽参数类型的一致。当信号和槽函数的参数个数不一致的时候,尽量要保证信号的参数的个数要多于槽函数的参数个数!
用户在发射一个信号时,就是给信号传参的时候。与之对应的的参数就会被传递到槽函数中,这样就达到了让信号给槽函数传参的效果。
举个示例:
- 在 Widget 类中定义
mysign
信号和headleMySignal
槽函数的声明,设置相同的参数类型和参数的个数:
槽函数实现如下:
- 在GUI 界面拖拽一个 PushButton 控件,当按下按钮,按钮控件发射信号调用槽函数,再通过槽函数内部发射 mysignal 信号(对 mysignal 信号传入实参),再去调用 headleMysignal 槽函数:
由于是 ui 文件拖拽的控件实现的槽函数,因此 pushbutton 不用手动调用 connect 函数。在这里只需要链接widget发射的信号和处理这个信号的 headleMysignal 槽函数即可:
实现的效果如下:
看到这里的示例,跟前面定义信号的示例没有什么改变,有种多此一举的感觉。
其实并不然,传参可以提到代码复用的效果!如果有多个逻辑,但是总体逻辑整体都一致,只是数据不同。这个时候传参的效果就会展现出来。
下面再来举个例子:以上述例子为扩展,在 ui 界面拖拽两个按钮。当用户点击不同按钮时,更改窗口的标题内容会有所不同。
- 在 ui 文件中拖拽两个按钮,分别实现不同的槽函数:
- 转到按钮一的槽函数,实现的按钮一槽函数的内容:
- 转到按钮二的槽函数,实现的按钮二槽函数的内容:
下面来看实现效果:
至此,通过这一套信号槽,搭配不同的参数,就可以设置不同标题的效果。
参数个数不一致问题
前面提到,信号和槽函数的参数类型必要保持一致。这个可以理解,信号的参数要传递给槽,类型不一致编译会直接报错。
至于参数的个数问题就要来探讨一下:
下面,我们拿前面的例子来更改一下代码。将信号参数个数变多,槽函数的参数不变( N :1 )来编译一下代码:
编译结果如下:
下面换一种方式,将信号参数保持不变,槽函数的参数个数设置多个(1:N):
设置第二个参数,没有去使用:
编译结果如下:
总结:
- 信号的参数个数超过槽函数的参数个数,代码还是可以被编译通过的
- 信号的参数个数少于槽函数的参数个数,编译直接报错
为什么信号的参数个数就可以多,槽函数参数只允许与信号相同,甚至是少呢?
这是因为一个槽函数可能被多个信号绑定。信号的参数不确定,但是能保证最少的参数的信号和槽函数的参数是可以对应的。当信号和槽函数的参数不匹配时,槽函数在获取参数时,会按照信号参数的顺序,从左往右依次获取,直到槽函数的最后一个参数都能被获取到对应的值!前提是,信号的参数个数多于槽函数参数的个数。
如果,槽函数的参数个数都多于信号的话,就不能保证槽函数的参数都能获取到对应的值。这也是为什么,信号的参数个数可以比槽函数的参数多,反过来就不行。
在这里还要注意一个点:
在 QT 中,如果想要某个类能够使用信号和槽(在类中定义信号和槽函数),在类的最开始部分就要包含一个宏:Q_OBJECT
这个宏展开会生成很多属于 Qt 内部的代码
如果类中没有加上 Q_OBJECT 这个宏,在使用信号和槽时,会报错!
断开信号和槽的连接 disconnect
disconnect 用法和 connect 类似。由于在Qt中,信号和槽的关系是多对多的。一旦一个信号绑定上一个槽函数后,每次触发这个信号都会调用这个槽函数。如果不去断开连接,下次再用这个信号去绑定其他的槽函数时,触发这个信号,就会调用两个槽函数。
lambda 表达式
lambda 本质是一个匿名函数,主要运用在 回调函数 中。lambda 表达式通常用来创建临时的槽函数。
语法:
[]()
{
//...
}
匿名函数的生命周期很短,创建使用后就被销毁
与 java 语言不同。在C++中,lambda 表达式是无法直接获取上层作用域中的变量的。为了解决作用域的问题,C++ 引入了 变量捕获 的语法,就是 lambda 表达式中想要用到哪些变量直接引用到 中括号 即可。
下面来举个例子:
创建一个按钮控件,当用户点击按钮时,更改按钮的位置和窗口标题内容。利用 lambda 表达式实现:
- 创建按钮控件,设置控件对应的位置:
- 将 lambda 表达式代替为槽函数,由于要更改按钮控件的位置 和 窗口标题的内容,因此需要将 button 变量 和 this 传给lambda表达式:
- 实现效果如下:
上面传参的方式可以得到很好的解决,但是,当参数很多的时候就会变得很麻烦。
下面来介绍第二种传变量的方式:
[=]() //等号的意义:将上层作用域的所有变量都传给lambda表达式
{
//...
}
将上面代码稍作修改:
实现效果如下:
看到这里就有小伙伴说了,什么时候用 lambda 表达式呢?
当槽函数比较简单且是一次性使用时,我们就可以将槽函数写成 lambda表达式
使用 lambda 表达式需要注意的一点就是变量的生命周期,一般创建控件都是 new 出来的,也就是堆区开辟。但是,不妨有一些控件在使用前就被销毁,因此,在传递变量给lambda表达式时,需要注意变量的生命周期!!!
lambda 表达式是 C++11 标准提出来的,如果 QT 版本低于 QT5 在使用 lambda表达式时会直接编译报错。
如果遇到使用 lambda 表达式出错的,可以在 .pro
文件中 添加这么一句代码 :CONFIG += c++11
,就可以解决报错问题。