Skip to content

信号槽

内容摘要:文章中主要介绍了Qt中的信号槽, 主要内容包括: 信号槽的本质、信号槽的关系、标准信号槽的使用、自定义信号槽的使用、信号槽的拓展、Lambda表达式。

信号和槽概述

信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式(发布-订阅模式)。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。

信号的本质

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

因此根据上述的描述我们得到一个结论 – 信号的本质就是事件,比如:

  • 按钮单击、双击
  • 窗口刷新
  • 鼠标移动、鼠标按下、鼠标释放
  • 键盘输入

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

  • 我们对哪个窗口进行操作, 哪个窗口就可以捕捉到这些被触发的事件。
  • 对于使用者来说触发了一个事件我们就可以得到Qt框架给我们发出的某个特定信号。
  • 信号的呈现形式就是函数, 也就是说某个事件产生了, Qt框架就会调用某个对应的信号函数, 通知使用者。

在QT中信号的发出者是某个实例化的类对象,对象内部可以进行相关事件的检测。

槽的本质

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

举个简单的例子:女朋友说:“我肚子饿了!”,于是我带她去吃饭。

上边例子中相当于女朋友发出了一个信号, 我收到了信号并其将其处理掉了。

实例对象角色描述
女朋友信号发出者信号携带的信息: 我饿了
信号接收者处理女朋友发射的信号: 带他去吃饭

在Qt中槽函数的所有者也是某个类的实例对象。

信号和槽的关系

在Qt中信号和槽函数都是独立的个体,本身没有任何联系,但是由于某种特性需求我们可以将二者连接到一起,好比牛郎和织女想要相会必须要有喜鹊为他们搭桥一样。在Qt中我们需要使用QOjbect类中的connect函数进二者的关联。

connect()函数和鹊桥作用相同

连接信号和槽的connect()函数原型如下, 其中PointerToMemberFunction是一个指向函数地址的指针

cpp
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方法,信号发出之后的处理动作
cpp
//  参数 signal 和 method 都是函数地址, 因此简化之后的 connect() 如下:
connect(const QObject *sender, &QObject::signal, const QObject *receiver, &QObject::method);

使用connect()进行信号槽连接的注意事项:

  • connect函数相对于做了信号处理动作的注册
  • 调用conenct函数的sender对象的信号并没有产生, 因此receiver对象的method也不会被调用
  • method槽函数本质是一个回调函数, 调用的时机是信号产生之后, 调用是Qt框架来执行的
  • connect中的sender和recever两个指针必须被实例化了, 否则conenct不会成功

标准信号槽

在Qt提供的很多标准类中都可以对用户触发的某些特定事件进行检测, 因此当用户做了这些操作之后, 事件被触发类的内部就会产生对应的信号, 这些信号都是Qt类内部自带的, 因此称之为标准信号。

同样的,在Qt的很多类内部为我了提供了很多功能函数,并且这些函数也可以作为触发的信号的处理动作,有这类特性的函数在Qt中称之为标准槽函数。

系统自带的信号和槽通常如何查找呢,这个就需要利用帮助文档了,比如在帮助文档中查询按钮的点击信号,那么需要在帮助文档中输入QPushButton

img

首先我们可以在Contents中寻找关键字 signals,信号的意思,但是我们发现并没有找到,这时候我们应该看当前类从父类继承下来了哪些信号

img

因此我们去他的父类QAbstractButton中就可以找到该关键字,点击signals索引到系统自带的信号有如下几个

image20200411142356836.webp

掌握标准信号、槽的查找方式之后以及connect()函数的作用之后, 下面通过一个简单的例子给大家讲解一下他们的使用方式。

功能实现: 点击窗口上的按钮,关闭窗口

功能分析:

  • 按钮: 信号发出者 -> QPushButton 类型
  • 窗口: 信号的接收者和处理者 -> QWidget 类型

需要使用的标准信号槽函数

cpp
// 单击按钮发出的信号
[signal] void QAbstractButton::clicked(bool checked = false)
// 关闭窗口的槽函数
[slot] bool QWidget::close();

