【Qt】信号与槽(Signal and Slot)- 简易计算器

摆脱传统 GUI 开发中繁琐的回调函数模式

一、概念

1. 信号(Signal)

信号本质上是一种特殊的成员函数,它由 Qt 对象在特定事件发生时 "发射"(emit)。例如,当用户点击一个QPushButton按钮时,按钮对象会自动发射clicked()信号。

信号本身不包含任何具体的执行代码,它仅仅是一个 "事件发生" 的通知,告诉系统 "某件事已经发生了"。

信号的声明必须放在类定义的signals:区域内,并且无需用户手动实现,Qt 的 MOC(Meta-Object Compiler,元对象编译器)会在编译期间自动为其生成对应的实现代码。

2. 槽(Slot)

槽则是普通的成员函数,它用于接收并处理特定的信号。当一个槽与某个信号成功连接后,一旦该信号被发射,对应的槽函数就会被自动调用。槽函数可以像其他普通成员函数一样包含任意的业务逻辑,例如更新界面显示、执行数据计算、触发其他操作等。

二、工作原理

信号与槽的协作过程主要依赖于 Qt 的元对象系统(Meta-Object System),其核心步骤可分为 "连接" 和 "触发" 两个阶段。

1. 连接阶段:信号与槽connect关联

**QObject::connect()**函数建立关联。

cpp 复制代码
template <typename Func1, typename Func2>

static bool QObject::connect(

const typename Qt::TypeTraits::FunctionPointer<Func1>::Object *sender,// 信号发送者

Func1 signal,// 发射的信号

const typename Qt::TypeTraits::FunctionPointer<Func2>::Object *receiver,// 信号接收者

Func2 slot,// 处理信号的槽函数

Qt::ConnectionType type = Qt::AutoConnection// 连接类型

);

当connect()函数调用成功后,Qt 会在内部维护一个 "信号 - 槽连接表",记录发送者、信号、接收者、槽函数以及连接类型等信息,为后续的信号触发做好准备。

2. 触发阶段:信号发射与槽函数执行

当发送者对象的特定事件发生时,它会调用emit关键字发射对应的信号 。此时,Qt 的元对象系统会遍历**"信号 - 槽连接表"**,查找所有与该信号关联的槽函数,并根据连接类型(如直接调用、队列调用等)的规则,在合适的线程中执行这些槽函数。

整个过程中,发送者和接收者之间实现了松耦合------不需要知道对方什么样( 发送者不需要知道接收者的存在,也不需要知道接收者会如何处理信号;接收者同样不需要了解发送者的内部逻辑,只需要专注于自身槽函数的业务实现。)这种解耦特性使得 Qt 应用程序的模块化程度更高,更易于维护和扩展。

三、常见连接类型

1. Qt::AutoConnection(默认)

默认的连接类型,Qt 会根据发送者和接收者是否处于同一线程自动选择连接方式。

  • 同一线程== Qt::DirectConnection
  • 不在同一线程== Qt::QueuedConnection

避免线程安全问题。

2. Qt::DirectConnection(直接连接)

当信号被发射时,槽函数会立即在发送者线程中被直接调用,其执行方式类似于普通函数调用。这种方式的优点是响应速度快。

3. Qt::QueuedConnection(队列连接)

当信号被发射时,Qt 会将信号对应的 "槽函数调用请求" 封装成一个事件,放入接收者线程的事件队列中。接收者线程的事件循环会在合适的时机取出该事件,并在接收者线程 中执行槽函数。这种方式是跨线程通信的安全选择,尤其适用于接收者为 UI 线程(主线程)的场景,可避免 UI 卡顿或崩溃

4. Qt::BlockingQueuedConnection(阻塞队列连接)

发送者线程会阻塞,直到槽函数执行完毕后才继续运行。

这种连接类型绝对不能用于 "发送者线程与接收者线程为同一线程" 的场景,否则会导致线程死锁。

四、简易计算器

widget.h

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include<Qstack>
#include <QWidget>
#include<string.h>
#include<QAction>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

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

