Faker :: Lost
REALITY IS FAKE
0%

Qt 开发教程 - 第2章 信号与槽机制

预计阅读 22 分钟

第2章:Qt 信号与槽机制

信号与槽(Signals and Slots)是 Qt 框架的核心特性,用于实现对象间的通信。它类似于其他语言中的回调函数或事件处理机制,但更加灵活、类型安全且易于使用。

核心概念

什么是信号与槽?

信号与槽 是 Qt 用于对象间通信的机制,当一个特定事件发生时(如按钮被点击),会发出一个"信号",而连接到该信号的"槽"函数会被自动调用。

┌─────────────┐         信号         ┌─────────────┐
│   发送者    │ ─────────────────────>│    接收者    │
│  (Sender)   │    Signal (信号)      │ (Receiver)  │
│             │                      │             │
│  QPushButton│  ──clicked()───>      │  MainWindow │
└─────────────┘                      └─────────────┘
                                          ↓
                                   ┌─────────────┐
                                   │   槽函数     │
                                   │    Slot     │
                                   │ Calculate() │
                                   └─────────────┘

三大核心要素

要素 英文 说明 示例
信号 Signal 事件发生时发出的通知 clicked(), textChanged()
Slot 响应信号的函数 任何 void 成员函数
连接 Connect connect() 绑定信号和槽 connect(sender, signal, receiver, slot)

基本语法

Qt 5 现代语法(推荐)

connect(发送者指针, &发送者类::信号, 接收者指针, &接收者类::槽);

示例:

// 按钮 点击 → 执行计算
connect(button, &QPushButton::clicked, this, &MainWindow::calculate);

// 输入框 文本变化 → 更新标签
connect(lineEdit, &QLineEdit::textChanged, this, &MainWindow::updateLabel);

Qt 4 传统语法(兼容旧代码)

connect(发送者, SIGNAL(信号(参数)), 接收者, SLOT(槽(参数)));

示例:

connect(button, SIGNAL(clicked()), this, SLOT(calculate()));
connect(lineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(updateLabel(const QString&)));

语法对比

特性 Qt 4 语法 Qt 5 语法
检查时机 运行时 编译时
类型安全 ❌ 不安全 ✅ 安全
代码提示 ❌ 无 ✅ 有
支持隐式转换 ✅ 是 ❌ 否
Lambda 表达式 ❌ 不支持 ✅ 支持

工作原理

执行流程

┌─────────────────────────────────────────────────────────┐
│                    信号槽工作流程                         │
└─────────────────────────────────────────────────────────┘

1. 用户操作
   ↓
2. 发送者发出信号 (emit signal)
   ↓
3. Qt 查找已建立的连接
   ↓
4. 调用接收者的槽函数
   ↓
5. 槽函数执行相应操作

完整示例流程

// 1. 建立连接(程序启动时执行一次)
connect(pbt1, &QPushButton::clicked, this, &MainWindow::Calculate_Ball_Volume);

// 2-5. 用户交互触发
用户点击按钮
    ↓
QPushButton 发出 clicked() 信号
    ↓
Qt 找到连接的槽
    ↓
调用 MainWindow::Calculate_Ball_Volume()
    ↓
执行计算并显示结果

使用示例

示例 1:按钮点击事件

// mainwindow.h
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);

private slots:
    void onButtonClick();  // 槽函数声明

private:
    QPushButton *button;
};

// mainwindow.cpp
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    button = new QPushButton("点击我", ui->centralwidget);

    // 建立连接
    connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClick);
}

void MainWindow::onButtonClick()
{
    QMessageBox::information(this, "提示", "按钮被点击了!");
}

示例 2:输入框实时响应

// 输入框文本变化时实时处理
connect(lineEdit, &QLineEdit::textChanged, this, [](const QString &text) {
    qDebug() << "当前输入:" << text;
});

示例 3:自定义信号

// mainwindow.h
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);

signals:  // 信号声明区域
    void valueChanged(int newValue);  // 自定义信号

public slots:
    void setValue(int value);  // 槽函数

private:
    int m_value;
};

// mainwindow.cpp
void MainWindow::setValue(int value)
{
    if (m_value != value) {
        m_value = value;
        emit valueChanged(value);  // 发出信号
    }
}

示例 4:使用 Lambda 表达式

