摆脱传统 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()函数手动断开,以避免不必要的资源消耗。