对于上边的需求只需要一句代码, 只需要写一句代码就能实现了

cpp
// 单击按钮关闭窗口
connect(ui->closewindow, &QPushButton::clicked, this, &MainWindow::close);

connect()操作一般写在窗口的构造函数中, 相当于在事件产生之前在qt框架中先进行注册, 这样在程序运行过程中假设产生了按钮的点击事件, 框架就会调用信号接收者对象对应的槽函数了, 如果信号不产生, 槽函数也就一直不会被调用。

自定义信号槽

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

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

  • 要编写新的类并且让其继承Qt的某些标准类
  • 这个新的子类必须从QObject类或者是QObject子类进行派生
  • 在定义类的头文件中加入 Q_OBJECT 宏
cpp
// 在头文件派生类的时候,首先像下面那样引入Q_OBJECT宏:
class MyMainWindow : public QWidget
{
    Q_OBJECT
    ......
}

自定义信号

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

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

  1. 信号是类的成员函数
  2. 返回值必须是 void 类型
  3. 信号的名字可以根据实际情况进行指定
  4. 参数可以随意指定, 信号也支持重载
  5. 信号需要使用 signals 关键字进行声明, 使用方法类似于public等关键字
  6. 信号函数只需要声明, 不需要定义(没有函数体实现)
  7. 在程序中发射自定义信号: 发送信号的本质就是调用信号函数
    • 习惯性在信号函数前加关键字: emit, 但是可以省略不写
    • emit只是显示的声明一下信号要被发射了, 没有特殊含义
    • 底层 emit == #define emit
cpp
// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class Test : public QObject
{
    Q_OBJECT
signals:
    void testsignal();
    // 参数的作用是数据传递, 谁调用信号函数谁就指定实参
    // 实参最终会被传递给槽函数
    void testsignal(int a);
};

自定义槽

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

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

  1. 返回值必须是 void 类型

  2. 槽也是函数, 因此也支持重载

  3. 槽函数需要指定多少个参数, 需要看连接的信号的参数个数

  4. 槽函数的参数是用来接收信号传递的数据的, 信号传递的数据就是信号的参数。举例:信号函数: void testsig(int a, double b); 槽函数: void testslot(int a, double b); 总结:

    • 槽函数的参数应该和对应的信号的参数个数, 从左到右类型依次对应
    • 信号的参数可以大于等于槽函数的参数个数,信号传递的数据被忽略了 信号函数: void testsig(int a, double b); 槽函数: void testslot(int a);
  5. Qt中槽函数的类型是多样的。Qt中的槽函数可以是类的成员函数全局函数静态函数Lambda表达式(匿名函数)

  6. 槽函数可以使用关键字进行声明: slots (Qt5中slots可以省略不写)

    • public slots:
    • private slots: –> 这样的槽函数不能在类外部被调用
    • protected slots: –> 这样的槽函数不能在类外部被调用
    cpp
    // 槽函数书写格式举例
    // 类中的这三个函数都可以作为槽函数来使用
    class Test : public QObject
    {
        public:
        void testSlot();
        static void testFunc();
    
        public slots:
        void testSlot(int id);
    };

根据特定场景自定义信号槽,还是上边的场景: 女朋友说:“我肚子饿了!”,于是我带她去吃饭。

cpp
// class GirlFriend
class GirlFriend : public QObject
{
    Q_OBJECT
public:
    explicit GirlFriend(QObject *parent = nullptr);

signals:
    void hungry();              // 不能表达出想要吃什么
    void hungry(QString msg);   // 可以通过参数表达想要吃什么
};

// class Me
class Me : public QObject
{
    Q_OBJECT
public:
    explicit Me(QObject *parent = nullptr);

public slots:
    // 槽函数
    void eatMeal();             // 不能知道信号发出者要吃什么
    void eatMeal(QString msg);  // 可以知道信号发出者要吃什么
};