// Qt 5 独有特性
connect(button, &QPushButton::clicked, this, [this]() {
    // Lambda 函数体
    QString text = lineEdit->text();
    label->setText("你输入了:" + text);
});

高级用法

1. 一对多连接

一个信号可以连接多个槽:

connect(button, &QPushButton::clicked, this, &MainWindow::slot1);
connect(button, &QPushButton::clicked, this, &MainWindow::slot2);
connect(button, &QPushButton::clicked, this, &MainWindow::slot3);

// 点击按钮时,slot1、slot2、slot3 按连接顺序依次执行

2. 多对一连接

多个信号可以连接同一个槽:

connect(button1, &QPushButton::clicked, this, &MainWindow::handleClick);
connect(button2, &QPushButton::clicked, this, &MainWindow::handleClick);
connect(button3, &QPushButton::clicked, this, &MainWindow::handleClick);

// 三个按钮都调用同一个槽函数

3. 信号连接信号

connect(button1, &QPushButton::clicked, button2, &QPushButton::click);
// 点击 button1 时自动点击 button2

4. 带参数的信号槽

// 信号声明
signals:
    void dataReceived(const QString &data, int timestamp);

// 槽函数声明
public slots:
    void processData(const QString &data, int timestamp);

// 连接
connect(sender, &Sender::dataReceived,
        receiver, &Receiver::processData);

// 发出信号
emit dataReceived("Hello", 12345);

5. 断开连接

// 断开特定连接
disconnect(button, &QPushButton::clicked, this, &MainWindow::onButtonClick);

// 断开对象的所有连接
button->disconnect();

6. Qt::ConnectionType 连接类型

enum ConnectionType {
    AutoConnection,      // 自动选择(默认)
    DirectConnection,    // 直接调用(同步)
    QueuedConnection,    // 事件队列(异步)
    BlockingQueuedConnection,  // 阻塞式异步
    UniqueConnection     // 避免重复连接
};

// 示例:跨线程时使用队列连接
connect(sender, &Sender::signal,
        receiver, &Receiver::slot,
        Qt::QueuedConnection);

常见问题

Q1: 信号和槽的参数必须匹配吗?

A: 槽函数的参数数量可以少于或等于信号的参数数量:

// ✅ 正确:槽参数 ≤ 信号参数
signal:  void dataChanged(int id, const QString &name);
slot:    void updateName(const QString &name);  // 忽略第一个参数

// ❌ 错误:槽参数 > 信号参数
slot:    void updateAll(int id, const QString &name, bool flag);

Q2: 为什么信号连接了但槽不执行?

检查清单:

// 1. 检查是否真的建立了连接
bool connected = connect(button, &QPushButton::clicked, this, &MainWindow::slot);
qDebug() << "连接状态:" << connected;

// 2. 检查槽函数是否声明在 slots 区域
private slots:
    void mySlot();  // ✅ 正确

// 3. 检查是否真的发出了信号
emit mySignal();  // 自定义信号需要手动 emit

Q3: 信号槽的执行顺序是确定的吗?

A: 同一对象的多个槽按连接顺序执行,不同对象的顺序不确定。

Q4: 如何在槽中获取信号发送者?

void MainWindow::mySlot()
{
    QObject *senderObj = sender();  // 获取发送者指针
    if (QPushButton *btn = qobject_cast<QPushButton*>(senderObj)) {
        qDebug() << "来自按钮:" << btn->text();
    }
}

Q5: Lambda 表达式的注意事项

// ⚠️ 注意捕获生命周期
int *data = new int(42);
connect(button, &QPushButton::clicked, this, [data]() {
    qDebug() << *data;  // 危险!data 可能已被释放
});

// ✅ 使用值捕获或确保生命周期
connect(button, &QPushButton::clicked, this, [value = *data]() {
    qDebug() << value;  // 安全
});

最佳实践

1. 命名规范

// 信号:使用过去时或事件名词
signals:
    void clicked();
    void valueChanged();
    void dataReceived();

// 槽:使用动词开头
private slots:
    void handleClick();
    void updateValue();
    void processData();

2. 避免循环连接

// ❌ 危险:可能造成无限递归
connect(obj1, &Obj1::signal1, obj2, &Obj2::slot2);
connect(obj2, &Obj2::signal2, obj1, &Obj1::slot1);

// ✅ 解决:使用标志位防止递归
bool m_processing = false;
void slot1() {
    if (m_processing) return;
    m_processing = true;
    // ... 处理逻辑
    m_processing = false;
}

