信号和槽本质上是通过connect函数来连接实现的。但是从写法或者操作上来说,有多种方式,以下总结了5种方式:

  • SIGNAL/SLOT(Qt4)
  • 函数地址(Qt5)
  • UI 设计师界面 - 转到槽
  • UI 设计师界面 - 信号槽编辑器
  • Lambda 表达式

大家可根据自己的喜好自行选择。

1. 效果演示

接下来通过一个案例,来演示这5种使用方法:
qt-base

2. 新建项目、界面布局

2.1 新建项目

新建一个名为HowToConnectSignalAndSlot的项目
qt-base

2.2 界面布局

双击mywidget.ui文件,切换到Design模式,按照如下方法,设计好界面:

  • 修改窗口标题为 “信号槽的多种连接方式”
  • 拖拽5QPushButton2个垂直弹簧
  • 修改按钮的objectNamebtnMaxbtnNormalbtnMinbtnClosebtnTitle
  • 布局窗口:垂直布局,以使得布局自适应窗口大小的变化
  • 修改按钮显示的文字
  • 设置按钮的样式表(如下):文本对齐方式、字体大小15、内部间距padding

经过以上几步,就完成了界面的布局,如下:
qt-base

2.3 修改样式

Qt样式表(Qt Style Sheets,简称QSS)是Qt框架提供的一种基于CSS(层叠样式表)的样式定制机制。
它允许开发者使用类似于Web开发中的CSS语法来定义Qt应用程序中各个控件的外观和感觉。

基本语法:

选择器 {
    属性1: 值1;
    属性2: 值2;
    属性3: 值3;
}

以上,选中最外层的MyWidget,然后在右侧的属性窗口中,设置styleSheet,如下:

QPushButton {
    text-align: left;
    font-size: 20px;
    padding: 10px;
}

当然了,也可以选中5个按钮,然后将按钮的styleSheet设置为以上。
接下来就开始实现5种连接方式。

3. SIGNAL/SLOT(Qt4)

QT4中,连接信号槽的方法如下:

connect(发送者对象, SIGNAL(信号名称(参数类型)), 接收者对象, SLOT(槽函数名称(参数类型)));

其中,SIGNAL/SLOT是两个宏,它们会将函数名以及对应的参数,转换为字符串。

这种方式,编译器不会做错误检查,即使函数名或者参数写错了,也可以编译通过,这样就把问题留在了运行阶段。
而我们编程开发的一个原则是尽可能早地发现并规避问题,因此这种方式不被推荐。

这是Qt4中的使用方式,目前在Qt6中还是支持的,但不排除以后会被废弃掉!

3.1 实现窗口最大化

下面通过这种方式,实现点击按钮,最大化窗口
mywidget.cpp的构造函数中,使用如下方式连接信号和槽:

MyWidget::MyWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MyWidget) {
    ui->setupUi(this);

    // 1.使用 SIGNAL/SLOT 的方式连接信号和槽
    connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));
}

此时,运行程序,点击按钮就可以最大化窗口了!

3.2 编译不检查

以上提到,如果函数的名字写错了,在编译时不会报错,比如将showMaximized不小心写成了showMaximize
点击【Build】菜单->【Rebuild】,在【Compile Output】窗口并不会报错

而在运行时,在【Application Output】窗口会看到报错,如下:

15:36:28: Starting F:\qt_project\HowToConnectSignalAndSlot\build\Desktop_Qt_6_10_2_MinGW_64_bit-Debug\HowToConnectSignalAndSlot.exe...
qt.core.qobject.connect: QObject::connect: No such slot MyWidget::showMaximize() in F:/qt_project/HowToConnectSignalAndSlot/mywidget.cpp:11
qt.core.qobject.connect: QObject::connect:  (sender name:   'btnMax')
qt.core.qobject.connect: QObject::connect:  (receiver name: 'MyWidget')

4. 函数地址(Qt5)

Qt5开始引入了编译期检查的新语法,信号和槽都使用函数的地址,相比Qt4的基于字符串的语法更安全、更直观。
基本语法:

connect(sender, &Sender::signal, receiver, &Receiver::slot);