信号槽使用拓展

  • 一个信号可以连接多个槽函数, 发送一个信号有多个处理动作

    • 需要写多个connect()连接
    • 槽函数的执行顺序是随机的, 和connect函数的调用顺序没有关系
    • 信号的接收者可以是一个对象, 也可以是多个对象
  • 一个槽函数可以连接多个信号, 多个不同的信号, 处理动作是相同的

    • 需要写多个connect()连接
  • 信号可以连接信号

    信号接收者可以不处理接收的信号, 而是继续发射新的信号,这相当于传递了数据, 并没有对数据进行处理

    cpp
    connect(const QObject *sender, &QObject::signal, 
            const QObject *receiver, &QObject::siganl-new);
  • 信号槽是可以断开的

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

信号槽的连接方式

  • Qt5的连接方式

    cpp
    // 语法:
    QMetaObject::Connection QObject::connect(
            const QObject *sender, PointerToMemberFunction signal, 
            const QObject *receiver, PointerToMemberFunction method, 
            Qt::ConnectionType type = Qt::AutoConnection);
    
    // 信号和槽函数也就是第2,4个参数传递的是地址, 编译器在编译过程中会对数据的正确性进行检测
    connect(const QObject *sender, &QObject::signal, 
            const QObject *receiver, &QObject::method);

    Qt::ConnectionType

  • Qt4的连接方式

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

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

    cpp
    // 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的类。

    cpp
    class Me : public QObject
    {
        Q_OBJECT
        // Qt4中的槽函数必须这样声明, qt5中的关键字 slots 可以被省略
    public slots:
        void eat();
        void eat(QString somthing);
        signals:
        void hungury();
        void hungury(QString somthing);
    };
    
    // 基于上边的类写出解决方案
    // 处理如下逻辑: 我饿了, 我要吃东西
    // 分析: 信号的发出者是我自己, 信号的接收者也是我自己
    Me m;
    // Qt4处理方式
    connect(&m, SIGNAL(eat()), &m, SLOT(hungury()));
    connect(&m, SIGNAL(eat(QString)), &m, SLOT(hungury(QString)));
    
    // Qt5处理方式
    connect(&m, &Me::eat, &m, &Me::hungury);    // error

    Qt5处理方式错误原因分析:

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

    解决方案:

    • 可以通过定义函数指针的方式指定出函数的具体参数,这样就可以确定函数的具体地址了。
    • 定义函数指针指向重载的某个信号或者槽函数,在connect()函数中将函数指针名字作为实参就可以了。
    cpp
    // 举例:
    void (Me::*func1)(QString) = &Me::eat;  // func1指向带参的信号
    void (Me::*func2)() = &Me::hungury; // func2指向不带参的槽函数

    Qt正确的处理方式:

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

总结

  • Qt4的信号槽连接方式因为使用了宏函数, 宏函数对用户传递的信号槽不会做错误检测, 容易出bug
  • Qt5的信号槽连接方式, 传递的是信号槽函数的地址, 编译器会做错误检测, 减少了bug的产生
  • 当信号槽函数被重载之后, Qt4的信号槽连接方式不受影响
  • 当信号槽函数被重载之后, Qt5中需要给被重载的信号或者槽定义函数指针

Lambda表达式

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

我们可以在连接信号槽的时间,直接把槽函数写成Lambda表达式的形式,这样就可以减少槽函数的定义,直接在connect函数中进行书写。形式如下:

cpp
connect(信号发出者, 信号, this, [=](){ 槽函数的处理逻辑; });
// this指针可以省略不写,变成了如下形式
connect(信号发出者, 信号, [=](){ 槽函数的处理逻辑; });

与回调函数对比

核心概念对比(本质区别)

  • 信号槽:Qt框架特有的事件驱动机制,通过元对象编译器(moc)实现,支持松耦合的消息传递。
  • 回调函数:传统C/C++的函数指针机制,通过注册函数指针实现异步通知。

实现原理对比

