【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()函数建立关联。
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
#ifndef WIDGET_H
#define WIDGET_H#include<Qstack>
#include <QWidget>
#include<string.h>
#include<QAction>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic: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
计算用栈算,一个数字栈,一个符号栈。
#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()函数手动断开,以避免不必要的资源消耗。