其中:

  • sender:信号发送者
  • &Sender::signal:发送的信号
  • receiver:信号接收者
  • &Receiver::slot:槽函数

这种方式,编译时就会对函数类型,参数个数做检查。

4.1 实现窗口正常显示

下面通过这种方式,实现点击按钮,正常化显示窗口
mywidget.cpp的构造函数中,使用如下方式连接信号和槽:

MyWidget::MyWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MyWidget) {
    ui->setupUi(this);

    // ...

    // 2.使用函数地址的方式连接信号和槽
    connect(ui->btnNormal, &QPushButton::clicked, this, &QWidget::showNormal);
}

此时,运行程序,点击按钮就可以正常化显示窗口了!

4.2 编译检查

如果函数的名字写错了,在编译时就会报错,比如将showNormal不小心写成了showNorma,少写一个字母L
此时,点击【Build】菜单->【Rebuild】,在【Compile Output】窗口会看到报错信息:

F:/qt_project/HowToConnectSignalAndSlot/mywidget.cpp: In constructor 'MyWidget::MyWidget(QWidget*)':
F:/qt_project/HowToConnectSignalAndSlot/mywidget.cpp:14:67: error: 'showNorma' is not a member of 'QWidget'
   14 |     connect(ui->btnNormal, &QPushButton::clicked, this, &QWidget::showNorma);
      |                                                                   ^~~~~~~~~

可见,使用这种方式,错误在编译阶段就会暴露出来,及早解决掉它,这也是Qt推荐的一种连接方式!

问题:使用这种方式,由于无法指定函数的参数,因此如果有重载的信号和槽时,编译器会产生二义性。
解决方法:使用QOverload。这在上一节已经做了详细说明。

5. UI设计师界面-转到槽

在设计师界面,不写代码而是通过鼠标点击,来实现点击btnMin按钮,最小化显示窗口

在UI设计师界面,右键单击btnMin,然后选择【Go to slot…】,弹出如下窗口:
qt-base

选择clicked(),即可在mywidget.hmywidget.cpp中声明并实现对应的槽函数,如下:

// mywidget.h
class MyWidget : public QWidget
{
private slots:
    void on_btnMin_clicked();
};

// mywidget.cpp
void MyWidget::on_btnMin_clicked()
{
    this->showMinimized();
}

说明:按钮的名字和自动生成的槽函数,对应关系为:

  • 按钮的名字:btnMin
  • 槽函数的名字为:on_btnMin_clicked

注意:如果修改了按钮的objectName,那么槽函数的名字也要随之修改。

6. UI设计师界面-信号槽编辑器

在设计师界面,不写代码而是通过鼠标点击,来实现点击btnClose按钮,关闭窗口

进入到UI设计师界面,【View】菜单 ->【Views】->【Signals & Slots Editor】,在打开的信号槽编辑器中,点击绿色的加号+
就可以连接信号和槽了:
qt-base

此时,代码文件并没有修改,而是修改了mywidget.ui文件,如下:

<ui>
 <connections>
  <connection>
   <sender>btnClose</sender>
   <signal>clicked()</signal>
   <receiver>MyWidget</receiver>
   <slot>close()</slot>
   <!-- ... -->
  </connection>
 </connections>
</ui>

我们在1.4小节时详细讲解过ui文件如何转换为编译器可以编译的.h/.cpp文件
这个mywidget.ui文件,最终会被转换为ui_mywidget.h文件
没有看之前的视频的小伙伴,可以去观看 1.4【程序员的自我修养】剖析项目构建流程

打开ui_mywidget.h文件,就可以看到,转换后还是通过connect来连接的信号和槽,如下:
qt-base

此时,运行程序,点击btnClose按钮,就可以关闭窗口了!

7. Lambda 表达式

槽函数还可以直接写成Lambda表达式的形式

Lambda表达式在现代语言中,比如C++javapythonkotlinJavaScriptC#中都是支持的,它使得代码更加简洁优雅。

C++11引入了Lambda表达式,用于定义并创建匿名的函数对象
Qt是基于C++的一个GUI框架,它完全支持C++的语法,因此在Qt中也是可以使用Lambda表达式的
如果你对Lambda表达式不了解,也不用担心,这里我们先复习一下C++Lambda表达式的用法