维度信号槽回调函数
绑定方式运行时动态绑定(connect函数)编译时静态绑定(函数指针赋值)
参数传递自动处理(类型安全)手动管理(需严格匹配参数类型)
线程安全性支持跨线程(需指定连接方式)需手动同步(默认非线程安全)
多对多支持天然支持(一个信号触发多个槽)需额外实现(如注册多个回调)

优缺点对比

信号槽优点

  • 松耦合:发送者与接收者无需直接关联
  • 类型安全:参数类型由编译器检查
  • 扩展性强:支持信号转发、参数过滤等高级特性
  • 线程友好:可通过Qt::ConnectionType控制线程行为

回调函数优点

  • 轻量级:无需依赖框架
  • 直接性:函数调用开销低
  • 灵活性:可自由定制调用逻辑

信号槽缺点

  • 依赖Qt框架
  • 元对象系统增加编译时间
  • 动态绑定可能影响性能(需与回调对比)

回调函数缺点

  • 紧耦合:调用方需知道回调函数的具体实现
  • 类型不安全:可能引发运行时错误
  • 多线程支持复杂:需手动处理锁机制

典型应用场景

  • 信号槽适用场景

    • GUI应用的事件响应(按钮点击、窗口关闭等)
    • 模块间松耦合通信(如网络模块与UI模块解耦)
    • 需要跨线程通信的场景
  • 回调函数适用场景

    • 轻量级库设计(如C风格API的异步通知)
    • 对性能敏感的高频操作
    • 简单的单线程事件处理

代码示例对比

信号槽示例

cpp
// 定义信号和槽
class MyWidget : public QWidget {
    Q_OBJECT
signals:
    void buttonClicked();
public slots:
    void onButtonClick() { /* 处理逻辑 */ }
};

// 连接信号槽
connect(button, &QPushButton::clicked, this, &MyWidget::onButtonClick);

回调函数示例

cpp
// 定义回调函数指针
typedef void (*Callback)(int);

// 使用回调
void asyncOperation(Callback cb) {
    // 异步操作完成后调用
    cb(result);
}

// 注册回调
asyncOperation([](int res) { /* 处理结果 */ });

总结

  • 信号槽是框架级解决方案,适合大型工程的复杂通信需求
  • 回调函数是语言级基础机制,适合轻量级或特定性能场景
  • 实际开发中常结合使用:关键业务用信号槽保证可维护性,性能瓶颈处用回调优化

"Qt信号槽与回调函数的核心区别在于设计理念:信号槽通过元对象系统实现了松耦合的事件驱动,而回调函数是基于函数指针的直接调用。信号槽在大型项目中优势明显,比如UI模块与业务逻辑的解耦,但需要承担框架开销;回调函数在轻量级场景下更高效,但容易导致代码紧耦合。在实际开发中,我会根据具体需求选择:对于需要跨线程或扩展性强的模块用信号槽,对性能敏感的底层库则使用回调函数。"

QSignalMapper

QSignalMapper 是 Qt 框架中的一个类,用于将多个信号映射到同一个槽函数。它可以将多个信号映射到同一个槽函数,从而实现信号的批量处理。

QSignalMapper 的使用步骤如下:

  1. 创建 QSignalMapper 对象。
  2. 将多个信号与 QSignalMapper 的 map() 方法关联。
  3. 将 QSignalMapper 的 mapped() 信号与槽函数关联。
  4. 当信号发出时,QSignalMapper 会自动调用槽函数,并将信号的参数传递给槽函数。

QSignalMapper 的优点是可以简化信号与槽的连接,避免了手动管理多个信号与槽的连接。它还可以方便地处理多个信号的参数,使得槽函数的处理更加灵活。

QSignalMapper 的缺点是需要手动管理信号与槽的连接,可能会增加代码的复杂度。此外,QSignalMapper 也可能会影响程序的性能,因为它需要额外的处理逻辑。

QSignalMapper 的典型应用场景包括:

  • 多个按钮的点击事件处理
  • 多个菜单的选择事件处理
  • 多个控件的状态改变事件处理