第六章 QT基础-QT内容补充:1、Qt中的信号槽

1. 信号和槽概述

信号槽是 Qt 框架引以为豪的机制之一,实际体现的是观察者模式(发布-订阅模式)。当某个事件发生后(如按钮被点击),它会发出一个信号(signal),任何接收方对象只要使用 connect() 将其槽函数绑定上,就可以自动触发槽(slot)函数进行处理。

就像广播一样,信号发出后不指定接收者,而是由接收者决定要不要处理这个信号。


1.1 信号的本质

信号是由于用户对窗口或控件进行了某些操作,导致窗口或控件产生了某个特定事件,这时候Qt对应的窗口类会发出某个信号,以此对用户的挑选做出反应。

信号 = 事件的通知机制。常见事件如:

  • 按钮单击、双击

  • 鼠标移动、按下、释放

  • 键盘输入

  • 窗口刷新

那么在Qt中信号是通过什么形式呈现给使用者的呢?

  • 信号的呈现形式就是函数, 也就是说某个事件产生了, Qt框架就会调用某个对应的信号函数, 通知使用者。
  • 在QT中信号的发出者是某个实例化的类对象对象内部可以进行相关事件的检测。

信号的本质是函数(只有声明没有定义),由某个实例化对象在检测到事件后自动调用,通知接收者。


1.2 槽的本质

槽函数 = 对信号的响应处理,是类的普通成员函数或静态函数。可以通过 connect() 和信号连接。

[!NOTE]
在Qt中槽函数是一类特殊的功能的函数在编码过程中也可以作为类的普通成员函数来使用。之所以称之为槽函数是因为它们还有一个职责就是对Qt框架中产生的信号进行处理。

示例类比:

实例对象 角色 描述
女朋友 信号发出者 说“我饿了!”
信号接收者 听到信号 → 处理:带她去吃饭
在Qt中槽函数的所有者也是某个类的实例对象。

1.3 信号和槽的关系

在Qt中信号和槽函数都是独立的个体,本身没有任何联系,但是由于某种特性需求我们可以将二者连接到一起:
信号和槽是独立的个体,需要通过 QObject::connect() 函数建立连接。

QMetaObject::Connection QObject::connect(
    	const QObject *sender, PointerToMemberFunction signal, 
        const QObject *receiver, PointerToMemberFunction method, 
		Qt::ConnectionType type = Qt::AutoConnection);
参数:
  - sender:   发出信号的对象
  - signal:   属于sender对象, 信号是一个函数, 这个参数的类型是函数
              指针, 信号函数地址
  - receiver: 信号接收者
  - method:   属于receiver对象, 当检测到sender发出了signal信号, 
              receiver对象调用method方法,信号发出之后的处理动作
 
//  参数 signal 和 method 都是函数地址, 因此简化之后的 connect() 如下:
connect(const QObject *sender, &QObject::signal, 
        const QObject *receiver, &QObject::method);

⚠️ 注意事项:

  • connect() 注册的是事件处理动作,并不立即触发。

  • 信号发出后 Qt 框架自动调用对应的槽函数。

  • sender 和 receiver 必须已实例化,否则连接无效。


2. 标准信号槽使用

2.1 标准信号/槽的查找方式

Qt 的标准控件(如按钮、窗口等)都包含内置信号和槽函数,例如:

  • QPushButton 单击信号其实定义在其父类 QAbstractButton 中。

查找方式:

  • 在帮助文档中搜索类名 → 查看 signalsslots
    ![[Pasted image 20250424143913.png]]

2.2 使用案例

功能:点击按钮 → 关闭窗口

元素 角色 类型
按钮 信号源 QPushButton
窗口 信号接收 QWidget
功能实现: 点击窗口上的按钮, 关闭窗口
功能分析:
	- 按钮: 信号发出者          -> QPushButton 类型
	- 窗口: 信号的接收者和处理者  -> QWidget 类型
使用的信号槽函数:
// 单击按钮发出的信号
[signal] void QAbstractButton::clicked(bool checked = false)
// 关闭窗口的槽函数
[slot] bool QWidget::close();
一句代码实现功能:
connect(ui->closewindow, &QPushButton::clicked, this, &MainWindow::close);

通常在构造函数中调用 connect(),事件注册先行,事件触发时再执行处理。


3. 自定义信号槽使用

Qt框架提供的信号槽在某些特定场景下是无法满足我们的项目需求的,因此我们还设计自己需要的的信号和槽,同样还是使用connect()对自定义的信号槽进行连接。