7.1 Lambda 基本语法

C++ 中的 Lambda 表达式,其实就是匿名函数,语法如下:

[capture](parameters) option -> return-type { body }

其中包含 5 个部分:

  • capture

捕获列表,它总是出现在Lambda表达式的开始
实际上,[]Lambda引出符,编译器根据该引出符判断接下来的代码是否是Lambda表达式
捕捉列表能够捕获上下文中的变量,以在 Lambda 表达式内使用,主要有如下几种情况:

[]  // 不捕获任何变量

[&] // 按引用捕获外部作用域中所有变量
[=] // 按值捕获外部作用域中所有变量。按值捕获的变量,在 Lambda 表达式内是只读的,不能修改赋值

[x, y]      // 按值捕获 x 和 y
[&x, &y]    // 按引用捕获 x 和 y

[=, &x] // 按值捕获外部作用域中所有变量,但x按引用捕获

// 捕获当前对象的 this 指针
// 捕获了 this 就可以在 Lambda 中使用当前类的成员变量和成员函数
// 如果已经使用了 & 或者 =,就默认添加此选项
[this]
  • parameters

参数列表,可选

  • option

函数选项,可选

  • return-type

返回值类型,可选。没有返回值的时候也可以连同符号->一起省略

  • body

函数体

7.2 Lambda 演练

接下来,在mywidget.cpp的构造函数中,逐一演示这5个部分的使用

MyWidget::MyWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MyWidget) {

    // ...
        
    // a. 演示lambda表达式

    // a.1 匿名函数的定义
#if 0
    []() {
        qDebug() << "lambda...";
    };
#endif

    // a.2 匿名函数的调用
#if 0
    []() {
        qDebug() << "lambda...";
    }();
#endif

    int a = 10;

    // a.3 不捕获任何变量
    // Variable 'a' cannot be implicitly captured in a lambda with no capture-default specified
#if 0
    []() {
        qDebug() << a;
    }();
#endif

    // a.4 按引用捕获
#if 0
    [&]() {
        qDebug() << a++;    // 10
    }();
    qDebug() << a;          // 11
#endif

    // a.5 按值捕获
    // 按值捕获的变量,在 lambda 表达式内是只读的,不能修改赋值
#if 0
    [=]() {
        // Cannot assign to a variable captured by copy in a non-mutable lambda
        qDebug() << a++;
    }();
#endif

    // a.6 按值捕获 + mutalbe 选项
    // 添加 mutable 选项,就可以在 lambda 内修改捕获的变量了
    // 并且=这种方式,是按值传递的,里面的修改,不会影响外边。
#if 0
    [=]() mutable {
        qDebug() << a++;    // 10
    }();
    qDebug() << a;          // 10
#endif

    // a.7 参数
#if 0
    [](int x, int y) {
        qDebug() << x + y;    // 3
    }(1, 2);
#endif

    // a.8 返回值
    // 返回值可以省略,编译器会自动推断 lambda 表达式的返回值类型
    // 返回值省略时,也可以连同符号`->`一起省略
#if 0
    int sum = [](int x, int y) -> int {
        return x + y;
    }(1, 2);
    qDebug() << sum;    // 3
#endif

#if 0
    int sum = [](int x, int y) {
        return x + y;
    }(1, 2);
    qDebug() << sum;    // 3
#endif
}

7.3 槽函数使用 Lambda

有了以上Lambda表达式的基本知识,将它作为槽函数,就水到渠成了,如下:

#include <QDateTime>

MyWidget::MyWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MyWidget) {

    // ...

    // 5. 使用lambda 表达式做槽函数
    connect(ui->btnTitle, &QPushButton::clicked, this, [this]() {
        QString title = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        this->setWindowTitle(title);
    });
}

此时,运行程序,点击btnTitle按钮,就可以修改窗口的标题了!

4. 点赞、获取源码

看到这里的小伙伴,去B站给明王一个【免费的点赞】吧,你的支持,是我持续更新优质内容的动力,感谢~

源码下载地址
链接: https://pan.baidu.com/s/18HWV6Q3qG8BQCrWDHcEvMA
提取码: ming