3. 使用 Qt 5 新语法

// ✅ 推荐:编译时检查,类型安全
connect(button, &QPushButton::clicked, this, &MainWindow::onClick);

// ❌ 避免:运行时检查,易出错
connect(button, SIGNAL(clicked()), this, SLOT(onClick()));

4. 及时断开不需要的连接

// 临时对象用完后断开
connect(tempObject, &TempObject::finished, this, [this]() {
    // 处理完成
    tempObject->disconnect();  // 断开所有连接
    tempObject->deleteLater();
});

5. 信号槽中的内存管理

// ✅ 使用 QObject 内存管理
QObject::connect(sender, &Sender::destroyed, receiver, [receiver]() {
    // sender 被销毁时的清理工作
});

// ✅ 设置父对象,自动释放
MyObject *obj = new MyObject(this);  // this 被销毁时 obj 自动释放

实战案例:完整的计算器程序

// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QGridLayout>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void calculateVolume();      // 计算球体体积
    void clearInput();           // 清空输入
    void onInputChanged(const QString &text);  // 输入变化时验证

private:
    Ui::MainWindow *ui;
    QLabel *label_r;
    QLabel *label_v;
    QLineEdit *lineEdit;
    QPushButton *btnCalculate;
    QPushButton *btnClear;

    void setupUI();              // 设置界面
    void createConnections();    // 创建连接
};

#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>

const static double PI = 3.14159;

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    setupUI();
    createConnections();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::setupUI()
{
    label_r = new QLabel("输入球的半径:", ui->centralwidget);
    label_v = new QLabel(ui->centralwidget);
    lineEdit = new QLineEdit(ui->centralwidget);
    btnCalculate = new QPushButton("计算体积", ui->centralwidget);
    btnClear = new QPushButton("清空", ui->centralwidget);

    QGridLayout *layout = new QGridLayout(ui->centralwidget);
    layout->addWidget(label_r, 0, 0);
    layout->addWidget(lineEdit, 0, 1);
    layout->addWidget(label_v, 1, 0, 1, 2);
    layout->addWidget(btnCalculate, 2, 0);
    layout->addWidget(btnClear, 2, 1);
}

void MainWindow::createConnections()
{
    // 按钮1:计算体积
    connect(btnCalculate, &QPushButton::clicked,
            this, &MainWindow::calculateVolume);

    // 按钮2:清空输入
    connect(btnClear, &QPushButton::clicked,
            this, &MainWindow::clearInput);

    // 输入框:实时验证
    connect(lineEdit, &QLineEdit::textChanged,
            this, &MainWindow::onInputChanged);

    // 使用 Lambda:输入框回车时计算
    connect(lineEdit, &QLineEdit::returnPressed,
            this, [this]() { calculateVolume(); });
}

void MainWindow::calculateVolume()
{
    bool ok;
    double radius = lineEdit->text().toDouble(&ok);

    if (!ok || radius < 0) {
        label_v->setText("输入无效!请输入正数");
        return;
    }

    double volume = (4.0 / 3.0) * PI * radius * radius * radius;
    label_v->setText(QString("球体体积:%1").arg(volume, 0, 'f', 2));
}

void MainWindow::clearInput()
{
    lineEdit->clear();
    label_v->clear();
}

void MainWindow::onInputChanged(const QString &text)
{
    if (text.isEmpty()) {
        label_v->clear();
    }
}

总结

信号槽核心要点

要点 说明
用途 对象间通信、事件处理
优点 松耦合、类型安全、灵活性高
语法 Qt 5 新语法(推荐)、Qt 4 传统语法
连接 connect(sender, signal, receiver, slot)
断开 disconnect() 或对象销毁自动断开
特点 一对多、多对一、支持 Lambda

学习路线

1. 基础:理解信号槽概念
   ↓
2. 语法:掌握 connect() 用法
   ↓
3. 实践:编写简单示例程序
   ↓
4. 进阶:自定义信号、Lambda、多线程
   ↓
5. 最佳:遵循最佳实践和设计模式

参考资源


通过本教程的学习,你应该能够掌握 Qt 信号与槽的核心概念和实际应用。信号槽是 Qt 开发的基础,熟练掌握它将大大提高你的开发效率。

💬 评论区

感谢阅读!如有任何问题或建议,欢迎交流。