Qt :信号与槽

发布于:2024-05-08 ⋅ 阅读:(22) ⋅ 点赞:(0)

信号介绍

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 中的信号三要素,再加上处理信号的控件就很容易记下来了。

下面来举个示例:

实现一个功能按钮,当用户点击这个按钮时,将整个窗口都关闭

  1. 在 Widget 构造函数内部,实例化一个 QPushButton 对象(记得包含头文件!)设置这个按钮的文本,并且移动到特定位置:
    在这里插入图片描述
    实现效果如下:
    在这里插入图片描述
    当然现在点击这个按钮是没有任何反应的,没有编写 connect 函数去处理信号对应的效果。
  2. 调用 conncet 函数,实现点击按钮关闭整个窗口的功能。
    在这里插入图片描述
    实现效果如下:
    在这里插入图片描述

connect 函数传参问题

下面来说一下,connect 函数的两个参数,分别是:

  1. const char* singal;
  2. 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 函数时,是需要对这两个传入的参数搭配两个宏来使用的

  1. 信号参数传参要搭配:SIGNAL
  2. 槽函数传参需要搭配: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 函数来关联信号和槽;
  1. 实例化一个 QPushButton 对象,设置这个按钮的文本,并且移动到特定位置:
    在这里插入图片描述
    在这里插入图片描述
  2. 在QWidget 类中编写 headleClicked 槽函数;调用 connect 函数,将mybutton 对象发出的信号 和 headleClicked 槽函数关联起来。实现对应的功能:
    在这里插入图片描述
    实现效果如下:
    在这里插入图片描述

方法二

  • 通过 ui 文件,直接编写槽函数
  1. 找到 widget.ui 文件,双击 widget.ui 文件,跳转到可视化界面:
    在这里插入图片描述
    在这里插入图片描述
  2. 找到 Buttons 模块下的 Push Button 控件,拖拽到右图的可视化编辑页面,编辑和调整按钮的文本和大小:
    在这里插入图片描述
  3. 鼠标右击刚刚拖拽的按钮控件,选择转到槽函数:
    在这里插入图片描述
    此时,会出现关于这个控件的所有信号选项。这个时候,选择我们需要实现功能的信号即可:
    在这里插入图片描述
  4. 点击 ok 选项后,会直接跳转到槽函数实现上。此时,只需要对槽函数进行编写功能即可:
    在这里插入图片描述
  5. 上述操作都做完后,我们可以直接编译程序,生成我们想要的效果:
    在这里插入图片描述

可以看到,在使用第二种方法是比较方便的,并不需要用户直接去定义槽函数的函数声明 和 函数名,只需要编写对应的功能即可。更甚至可以不用直接去调用 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

下面举个示例:

  1. 在 Widget 类中,定义 mysignal 信号、定义触发 mysignal 信号所实现的槽函数
    在这里插入图片描述
    在这里插入图片描述
  2. 在GUI 界面拖拽一个 PushButton 控件,当按下按钮,按钮控件发射信号调用槽函数,再通过槽函数内部发射 mysignal 信号,再去调用 headleMysignal 槽函数:
    在这里插入图片描述
    由于是 ui 文件拖拽的控件实现的槽函数,因此 pushbutton 不用手动调用 connect 函数。在这里只需要链接widget发射的信号和处理这个信号的 headleMysignal 槽函数即可:
    在这里插入图片描述
    效果如下:
    在这里插入图片描述

定义带参数的信号和槽

定义信号和槽函数时,是可以带参数的。

  • 当信号带有参数的时候,使用槽的参数必须和信号的参数一致

当然,这里指代的一致性,是信号和槽参数类型的一致。当信号和槽函数的参数个数不一致的时候,尽量要保证信号的参数的个数要多于槽函数的参数个数!

用户在发射一个信号时,就是给信号传参的时候。与之对应的的参数就会被传递到槽函数中,这样就达到了让信号给槽函数传参的效果。

举个示例:

  1. 在 Widget 类中定义 mysign 信号和 headleMySignal 槽函数的声明,设置相同的参数类型和参数的个数:
    在这里插入图片描述
    槽函数实现如下:

在这里插入图片描述

  1. 在GUI 界面拖拽一个 PushButton 控件,当按下按钮,按钮控件发射信号调用槽函数,再通过槽函数内部发射 mysignal 信号(对 mysignal 信号传入实参),再去调用 headleMysignal 槽函数:
    在这里插入图片描述
    由于是 ui 文件拖拽的控件实现的槽函数,因此 pushbutton 不用手动调用 connect 函数。在这里只需要链接widget发射的信号和处理这个信号的 headleMysignal 槽函数即可:
    在这里插入图片描述
    实现的效果如下:
    在这里插入图片描述

看到这里的示例,跟前面定义信号的示例没有什么改变,有种多此一举的感觉。

其实并不然,传参可以提到代码复用的效果!如果有多个逻辑,但是总体逻辑整体都一致,只是数据不同。这个时候传参的效果就会展现出来。