如果想要在QT类中自定义信号槽, 需要满足一些条件, 并且有些事项也需要注意:

  • 要编写新的类并且让其继承Qt的某些标准类

  • 这个新的子类必须从QObject类或者是QObject子类进行派生

  • 在定义类的头文件中加入 Q_OBJECT 宏

// 在头文件派生类的时候,首先像下面那样引入Q_OBJECT宏:
class MyMainWindow : public QWidget
{
    Q_OBJECT
    ......
}

3.1 自定义信号

在Qt中信号的本质是事件, 但是在框架中也是以函数的形式存在的, 只不过信号对应的函数只有声明, 没有定义。如果Qt中的标准信号不能满足我们的需求,可以在程序中进行信号的自定义,当自定义信号对应的事件产生之后,认为的将这个信号发射出去即可(其实就是调用一下这个信号函数)。

[!important]
下边给大家阐述一下, 自定义信号的要求和注意事项:

  • 信号是类的成员函数

  • 返回值必须是 void 类型

  • 信号的名字可以根据实际情况进行指定

  • 参数可以随意指定, 信号也支持重载

  • 信号需要使用 signals 关键字进行声明, 使用方法类似于public等关键字

  • 信号函数只需要声明, 不需要定义(没有函数体实现)

  • 在程序中发射自定义信号: 发送信号的本质就是调用信号函数

    • 习惯性在信号函数前加关键字: emit, 但是可以省略不写

    • emit只是显示的声明一下信号要被发射了, 没有特殊含义

    • 底层 emit == #define emit

// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class Test : public QObject
{
    Q_OBJECT
signals:
    void testsignal();
    // 参数的作用是数据传递, 谁调用信号函数谁就指定实参
    // 实参最终会被传递给槽函数
    void testsignal(int a);
};

3.2 自定义槽

槽函数就是信号的处理动作,在Qt中槽函数可以作为普通的成员函数来使用。如果标准槽函数提供的功能满足不了需求,可以自己定义槽函数进行某些特殊功能的实现。自定义槽函数和自定义的普通函数写法是一样的。

[!important]
下边给大家阐述一下, 自定义槽的要求和注意事项:

  • 返回值必须是 void 类型
  • 槽也是函数, 因此也支持重载
  • 槽函数需要指定多少个参数, 需要看连接的信号的参数个数
  • 槽函数的参数是用来接收信号传递的数据的, 信号传递的数据就是信号的参数
举例:

信号函数: void testsig(int a, double b);
槽函数: void testslot(int a, double b);

总结:

  • 槽函数的参数应该和对应的信号的参数个数, 从左到右类型依次对应
  • 信号的参数可以大于等于槽函数的参数个数 == 信号传递的数据被忽略了
// 信号函数:
void testsig(int a, double b);
// 槽函数: 
void testslot(int a);

Qt中槽函数的类型是多样的:

  • Qt中的槽函数可以是类的成员函数、全局函数、静态函数、Lambda表达式(匿名函数)
  • 槽函数可以使用关键字进行声明: slots (Qt5中slots可以省略不写)
public slots:
private slots:    // 这样的槽函数不能在类外部被调用
protected slots:  // 这样的槽函数不能在类外部被调用
// 槽函数书写格式举例
// 类中的这三个函数都可以作为槽函数来使用
class Test : public QObject
{
    public:
    void testSlot();
    static void testFunc();

    public slots:
    void testSlot(int id);
};

根据特定场景自定义信号槽

还是上边的场景: 女朋友说:“我肚子饿了!”,于是我带她去吃饭。
// 定义一个 GirlFriend 类,继承自 QObject(必须,才能使用 Qt 的信号槽机制)
class GirlFriend : public QObject
{
    Q_OBJECT // Qt 的元对象宏,启用信号槽机制(必须写)

public:
    // 构造函数,带一个父对象参数,默认值为 nullptr
    // 父对象机制是 Qt 内存管理的一部分,用于对象生命周期管理
    explicit GirlFriend(QObject *parent = nullptr);

signals:
    // 声明一个不带参数的信号 hungry(信号是函数声明,没有函数体)
    // 当这个信号被触发时,只能表达“饿了”,但无法说明具体想吃什么
    void hungry();

    // 声明一个带参数的信号 hungry,参数类型为 QString
    // 参数 msg 可以传递想吃什么(如“烧烤”、“火锅”等)
    void hungry(QString msg);
};

