[学习][笔记] qt5 从入门到入坟:<二>信号与槽

简介: [学习][笔记] qt5 从入门到入坟:<二>信号与槽

信号与槽 signals and slots

详细介绍

所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,用自己的一个函数(成为槽(slot))来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt

的信号槽使用了额外的处理来实现,并不是 GoF 经典的观察者模式的实现方式。)

实践

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
//    MainWindow w;
//    w.setWindowTitle("signal_and_slot");
    QPushButton button("Quit");
    QObject::connect(&button, &QPushButton::clicked,
    &app, &QApplication::quit);
  /*qt5使用lambda表达式写法*/
    QObject::connect(&button, &QPushButton::clicked,
    [](bool) {
        qDebug() << "You clicked me!";
    });
    button.show();
//    w.show();
    return app.exec();
}

解释

将按钮button的QPushButton::clicked 事件(信号)跟应用app对象的QApplication::quit事件(槽)绑定在一块,按钮的点击->应用退出

形式

以qt5中 QObject::connect()为例子

// !!! Qt 5

connect(sender, signal,

receiver, slot);

这是我们最常用的形式。connect()一般会使用前面四个参数,第一个是发出信号的对象,第二个是发送对象发出的信号,第三个是接收信号的对象,第四个是接收对象在接收到信号之后所需要调用的函数。也就是说,当

sender 发出了 signal 信号之后,会自动调用 receiver 的 slot 函数。

qt5

QMetaObject::Connection connect(const QObject *, const char *,
                                const QObject *, const char *,
                                Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
                                const QObject *, const QMetaMethod &,
                                Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, const char *,
                                const char *,
                                Qt::ConnectionType) const;
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
                                const QObject *, PointerToMemberFunction,
                                Qt::ConnectionType)
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
                                Functor);

第一个,receiver 类型是const QObject *,slot 类型是const char *。这个函数将 signal 和 slot作为字符串处理。

第二个,sender 和 receiver 同样是const QObject *,但是 signal 和 slot都是const QMetaMethod&。我们可以将每个函数看做是QMetaMethod的子类。因此,这种写法可以使用QMetaMethod进行类型比对。

第三个,sender同样是const QObject *,signal 和 slot 同样是const char *,但是却缺少了receiver 这个函数其实是将 this 指针作为 receiver。

第四个,sender 和 receiver也都存在,都是const QObject *,但是 signal 和 slot类型则是PointerToMemberFunction。看这个名字就应该知道,这是指向成员函数的指针。

第五个,前面两个参数没有什么不同,最后一个参数是Functor类型。这个类型可以接受static 函数、全局函数以及 Lambda 表达式。

Qt 4 的connect()函数与 Qt 5 最大的区别在于,Qt 4 的 signal 和 slot 只有const char *这么一种形式。

bool connect(const QObject *, const char *,
             const QObject *, const char *,
             Qt::ConnectionType);
bool connect(const QObject *, const QMetaMethod &,
             const QObject *, const QMetaMethod &,
             Qt::ConnectionType);
bool connect(const QObject *, const char *,
             const char *,
             Qt::ConnectionType) const

如果我们将上面的代码修改为 Qt 4 的,则应该是这样的:

QObject::connect(&button, SIGNAL(clicked()),
                         &app,    SLOT(quit()));

我们使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。注意,即使quit()是QApplication的 static

函数,也必须传入一个对象指针。这也是 Qt 4 的信号槽语法的局限之处。另外,注意到connect()函数的 signal 和 slot

都是接受字符串,因此,不能将全局函数或者 Lambda 表达式传入connect()。一旦出现连接不成功的情况,Qt 4

是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。

代码:信号槽

自定义信号槽

信号槽不是 GUI 模块提供的,而是 Qt 核心特性之一。因此,我们可以在普通的控制台程序使用信号槽。