下面再来举个例子:以上述例子为扩展,在 ui 界面拖拽两个按钮。当用户点击不同按钮时,更改窗口的标题内容会有所不同。

  1. 在 ui 文件中拖拽两个按钮,分别实现不同的槽函数:
    在这里插入图片描述
  2. 转到按钮一的槽函数,实现的按钮一槽函数的内容:
    在这里插入图片描述
    在这里插入图片描述
  3. 转到按钮二的槽函数,实现的按钮二槽函数的内容:
    在这里插入图片描述
    在这里插入图片描述
    下面来看实现效果:
    在这里插入图片描述

至此,通过这一套信号槽,搭配不同的参数,就可以设置不同标题的效果。

参数个数不一致问题

前面提到,信号和槽函数的参数类型必要保持一致。这个可以理解,信号的参数要传递给槽,类型不一致编译会直接报错。

至于参数的个数问题就要来探讨一下:

下面,我们拿前面的例子来更改一下代码。将信号参数个数变多,槽函数的参数不变( N :1 )来编译一下代码:

在这里插入图片描述
在这里插入图片描述

编译结果如下:
在这里插入图片描述

下面换一种方式,将信号参数保持不变,槽函数的参数个数设置多个(1:N):

在这里插入图片描述
设置第二个参数,没有去使用:
在这里插入图片描述

在这里插入图片描述

编译结果如下:
在这里插入图片描述

总结:

  • 信号的参数个数超过槽函数的参数个数,代码还是可以被编译通过的
  • 信号的参数个数少于槽函数的参数个数,编译直接报错

为什么信号的参数个数就可以多,槽函数参数只允许与信号相同,甚至是少呢?

这是因为一个槽函数可能被多个信号绑定。信号的参数不确定,但是能保证最少的参数的信号和槽函数的参数是可以对应的。当信号和槽函数的参数不匹配时,槽函数在获取参数时,会按照信号参数的顺序,从左往右依次获取,直到槽函数的最后一个参数都能被获取到对应的值!前提是,信号的参数个数多于槽函数参数的个数。

如果,槽函数的参数个数都多于信号的话,就不能保证槽函数的参数都能获取到对应的值。这也是为什么,信号的参数个数可以比槽函数的参数多,反过来就不行。

在这里还要注意一个点:
在 QT 中,如果想要某个类能够使用信号和槽(在类中定义信号和槽函数),在类的最开始部分就要包含一个宏:Q_OBJECT

这个宏展开会生成很多属于 Qt 内部的代码

在这里插入图片描述

如果类中没有加上 Q_OBJECT 这个宏,在使用信号和槽时,会报错!

断开信号和槽的连接 disconnect

disconnect 用法和 connect 类似。由于在Qt中,信号和槽的关系是多对多的。一旦一个信号绑定上一个槽函数后,每次触发这个信号都会调用这个槽函数。如果不去断开连接,下次再用这个信号去绑定其他的槽函数时,触发这个信号,就会调用两个槽函数。

lambda 表达式

lambda 本质是一个匿名函数,主要运用在 回调函数 中。lambda 表达式通常用来创建临时的槽函数。

语法:

[]()
{
	//...
}

匿名函数的生命周期很短,创建使用后就被销毁

与 java 语言不同。在C++中,lambda 表达式是无法直接获取上层作用域中的变量的。为了解决作用域的问题,C++ 引入了 变量捕获 的语法,就是 lambda 表达式中想要用到哪些变量直接引用到 中括号 即可。

下面来举个例子:

创建一个按钮控件,当用户点击按钮时,更改按钮的位置和窗口标题内容。利用 lambda 表达式实现:

  1. 创建按钮控件,设置控件对应的位置:
    在这里插入图片描述
  1. 将 lambda 表达式代替为槽函数,由于要更改按钮控件的位置 和 窗口标题的内容,因此需要将 button 变量 和 this 传给lambda表达式:
    在这里插入图片描述
  2. 实现效果如下:
    在这里插入图片描述

上面传参的方式可以得到很好的解决,但是,当参数很多的时候就会变得很麻烦。

下面来介绍第二种传变量的方式:

[=]() //等号的意义:将上层作用域的所有变量都传给lambda表达式
{
	//...
}    

将上面代码稍作修改:在这里插入图片描述
实现效果如下:
在这里插入图片描述

看到这里就有小伙伴说了,什么时候用 lambda 表达式呢?

当槽函数比较简单且是一次性使用时,我们就可以将槽函数写成 lambda表达式

使用 lambda 表达式需要注意的一点就是变量的生命周期,一般创建控件都是 new 出来的,也就是堆区开辟。但是,不妨有一些控件在使用前就被销毁,因此,在传递变量给lambda表达式时,需要注意变量的生命周期!!!

lambda 表达式是 C++11 标准提出来的,如果 QT 版本低于 QT5 在使用 lambda表达式时会直接编译报错。

如果遇到使用 lambda 表达式出错的,可以在 .pro 文件中 添加这么一句代码 :CONFIG += c++11,就可以解决报错问题。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到