private slots:
    void on_oneButton_clicked();

    void on_twoButton_clicked();

    void on_threeButton_clicked();

    void on_fourButton_clicked();

    void on_fiveButton_clicked();

    void on_sixButton_clicked();

    void on_sevenButton_clicked();

    void on_eightButton_clicked();

    void on_nineButton_clicked();

    void on_zeroButton_clicked();

    void on_leftButton_clicked();

    void on_rightButton_clicked();

    void on_addButton_clicked();

    void on_minusButton_clicked();

    void on_mulButton_clicked();

    void on_divisionButton_clicked();

    void on_clearButton_clicked();

    void on_delButton_clicked();

    void on_equlButton_clicked();

private:
    Ui::Widget *ui;
    QString expression;//存计算式子
};
#endif // WIDGET_H

widget.cpp

计算用栈算,一个数字栈,一个符号栈。

cpp 复制代码
#include "widget.h"
#include "./ui_widget.h"

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

    this->setMaximumSize(200,280);
    this->setMinimumSize(200,280);
    this->setWindowTitle("calculator");

    //字体对象
    QFont f("宋体",14);
    ui->lineEdit->setFont(f);

    //按钮上放图片
    QIcon con("D:\\Item\\QtProject\\calculator\\del.png");
    ui->delButton->setIcon(con);

    //改按钮颜色
    ui->equlButton->setStyleSheet("background:orange");
}

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

void Widget::on_oneButton_clicked()
{
    expression+="1";
    ui->lineEdit->setText(expression);
}

void Widget::on_twoButton_clicked()
{
    expression+="2";
    ui->lineEdit->setText(expression);
}


void Widget::on_threeButton_clicked()
{
    expression+="3";
    ui->lineEdit->setText(expression);
}


void Widget::on_fourButton_clicked()
{
    expression+="4";
    ui->lineEdit->setText(expression);
}


void Widget::on_fiveButton_clicked()
{
    expression+="5";
    ui->lineEdit->setText(expression);
}


void Widget::on_sixButton_clicked()
{
    expression+="6";
    ui->lineEdit->setText(expression);
}


void Widget::on_sevenButton_clicked()
{
    expression+="7";
    ui->lineEdit->setText(expression);
}


void Widget::on_eightButton_clicked()
{
    expression+="8";
    ui->lineEdit->setText(expression);
}


void Widget::on_nineButton_clicked()
{
    expression+="9";
    ui->lineEdit->setText(expression);
}


void Widget::on_zeroButton_clicked()
{
    expression+="0";
    ui->lineEdit->setText(expression);
}


void Widget::on_leftButton_clicked()
{
    expression+="(";
    ui->lineEdit->setText(expression);
}


void Widget::on_rightButton_clicked()
{
    expression+=")";
    ui->lineEdit->setText(expression);
}


void Widget::on_addButton_clicked()
{
    expression+="+";
    ui->lineEdit->setText(expression);
}


void Widget::on_minusButton_clicked()
{
    expression+="-";
    ui->lineEdit->setText(expression);
}


void Widget::on_mulButton_clicked()
{
    expression+="*";
    ui->lineEdit->setText(expression);
}


void Widget::on_divisionButton_clicked()
{
    expression+="/";
    ui->lineEdit->setText(expression);
}


void Widget::on_clearButton_clicked()
{
    expression.clear();
    ui->lineEdit->clear();
}


void Widget::on_delButton_clicked()
{
    //删掉n个字符
    expression.chop(1);
    ui->lineEdit->setText(expression);
}