经典的观察者模式在讲解举例的时候通常会举报纸和订阅者的例子。有一个报纸类Newspaper,有一个订阅者类Subscriber。Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。在这个例子中,观察者是Subscriber,被观察者是Newspaper。在经典的实现代码中,观察者会将自身注册到被观察者的一个容器中(比如subscriber.registerTo(newspaper))。被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者(newspaper.notifyAllSubscribers())。

实践

//!!! Qt5
#include <QObject>
// newspaper.h
class Newspaper : public QObject
{
    Q_OBJECT
public:
    Newspaper(const QString & name) :
        m_name(name)
    {
    }
    void send()
    {
        emit newPaper(m_name);
    }
signals:
    void newPaper(const QString &name);
private:
    QString m_name;
};
// reader.h
#include <QObject>
#include <QDebug>
class Reader : public QObject
{
    Q_OBJECT
public:
    Reader() {}
    void receiveNewspaper(const QString & name)
    {
        qDebug() << "Receives Newspaper: " << name;
    }
};
// main.cpp
#include <QCoreApplication>
#include "newspaper.h"
#include "reader.h"
int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    Newspaper newspaper("Newspaper A");
    Reader reader;
    QObject::connect(&newspaper, &Newspaper::newPaper,
                     &reader,    &Reader::receiveNewspaper);
    newspaper.send();
    return app.exec();
}

解释

Newspaper,Reader类 都继承QObject 才有信号槽能力 且 添加宏Q_OBJECT ,任何QObject都需要Q_OBJECT这个宏

发射方Newspaper 定义Signal 表示信号函数 信号函数newPaper无需 实现 emit 关键字表示发射 当send时候发射newPaper信号 受访问控制影响 信号函数不能定义为private,protect

接收方Reader 定义接受函数 receiveNewspaper

main()函数中,我们首先创建了Newspaper和Reader两个对象,然后使用QObject::connect()函数。这个函数我们上一节已经详细介绍过,这里应该能够看出这个连接的含义。然后我们调用Newspaper的send()函数。这个函数只有一个语句:发出信号。由于我们的连接,当这个信号发出时,自动调用 reader 的槽函数,打印出语句。

自定义信号槽 条件

  • 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
  • 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;
  • 槽函数是普通的成员函数,作为成员函数,会受到public、private、protected 的影响;
  • 使用 emit 在恰当的位置发送信号;
  • 使用QObject::connect()函数连接信号和槽。

信号槽qt4和qt5的区别

有重载的信号

如果信号有重载,比如我们向Newspaper类增加一个新的信号:

void newPaper(const QString &name, const QDate &date);

此时如果还是按照前面的写法,编译器会报出一个错误:由于这个函数(注意,信号实际也是一个普通的函数)有重载,因此不能用一个取址操作符获取其地址。

qt4 我们使用SIGNAL和SLOT两个宏来连接信号槽。

QObject::connect(&newspaper, SIGNAL(newPaper(QString, QDate)),
                 &reader,    SLOT(receiveNewspaper(QString, QDate)));

在 Qt 4 中不存在我们所说的错误,因为 Qt 4 的信号槽连接是带有参数的。

归根结底,这个错误是因为函数重载,编译器不知道要取哪一个函数的地址,而我们显式指明一个函数就可以了。

Qt5 解决方案

显式指明调用的函数

void (Newspaper:: *newPaperNameDate)(const QString &, const QDate &) = 
&Newspaper::newPaper;
QObject::connect(&newspaper, newPaperNameDate, 
      &reader,    &Reader::receiveNewspaper);

或者好看点写法 ,但是如果你改变了信号的类型,那么你就会有一个潜在的运行时错误。

QObject::connect(&newspaper,
                 (void (Newspaper:: *)(const QString &, const QDate &))&Newspaper::newPaper,
                 &reader,
                 &Reader::receiveNewspaper);

最正规的写法 静态转换 如果参数改变 会提示编译错误

