信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式(发布-订阅模式)。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
信号是由于用户对窗口或控件进行了某些操作,导致窗口或控件产生了某个特定事件,这时候Qt对应的窗口类会发出某个信号,以此对用户的挑选做出反应。
因此根据上述的描述我们得到一个结论:信号的本质就是事件,比如:
那么在Qt中信号是通过什么形式呈现给使用者的呢?
在QT中信号的发出者是某个实例化的类对象,对象内部可以进行相关事件的检测。
槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的C++函数是一样的,可以定义在类的任何部分(public、private或 protected),可以具有任何参数,可以被重载,也可以被直接调用(但是不能有默认参数)。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。
本文福利, 免费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块等等)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓
举个简单的例子:
女朋友说:“我肚子饿了!”,于是我带她去吃饭。
上边例子中相当于女朋友发出了一个信号, 我收到了信号并其将其处理掉了。
在Qt中槽函数的所有者也是某个类的实例对象。
写信:发件人 信的内容 收件人 收到信做事情
在Qt中信号和槽函数都是独立的个体,本身没有任何联系,但是由于某种特性需求我们可以将二者连接到一起,好比牛郎和织女想要相会必须要有喜鹊为他们搭桥一样。
信号与槽关联是用 QObject::connect() 函数实现的,其基本格式是:
[static] QMetaObject::Connection QObject::connect(
const QObject *sender,
const char *signal,
const QObject *receiver,
const char *method,
Qt::ConnectionType type = Qt::AutoConnection);
connect函数相对于做了信号处理动作的注册,调用conenct连接信号与槽时,sender对象的信号并没有产生, 因此receiver对象的method也不会被调用,method槽函数本质是一个回调函数, 调用的时机是信号产生之后。 调用槽函数是Qt框架来执行的,connect中的sender和recever两个指针必须被实例化了, 否则conenct不会成功。
在Qt提供的很多类中都可以对用户触发的某些特定事件进行检测, 当事件被触发后就会产生对应的信号, 这些信号都是Qt类内部自带的, 因此称之为标准信号。
同样的,在Qt的很多类内部为我了提供了很多功能函数,并且这些函数也可以作为触发的信号的处理动作,有这类特性的函数在Qt中称之为标准槽函数。
系统自带的信号和槽通常如何查找呢,这个就需要利用帮助文档了,在帮助文档中比如我们上面的按钮的点击信号,在帮助文档中输入QPushButton,首先我们可以在Contents中寻找关键字 signals,信号的意思,但是我们发现并没有找到,这时候我们应该看当前类从父类继承下来了哪些信号,因此我们去他的父类QAbstractButton中就可以找到该关键字,点击signals索引到系统自带的信号有如下几个
功能实现: 点击窗口上的按钮, 关闭窗口
按钮: 信号发出者 -> QPushButton
窗口: 信号的接收者和处理者 -> QWidget
// 单击按钮发出的信号
[signal] void QAbstractButton::clicked(bool checked = false)
// 关闭窗口的槽函数
[slot] bool QWidget::close();
// 单击按钮关闭窗口
connect(ui->closewindow, &QPushButton::clicked, this, &MainWindow::close);
Qt5的连接方式
// Qt5的信号槽连接方式
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);
Qt4的连接方式
这种旧的信号槽连接方式在Qt5中是支持的, 但是不推荐使用, 因为这种方式在进行信号槽连接的时候, 信号槽函数通过宏SIGNAL和SLOT转换为字符串类型。 因为信号槽函数的转换是通过宏来进行转换的,因此传递到宏函数内部的数据不会被进行检测, 如果使用者传错了数据,编译器也不会报错,但实际上信号槽的连接已经不对了,只有在程序运行起来之后才能发现问题,而且问题不容易被定位。
// 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, ...)));
应用举例
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 me;
// Qt4处理方式 注意不要把信号与槽的名字写错了,因为是转为字符串写错了不会报错,但是连接会失败
connect(&me, SIGNAL(eat()), &me, SLOT(hungury()));
connect(&me, SIGNAL(eat(QString)), &me, SLOT(hungury(QString)));
// Qt5处理方式
connect(&me, &Me::eat, &me, &Me::hungury); // error:no matching member function for call to 'connect'
//信号
void (Me::*funchungury)() = &Me::hungury;
void (Me::*funchungury_QString)(QString) = &Me::hungury;
//槽
void (Me::*funceat)() = &Me::eat;
void (Me::*funceat_QString)(QString) = &Me::eat;
//有参连接
connect(me,funchungury_QString,me,funceat_QString);
//无参连接
connect(me,funchungury,me,funceat);
二,通过Qt提供的重载类(QOverload)解决
//有参连接
connect(this,QOverload::of(&MyButton::hungury),this,QOverload::of(&MyButton::eat));
//无参连接
connect(this,QOverload<>::of(&MyButton::hungury),this,QOverload<>::of(&MyButton::eat));
总结
Qt4的信号槽连接方式因为使用了宏函数, 宏函数对用户传递的信号槽不会做错误检测, 容易出bug
Qt5的信号槽连接方式, 传递的是信号槽函数的地址, 编译器会做错误检测, 减少了bug的产生
当信号槽函数被重载之后, Qt4的信号槽连接方式不受影响
当信号槽函数被重载之后, Qt5中需要给被重载的信号或者槽定义函数指针
Qt框架提供的信号槽在某些特定场景下是无法满足我们的项目需求的,因此我们还设计自己需要的的信号和槽,同样还是使用connect()对自定义的信号槽进行连接。
如果想要使用自定义的信号和槽, 首先要编写新的类并且让其继承Qt的某些标准类,我们自己编写的类想要在Qt中使用使用信号槽机制, 那么必须要满足的如下条件:
// 在头文件派生类的时候,首先像下面那样引入Q_OBJECT宏:
class MyMainWindow : public QWidget
{
Q_OBJECT
public:
......
}
emit mysignals(); //发送信号
emit是一个空宏,没有特殊含义,仅用来表示这个语句是发射一个信号,不写当然可以,但是不推荐。
// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class MyButton : public QPushButton
{
Q_OBJECT
signals:
void testsignal();
void testsignal(int a);
};
//qRegisterMetaType
信号参数的作用是数据传递, 谁调用信号函数谁就指定实参,实参最终会被传递给槽函数
槽函数就是信号的处理动作,自定义槽函数和自定义的普通函数写法是一样的。
特点:
槽函数的类型:
场景举例
// 女朋友饿了, 我请她吃饭
// class GirlFriend
// class OneSelf
class GirlFriend:public QObject
{
Q_OBJECT
public:
GirlFriend(QObject*parent = nullptr):QObject(parent)
{}
signals:
void hungry();
public slots:
};
class OneSelf:public QObject
{
Q_OBJECT
public:
OneSelf(QObject*parent = nullptr):QObject(parent)
{}
void goEat()
{
qDebug()<<"goEat";
}
static void goEatFood()
{
qDebug()<<"goEatFood";
}
public slots:
void onHungry()
{
qDebug()<<"宝贝饿了呀,多喝热水哟~";
}
};
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
public slots:
void onBtnClicked();
private:
GirlFriend *girl;
OneSelf* self;
};
widget.cpp
#include "widget.h"
#include
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
girl = new GirlFriend(this);
self = new OneSelf(this);
//连接槽函数
connect(girl,&GirlFriend::hungry,self,&OneSelf::onHungry);
//连接普通成员函数
connect(girl,&GirlFriend::hungry,self,&OneSelf::goEat);
//连接静态成员函数
connect(girl,&GirlFriend::hungry,self,&OneSelf::goEatFood);
QPushButton*btn = new QPushButton("按下就饿了",this);
//通过widget间接发送girl的hungry信号
connect(btn,&QPushButton::clicked,this,&Widget::onBtnClicked);
//连接信号,直接发送girl的hungry信号
//connect(btn,&QPushButton::clicked,girl,&GirlFriend::hungry);
}
Widget::~Widget()
{
}
void Widget::onBtnClicked()
{
emit girl->hungry();
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_pushButton_clicked();
void myslot1(QString); //定义私有槽
void myslot2(QString); //定义私有槽
void myslot3(QString); //定义私有槽
private:
Ui::MainWindow *ui;
};
//公有继承自 QObject
class MyClass : public QObject
{
Q_OBJECT //声明宏,为了可以使用信号槽机制
public:
MyClass(){}
~MyClass(){}
signals:
void mysignal(QString); //定义信号(信号全部是公有的)
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
MyClass myclass; //MyClass实例化
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//信号槽连接(连接顺序:slot2, slot1, slot3)
connect(&myclass,SIGNAL(mysignal(QString)), this,SLOT(myslot2(QString)),Qt::UniqueConnection);
connect(&myclass,SIGNAL(mysignal(QString)), this,SLOT(myslot1(QString)),Qt::UniqueConnection);
connect(&myclass,SIGNAL(mysignal(QString)), this,SLOT(myslot3(QString)),Qt::UniqueConnection);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
//触发信号(推荐在本类触发信号,这里只是为了演示目的)
emit myclass.mysignal("这是一条来自myclass对象的信号");
}
//槽的实现
void MainWindow::myslot1(QString str)
{
qDebug()<<"slot1:"<
运行结果如下:
得出结论: 槽函数的执行顺序和信号槽连接的顺序一致
多个信号对应一个槽时,如何在槽函数中判断发出者?
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_pushButton_clicked();
void myslot(QString); //定义私有槽
private:
Ui::MainWindow *ui;
};
//公有继承自 QObject
class MyClass : public QObject
{
Q_OBJECT //声明宏,为了可以使用信号槽机制
public:
MyClass(){}
~MyClass(){}
signals:
void mysignal(QString); //定义信号(信号全部是公有的)
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
MyClass myclass1; //MyClass实例化1
MyClass myclass2; //MyClass实例化2
MyClass myclass3; //MyClass实例化3
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
myclass1.setObjectName("myclass1");
myclass2.setObjectName("myclass2");
myclass3.setObjectName("myclass3");
//信号槽连接
connect(&myclass1,SIGNAL(mysignal(QString)), this,SLOT(myslot(QString)),Qt::UniqueConnection);
connect(&myclass2,SIGNAL(mysignal(QString)), this,SLOT(myslot(QString)),Qt::UniqueConnection);
connect(&myclass3,SIGNAL(mysignal(QString)), this,SLOT(myslot(QString)),Qt::UniqueConnection);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
//触发信号(推荐在本类触发信号,这里只是为了演示目的)
emit myclass1.mysignal("这是一条测试信号");
emit myclass2.mysignal("这是一条测试信号");
emit myclass3.mysignal("这是一条测试信号");
}
//槽的实现
void MainWindow::myslot(QString str)
{
qDebug()<<"from "<objectName()<<""<
运行结果如下:
原理:首先利用 QObject::setObjectName(const QString&) 方法设置信号发出者的对象名称,
然后在槽函数中利用 QObject::sender()->objectName() 方法获取信号发出者的对象名称。
void MainWindow::onClicked()
{
qDebug()<<"btn2 onClicked";
}
void MainWindow::test2()
{
QPushButton*btn = new QPushButton("btn",this);
QPushButton*btn2 = new QPushButton("btn2",this);
btn2->move(100,0);
connect(btn, &QPushButton::clicked, btn2, &QPushButton::clicked);//点击btn按钮,会让btn2按钮发出clicked信号
connect(btn2, &QPushButton::clicked, this, &MainWindow::onClicked);//点击btn2按钮,会执行onClicked()槽函数
}
disconnect(const QObject *sender,
&QObject::signal,
const QObject *receiver,
&QObject::method);
disconnect(btn2, &QPushButton::clicked, this, &MainWindow::onClicked);
Lambda表达式就是一个匿名函数, 语法格式如下:
[capture](params) opt -> ret {body;};
- capture: 捕获列表
- params: 参数列表
- opt: 函数选项
- ret: 返回值类型
- body: 函数体
// 示例代码->匿名函数的调用:
int ret = [](int a) -> int
{
return a+1;
}(100);
示例:
QPushButton*btn = new QPushButton("touch me",this);
QPushButton*btn2 = new QPushButton("天王盖地虎",this);
btn2->move(100,0);
//禁止用&引用捕获临时变量,因为函数结束变量会销毁,在lambda中使用会产生错误
//应该使用按值捕获 =
connect(btn,&QPushButton::clicked,this,[=]()
{
static int flag = false; //可以这样用
if(!flag)
{
btn2->setText("小鸡顿蘑菇");
}else
{
btn2->setText("天王盖地虎");
}
flag = !flag;
});
关于Lambda表达式的细节介绍:
本文福利, 免费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,Qt编程入门,QT信号与槽机制,QT图像绘制,QT网络,QT数据库编程,QT项目实战,QT嵌入式开发,Quick模块等等)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