// 定义一个 Me 类,继承自 QObject,用于处理接收到的 hungry 信号
class Me : public QObject
{
    Q_OBJECT // 启用 Qt 的信号槽系统

public:
    // 构造函数,带一个父对象参数
    explicit Me(QObject *parent = nullptr);

public slots:
    // 槽函数:eatMeal,不带参数
    // 接收不带参数的 hungry 信号,只能响应“吃饭”动作,但不知道具体吃什么
    void eatMeal();

    // 槽函数:eatMeal,带一个 QString 类型参数
    // 接收带参数的 hungry 信号,可以根据 msg 内容决定吃什么
    void eatMeal(QString msg);
};


3.3 QObject

QObject 是 Qt 所有对象模型功能(如信号槽、对象树、动态属性、事件机制等)的基础类。

详细解释:

1. QObject 是 Qt 所有对象类的父类

几乎所有重要的 Qt 类都继承自 QObject,如 QWidgetQPushButtonQTimerQThreadQApplication 等。

2. 提供“信号与槽”机制(核心)

  • 只有继承自 QObject 的类才能使用 signalsslots 关键字。

  • QObject 搭配 Q_OBJECT 宏,可以启用 元对象系统(Meta-Object System),这使得 connect()emit 成为可能。

3. 支持“对象树结构”(父子关系)

  • 每个 QObject 都可以有一个父对象(QObject *parent
  • 父对象被销毁时,子对象会自动销毁,避免手动内存管理。
QPushButton *btn = new QPushButton(parentWidget); // parentWidget 是父对象

返回对象指针给btn

4. 支持“事件处理系统”

  • 所有事件系统(如鼠标点击、键盘输入、窗口刷新)都是通过 QObject::event() 分发的。
特性 是否由 QObject 提供 说明
信号槽机制 必须继承 QObject,且加上 Q_OBJECT 宏
对象名管理 objectName()setObjectName()
父子对象管理 自动管理内存,层级结构清晰
事件处理 所有 QObject 子类都能接收和处理事件
动态属性 setProperty()property()
定时器 startTimer()timerEvent()

示例:

class GirlFriend : public QObject
{
    Q_OBJECT // 启用 Qt 元对象系统,才能使用信号槽
public:
    explicit GirlFriend(QObject *parent = nullptr); // 支持父对象管理

signals:
    void hungry(); // 只有继承 QObject 的类才能使用 signals 关键字
};

❗ Q_OBJECT 宏和 QObject 的配合使用

class MyClass : public QObject
{
    Q_OBJECT
};
  • Q_OBJECT 是启用元对象特性的关键(如信号槽、反射机制等)。
  • 缺少这个宏,即使继承了 QObject,信号槽也不能工作。

[!NOTE] Qt 类继承结构简图:

QObject
 ├── QPaintDevice
 │    ├── QWidget
 │         ├── QPushButton
 │         ├── QLabel
 │         └── ...
 ├── QThread
 ├── QTimer
 ├── QApplication
 └── ...

4. 信号槽拓展

4.1 信号槽使用拓展

  • 一个信号可以连接多个槽函数, 发送一个信号有多个处理动作
    • 需要写多个 connect() 连接
    • 槽函数的执行顺序是随机的, 和 connect() 函数的调用顺序没有关系
  • 信号的接收者可以是一个对象, 也可以是多个对象
  • 一个槽函数可以连接多个信号, 多个不同的信号, 处理动作是相同的
    • 需要写多个 connect() 连接
  • 信号可以连接信号
    • 信号接收者可以不处理接收的信号, 而是继续发射新的信号,这相当于传递了数据, 并没有对数据进行处理
connect(const QObject *sender, &QObject::signal, 
        const QObject *receiver, &QObject::siganl-new);
  • 信号槽是可以断开的
disconnect(const QObject *sender, &QObject::signal, 
           const QObject *receiver, &QObject::method);

4.2 信号槽的连接方式

Qt5 的连接方式:
// 语法:
QMetaObject::Connection QObject::connect(
    const QObject *sender, PointerToMemberFunction signal, 
    const QObject *receiver, PointerToMemberFunction method, 
    Qt::ConnectionType type = Qt::AutoConnection);
    //  参数5:type(连接方式,可选参数,默认 Qt::AutoConnection)
    // 表示连接的类型,共有以下几种枚举值:
    // Qt::AutoConnection:自动判断线程,是否异步处理(默认值)
    // Qt::DirectConnection:同步执行槽函数(通常用于同线程)
    // Qt::QueuedConnection:异步执行槽函数(通常用于不同线程)
    // Qt::BlockingQueuedConnection:异步 + 等待完成(用于特殊场景)
    // Qt::UniqueConnection:防止重复连接同一个信号和槽(常用于初始化中)

信号和槽函数也就是第2,4个参数传递的是地址, 编译器在编译过程中会对数据的正确性进行检测

connect(const QObject *sender, &QObject::signal, const QObject *receiver, &QObject::method);

✅ 实际例子:

connect(button, &QPushButton::clicked, this, &MainWindow::close);

等价于:

QMetaObject::Connection conn = QObject::connect(     
	button,                    // 信号的发送者     
	&QPushButton::clicked,     // 信号的函数地址     
	this,                      // 槽函数的接收者     
	&MainWindow::close,        // 槽函数地址     
	Qt::AutoConnection         // 连接方式(默认值) );


Qt4 的连接方式:

这种旧的信号槽连接方式在 Qt5 中是支持的, 但是不推荐使用, 因为这种方式在进行信号槽连接的时候, 信号槽函数通过宏 SIGNAL()SLOT() 转换为字符串类型。

因为信号槽函数的转换是通过宏来进行转换的,因此传递到宏函数内部的数据不会被进行检测, 如果使用者传错了数据,编译器也不会报错,但实际上信号槽的连接已经不对了,只有在程序运行起来之后才能发现问题,而且问题不容易被定位

// Qt4的信号槽连接方式
[static] QMetaObject::Connection QObject::connect(
    const QObject *sender, const char *signal, 
    const QObject *receiver, const char *method, 
    Qt::ConnectionType type = Qt::AutoConnection);

connect(const QObject *sender, SIGNAL(信号函数名(参数1, 参数2, ...)),
        const QObject *receiver, SLOT(槽函数名(参数1, 参数2, ...)));

Qt4中声明槽函数必须要使用 slots 关键字, 不能省略


应用举例

场景描述:
    - 我肚子饿了, 我要吃东西。
分析: 
    - 信号的发出者是我自己, 信号的接收者也是我自己

我们首先定义出一个Qt的类。

class Me : public QObject
{
    Q_OBJECT
public slots: // Qt4中的槽函数必须这样声明, qt5中的关键字 slots 可以被省略
    void eat();
    void eat(QString somthing);
signals:
    void hungury();
    void hungury(QString somthing);
};

基于上边的类写出解决方案

处理如下逻辑: 我饿了, 我要吃东西
分析: 信号的发出者是我自己, 信号的接收者也是我自己

Me m;

// Qt4处理方式
connect(&m, SIGNAL(hungury()), &m, SLOT(eat()));
connect(&m, SIGNAL(hungury(QString)), &m, SLOT(eat(QString)));
// Qt5处理方式
connect(&m, &Me::hungury, &m, &Me::eat);    // error

[!warning] Qt5处理方式错误原因分析:

上边的写法之所以错误是因为这个类中信号槽都是重载过的, 信号和槽都是通过函数名去关联函数的地址, 但是这个同名函数对应两块不同的地址, 一个带参, 一个不带参, 因此编译器就不知道去关联哪块地址了, 所以如果我们在这种时候通过以上方式进行信号槽连接, 编译器就会报错。


解决方案:

可以通过定义函数指针的方式指定出函数的具体参数,这样就可以确定函数的具体地址了。
定义函数指针指向重载的某个信号或者槽函数,在connect()函数中将函数指针名字作为实参就可以了。

// 举例:
void (Me::*func1)(QString) = &Me::eat;     // func1指向带参的槽函数
void (Me::*func2)() = &Me::hungury;        // func2指向不带参的信号

Qt正确的处理方式:

// 定义函数指针指向重载的某一个具体的槽函数地址
void (Me::*myslot)(QString) = &Me::eat;
// 定义函数指针指向重载的某一个具体的信号地址
void (Me::*mysignal)(QString) = &Me::hungury;
// 使用定义的函数指针完成信号槽的连接
connect(&m, mysignal, &m, myslot);

总结

Qt版本 特点 推荐使用
Qt4 使用宏函数,信号槽转换为字符串,无类型检测,易出错
Qt5 使用函数指针,类型安全,编译期可检测,推荐使用
重载 Qt4方式不受影响;Qt5中需要使用函数指针 disambiguation 方式处理
对比项目 Qt4 宏写法 Qt5 函数指针写法
写法是否模糊 ✅ 可以模糊调用,字符串形式 ❌ 不允许模糊,函数指针必须唯一
函数是否可重载 ✅ 支持重载,靠字符串匹配 ✅ 支持,但需要手动 disambiguate
编译期检查 ❌ 不检查参数是否匹配 ✅ 会检查参数是否匹配,安全
易错性 ⚠️ 拼错字符串也能编译 ✅ 编译器能立刻提示错误
推荐程度 ❌ 不推荐(Qt4 的老写法) ✅ Qt5 及以上推荐

函数重载(Function Overloading)是指在同一个作用域中多个函数的名字相同,但参数不同(数量或类型不同)。

void print(int x);         // 打印整数 
void print(double y);      // 打印小数 
void print(QString str);   // 打印字符串

上面这三个 print 函数函数名一样,但参数类型不同,这就是函数重载。


5. Lambda 表达式 [不理解]

Lambda 表达式是 C++11 最重要也是最常用的特性之一,是现代编程语言的一个特点,简洁,提高了代码的效率并且可以使程序更加灵活。Qt 是完全支持 C++ 语法的, 因此在 Qt 中也可以使用 Lambda 表达式。


5.1 语法格式

Lambda 表达式就是一个匿名函数, 语法格式如下:

[capture](params) opt -> ret {body;};

参数说明:

  • capture: 捕获列表
  • params: 参数列表
  • opt: 函数选项
  • ret: 返回值类型
  • body: 函数体

关于 Lambda 表达式的细节介绍:

捕获列表:捕获一定范围内的变量

捕获形式 说明
[] 不捕捉任何变量
[&] 捕获外部作用域中所有变量,并作为引用在函数体内使用
[=] 捕获外部作用域中所有变量,并作为副本在函数体内使用
[=, &foo] 按值捕获所有变量,foo 特别按引用捕获
[bar] 仅按值捕获变量 bar
[&bar] 仅按引用捕获变量 bar
[this] 捕获当前类中的 this 指针,允许访问类成员

参数列表: 与普通函数参数列表相同
opt 选项(可选):

  • mutable: 允许修改按值捕获的副本变量(不是原变量)
  • exception: 指定函数抛出异常类型,如 throw();
    返回值类型:
  • 当返回值为 void 或只有一个 return 时可省略
  • 若存在多种返回路径,需显式声明返回值类型
    函数体: 不可省略,哪怕是空函数体 {}

5.2 定义和调用

因为 Lambda 表达式是一个匿名函数,因此没有函数声明,直接定义即可。
但如果只定义不调用,函数不会执行

// 匿名函数的定义,程序执行时此函数不会被调用
[](){
    qDebug() << "hello, 我是一个lambda表达式...";
};
✅ 匿名函数的定义+调用
int ret = [](int a) -> int
{
    return a+1;
}(100);  // 100是传递给匿名函数的参数

在 Lambda 表达式的捕获列表([])中添加不同的关键字,就可以在函数体中使用外部变量了。

// 在匿名函数外部定义变量
int a = 100, b = 200, c = 300;
❌ 不捕获变量,直接使用会出错
[](){
    // 打印外部变量的值
    qDebug() << "a:" << a << ", b: " << b << ", c:" << c;  // error, 不能使用任何外部变量
};

✅ 使用引用方式捕获外部变量
[&](){
    qDebug() << "hello, 我是一个lambda表达式...";
    qDebug() << "使用引用的方式传递数据: ";
    qDebug() << "a+1:" << a++ << ", b+c= " << b+c;
}();

✅ 使用值拷贝方式 + mutable 修改副本
[=](int m, int n) mutable {
    qDebug() << "hello, 我是一个lambda表达式...";
    qDebug() << "使用拷贝的方式传递数据: ";
    
    // 拷贝的外部数据在函数体内部是只读的
    // 如果不添加 mutable,不能修改这些只读数据的值
    // 添加 mutable 后允许修改这些副本,注意:不会影响外部变量本身
    
    qDebug() << "a+1:" << a++ << ", b+c= " << b+c;
    qDebug() << "m+1: " << ++m << ", n: " << n;
}(1, 2);

作者 & 版权说明

  • 作者:苏丙榅
  • 链接:https://subingwen.cn/qt/qt-signal-slot/#5-2-定义和调用
  • 来源:爱编程的大丙
  • 版权说明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(qt,数据库,开发语言)