QObject::connect(&newspaper,
                 static_cast<void (Newspaper:: *)(const QString &, const QDate &)>(&Newspaper::newPaper),
                 &reader,
                 &Reader::receiveNewspaper);

带有默认参数的槽函数

Qt

允许信号和槽的参数数目不一致:槽函数的参数数目可以比信号的参数少。这是因为,我们信号的参数实际是作为一种返回值。正如普通的函数调用一样,我们可以选择忽略函数返回值,但是不能使用一个并不存在的返回值。如果槽函数的参数数目比信号的多,在槽函数中就使用到这些参数的时候,实际这些参数并不存在(因为信号的参数比槽的少,因此并没有传过来),函数就会报错。这种情况往往有两个原因:一是槽的参数就是比信号的少,此时我们可以像前面那种写法直接连接。另外一个原因是,信号的参数带有默认值。比如

void QPushButton::clicked(bool checked = false)
槽函数的参数可以比信号的多
// Newspaper
signals:
    void newPaper(const QString &name);
// Reader
    void receiveNewspaper(const QString &name, const QDate &date = QDate::currentDate());

虽然Reader::receiveNewspaper()的参数数目比Newspaper::newPaper()多,但是由于Reader::receiveNewspaper()后面一个参数带有默认值,所以该参数不是必须提供的。但是,如果你按照前面的写法,比如如下的代码:

QObject::connect(&newspaper,
                 static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper),
                 &reader,
                 static_cast<void (Reader:: *)(const QString &, const QDate & =QDate::currentDate())>(&Reader::receiveNewspaper));

你会得到一个断言错误:

The slot requires more arguments than the signal provides.

我们不能在函数指针中使用函数参数的默认值。这是 C++ 语言的限制:参数默认值只能使用在直接地函数调用中。当使用函数指针取其地址的时候,默认参数是不可见的!

当然,此时你可以选择 Qt 4 的连接语法。如果你还是想使用 Qt 5 的新语法,目前的办法只有一个:Lambda 表达式。不要担心你的编译器不支持 Lambda 表达式,因为在你使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。于是,我们的代码就变成了:

QObject::connect(&newspaper,
                 static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper),
                 [=](const QString &name) { /* Your code here. */ });

代码:自定义信号槽

信号槽的实现

Qt 信号槽的实现

总结


相关文章
|
17天前
【Qt 学习笔记】按钮实现helloworld | 信号与槽概述
【Qt 学习笔记】按钮实现helloworld | 信号与槽概述
21 0
|
1月前
|
存储 安全 编译器
【Qt 底层机制之信号和槽 】深入探究Qt信号和槽背后的原理
【Qt 底层机制之信号和槽 】深入探究Qt信号和槽背后的原理
141 4
|
2月前
|
编译器 C++ 开发者
QT基础【7-跨进程发送信号】
QT基础【7-跨进程发送信号】
|
2月前
|
开发者
QT基础【6-跨界面发送信号】
QT基础【6-跨界面发送信号】
|
2月前
|
数据挖掘 C++
QT基础入门——项目案例(七)
QT基础入门——项目案例(七)
97 0
QT基础入门——项目案例(七)
|
2月前
|
API
QT基础入门——Qt事件(五)
QT基础入门——Qt事件(五)
54 0
QT基础入门——Qt事件(五)
|
2月前
|
Unix Java Linux
QT基础入门——认识与创建QT(一)
QT基础入门——认识与创建QT(一)
62 0
QT基础入门——认识与创建QT(一)
|
1月前
|
Linux 编译器 API
探索Qt图像处理的奥秘:从入门到精通
探索Qt图像处理的奥秘:从入门到精通
81 2
|
2月前
|
编译器 C++ 开发者
QT基础【5-信号与槽】
QT基础【5-信号与槽】
|
2月前
|
编解码
QT基础入门——文件操作(六)
QT基础入门——文件操作(六)
27 0
QT基础入门——文件操作(六)

热门文章

最新文章

推荐镜像

更多