void Widget::on_equlButton_clicked()
{
    QStack<int> s_num, s_opt;
    char opt[128] = {0};
    int i = 0, tmp = 0, num1, num2;

    // 将表达式转换为C字符串
    QByteArray expr = expression.toLocal8Bit();
    char* p = expr.data();

    // 解析表达式
    while (*p != '\0' || !s_opt.isEmpty())
    {
        if (*p >= '0' && *p <= '9')
        {
            // 处理多位数
            tmp = tmp * 10 + (*p - '0');
            p++;
            if (*p < '0' || *p > '9')
            {
                s_num.push(tmp);
                tmp = 0;
            }
        }
        else
        {
            // 处理运算符
            if (s_opt.isEmpty() || *p == '(' ||
                (s_opt.top() == '(') ||
                ((s_opt.top() == '+' || s_opt.top() == '-') && (*p == '*' || *p == '/')))
            {
                s_opt.push(*p);
                p++;
            }
            else if (*p == ')' || s_opt.top() == ')')
            {
                // 处理括号
                if (*p == ')')
                {
                    while (s_opt.top() != '(')
                    {
                        opt[i++] = s_opt.top();
                        s_opt.pop();
                    }
                    s_opt.pop(); // 弹出'('
                    p++;
                }
            }
            else
            {
                // 处理运算符优先级
                opt[i++] = s_opt.top();
                s_opt.pop();
            }
        }
    }

    // 计算栈中剩余的运算符
    while (!s_opt.isEmpty())
    {
        opt[i++] = s_opt.top();
        s_opt.pop();
    }

    // 执行计算
    for (int j = 0; j < i; j++)
    {
        num2 = s_num.top();
        s_num.pop();
        num1 = s_num.top();
        s_num.pop();

        switch (opt[j])
        {
        case '+':
            s_num.push(num1 + num2);
            break;
        case '-':
            s_num.push(num1 - num2);
            break;
        case '*':
            s_num.push(num1 * num2);
            break;
        case '/':
            if (num2 == 0)
            {
                ui->lineEdit->setText("Error");
                expression.clear();
                return;
            }
            s_num.push(num1 / num2);
            break;
        default:
            break;
        }
    }

    // 显示结果
    if (!s_num.isEmpty())
    {
        ui->lineEdit->setText(QString::number(s_num.top()));
        expression = QString::number(s_num.top());
    }
    else
    {
        ui->lineEdit->setText("Error");
        expression.clear();
    }
}

五、信号与槽的优势及注意事项

1. 核心优势

  • 松耦合设计:发送者与接收者相互独立,无需了解对方的实现细节,便于代码的维护和扩展。
  • 类型安全:Qt 5 基于函数指针的连接方式会在编译期间进行类型检查,若信号与槽的参数不匹配,会直接报错,避免运行时错误。
  • 多对多支持:一个信号可以连接多个槽函数(信号发射时所有槽函数都会执行),一个槽函数也可以接收多个信号,灵活性极高。
  • 跨线程通信:通过不同的连接类型(如Qt::QueuedConnection),可安全实现多线程间的对象通信,无需手动处理线程同步。

2. 注意事项

  • 必须继承 QObject:只有继承自QObject的类才能使用信号与槽机制,且类定义中必须包含Q_OBJECT宏(即使类中没有自定义信号或槽,若使用了 Qt 的元对象功能,也需添加该宏)。
  • 信号与槽的参数匹配:信号的参数数量可以多于槽函数的参数数量,但信号的前 N 个参数类型必须与槽函数的 N 个参数类型完全匹配(多余的参数会被忽略);反之,若槽函数的参数数量多于信号,则无法建立连接。
  • 避免循环连接:若 A 的信号连接 B 的槽,而 B 的信号又连接 A 的槽,可能会导致信号与槽的循环触发,最终引发程序逻辑错误或崩溃。
  • 断开无用连接 :当发送者或接收者对象被销毁时,Qt 会自动断开与它们相关的所有连接,无需手动处理;但对于长期存在的对象,若某些连接不再需要,通过QObject::disconnect()函数手动断开,以避免不必要的资源消耗。
相关推荐
歪歪1003 小时前
React Native开发Android&IOS流程完整指南
android·开发语言·前端·react native·ios·前端框架
yaoxin5211233 小时前
212. Java 函数式编程风格 - Java 编程风格转换:命令式 vs 函数式(以循环为例)
java·开发语言
ZYMFZ3 小时前
python面向对象
前端·数据库·python
wangqiaowq3 小时前
ImmutableList.of() 是 Google Guava 库 提供的一个静态工厂方法,用于创建一个不可变的(immutable)列表。
开发语言·windows·python
十五年专注C++开发3 小时前
QDarkStyleSheet: 一个Qt应用的暗色主题解决方案
开发语言·c++·qt·qss
麦麦鸡腿堡3 小时前
Java的代码块介绍与快速入门
java·开发语言
小龙报3 小时前
《算法每日一题(1)--- 连续因子》
c语言·开发语言·c++·windows·git·算法·visual studio
流浪大叔3 小时前
Python下载实战技巧的技术文章大纲
开发语言·python
祁同伟.3 小时前
【C++】异常
开发语言·c++