C++ ——Qt的信号和槽的详解

1、概述

信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt 的信号槽使用了额外的处理来实现,并不是 GoF 经典的观察者模式的实现方式。)

信号和槽是Qt特有的信息传输机制,是Qt设计程序的重要基础,它可以让互不干扰的对象建立一种联系。

槽的本质是类的成员函数,其参数可以是任意类型的。和普通C++成员函数几乎没有区别,它可以是虚函数;也可以被重载;可以是公有的、保护的、私有的、也可以被其他C++成员函数调用。唯一区别的是:槽可以与信号连接在一起,每当和槽连接的信号被发射的时候,就会调用这个槽。

1.1对象树(子对象动态分配空间不需要释放)

参考连接:https://blog.csdn.net/fzu_dianzi/article/details/6949081

Qt提供了一种机制,能够自动、有效的组织和管理继承自QObject的Qt对象,这种机制就是对象树。

Qt对象树在用户界面编程上是非常有用的。它能够帮助程序员减轻内存泄露的压力。

比如说当应用程序创建了一个具有父窗口部件的对象时,该对象将被加入父窗口部件的孩子列表。当应用程序销毁父窗口部件时,其下的孩子列表中的对象将被一一删除。这让我们在编程时,能够将主要精力放在系统的业务上,提高编程效率,同时也保证了系统的稳健性。

下面笔者将简单分析对象树。

代码验证:

int main(int argc, char *argv[])

{

    QApplication app(argc, argv);

    QDialog *dlg = new QDialog(0);

    QPushButton *btn = new QPushButton(dlg);

    qDebug() << "dlg = " << dlg;

    qDebug() << "btn = " << btn;

    dlg->exec();    delete btn;

    qDebug() << "dlg = " << dlg;    return 0;

}

dlg = QDialog(0x3ea1a0) 

btn = QPushButton(0x3ea228)/*关闭窗口后,dlg = QDialog(0x3ea1a0)

这说明关闭窗口,不会销毁该窗口部件,而是将其隐藏起来。

我们在qDebug() << "dlg = " << dlg;

之后加上

qDebug() << "btn = " << btn;

明显的,我们之前已经delete btn,btn指针没有被赋值为0,这是编译器决定的。

执行程序后,必然出现段错误。

2、

将程序稍微修改下。*/int main(int argc, char *argv[])

{

    QApplication app(argc, argv);

    QDialog *dlg = new QDialog(0);

    QPushButton *btn = new QPushButton(dlg);

    qDebug() << "dlg = " << dlg;

    qDebug() << "btn = " << btn;

    dlg->exec();    delete dlg;

    qDebug() << "btn = " << btn;  

  return 0;

}

2、信号和槽

为了体验一下信号槽的使用,我们以一段简单的代码说明:

Qt5 的书写方式:(推荐的使用)★★★★★

#include <QApplication>

#include <QPushButton>

 int main(int argc, char *argv[])

{

    QApplication app(argc, argv);

    QPushButton button("Quit");

QObject::connect(&button, &QPushButton::clicked,&app, &QApplication::quit);

    button.show();    

return app.exec();

}

我们按照前面文章中介绍的在 Qt Creator 中创建工程的方法创建好工程,然后将main()函数修改为上面的代码。点击运行,我们会看到一个按钮,上面有“Quit”字样。点击按钮,程序退出。

connect()函数最常用的一般形式:

connect(sender, signal, receiver, slot);

参数:

 sender:发出信号的对象

 signal:发送对象发出的信号

 receiver:接收信号的对象

 slot:接收对象在接收到信号之后所需要调用的函数

信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。

如果信号槽不符合,或者根本找不到这个信号或者槽函数,比如我们改成:

connect(&button, &QPushButton::clicked, &QApplication::quit2);

由于 QApplication 没有 quit2 这样的函数,因此在编译时会有编译错误:

'quit2' is not a member of QApplication

这样,使用成员函数指针我们就不会担心在编写信号槽的时候出现函数错误。

Qt4 的书写方式:

int main(int argc, char *argv[])

        QApplication a(argc, argv); 

        QPushButton *button = new QPushButton("Quit"); 

        connect(button, SIGNAL(clicked()), &a, SLOT(quit())); 

        button->show(); 

        return a.exec(); 

}

这里使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。注意到connect()函数的 signal 和 slot 都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。

Qt5在语法上完全兼容Qt4

小总结:

1>. 格式: connect(信号发出者对象(指针), &className::clicked, 信号接收者对象(指针), &classB::slot);

2>. 标准信号槽的使用:

connect(sender, &Send::signal, receiver, &Receiver::slot)

3、自定义信号槽

使用connect()可以让我们连接系统提供的信号和槽。但是,Qt 的信号槽机制并不仅仅是使用系统提供的那部分,还会允许我们自己设计自己的信号和槽。

下面我们看看使用 Qt 的信号槽,实现一个报纸和订阅者的例子:

有一个报纸类Newspaper,有一个订阅者类Subscriber。Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。

#include <QObject>

 ////////// newspaper.h //////////

class Newspaper : public QObject

{

    Q_OBJECTpublic:

    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_OBJECTpublic:

    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这个类。这个类继承了QObject类。只有继承了QObject类的类,才具有信号槽的能力。所以,为了使用信号槽,必须继承QObject。凡是QObject类(不管是直接子类还是间接子类),都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽,都应该添加这个宏。这个宏的展开将为我们的类提供信号槽机制、国际化机制以及 Qt 提供的不基于 C++ RTTI 的反射能力。

● Newspaper类的 public 和 private 代码块都比较简单,只不过它新加了一个 signals。signals 块所列出的,就是该类的信号。信号就是一个个的函数名,返回值是 void(因为无法获得信号的返回值,所以也就无需返回任何值),参数是该类需要让外界知道的数据。信号作为函数名,不需要在 cpp 函数中添加任何实现。

●Newspaper类的send()函数比较简单,只有一个语句emit newPaper(m_name);。emit 是 Qt 对 C++ 的扩展,是一个关键字(其实也是一个宏)。emit 的含义是发出,也就是发出newPaper()信号。感兴趣的接收者会关注这个信号,可能还需要知道是哪份报纸发出的信号?所以,我们将实际的报纸名字m_name当做参数传给这个信号。当接收者连接这个信号时,就可以通过槽函数获得实际值。这样就完成了数据从发出者到接收者的一个转移。

● Reader类更简单。因为这个类需要接受信号,所以我们将其继承了QObject,并且添加了Q_OBJECT宏。后面则是默认构造函数和一个普通的成员函数。Qt 5 中,任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。与信号函数不同,槽函数必须自己完成实现代码。槽函数就是普通的成员函数,因此作为成员函数,也会受到 public、private 等访问控制符的影响。(如果信号是 private 的,这个信号就不能在类的外面连接,也就没有任何意义。)

3.1自定义信号槽需要注意的事项

●发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);

●使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;

●槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;

●使用 emit 在恰当的位置发送信号;

●使用QObject::connect()函数连接信号和槽。

●任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数

3.2信号槽的更多用法

一个信号可以和多个槽相连

  如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。

多个信号可以连接到一个槽

  只要任意一个信号发出,这个槽就会被调用

●一个信号可以连接到另外的一个信号

  当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。

●槽可以被取消链接

  这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。

●使用Lambda 表达式

  在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。

  我们的代码可以写成下面这样:

QObject::connect(&newspaper, static_cast

(const QString &)>(&Newspaper::newPaper),

[=](const QString &name) 

{ /* Your code here. */ }

);

在连接信号和槽的时候,槽函数可以使用Lambda表达式的方式进行处理。

4、Lambda表达式

C++11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。首先看一下Lambda表达式的基本构成:

   [函数对象参数](操作符重载函数参数)mutable或exception ->返回值{函数体}

 ①函数对象参数;

[],标识一个Lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:

     ▲空。没有使用任何函数对象参数。

▲=。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。

▲&。函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。

     ▲ this。函数体内可以使用Lambda所在类中的成员变量。

     ▲ a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。

     ▲ &a。将a按引用进行传递。

       ▲ a, &b。将a按值进行传递,b按引用进行传递。

     ▲ =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。

     ▲ &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。

int m = 0, n = 0;

[=] (int a) mutable { m = ++n + a; }(4);

      [&] (int a) { m = ++n + a; }(4);

      [=,&m] (int a) mutable { m = ++n + a; }(4);

      [&,m] (int a) mutable { m = ++n + a; }(4);

      [m,n] (int a) mutable { m = ++n + a; }(4);

      [&m,&n] (int a) { m = ++n + a; }(4);

② 操作符重载函数参数;

标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。

③ 可修改标示符;

mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。

④ 错误抛出标示符;

exception声明,这部分也可以省略。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)

⑤ 函数返回值;

->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

⑥ 是函数体;

{},标识函数的实现,这部分不能省略,但函数体可以为空。

一个好的学习环境能营造出一种好的学习氛围,大家互相讨论,亦师亦友,为了同一个梦想前进,这是一件浪漫并且热血的事,如果你是C/C++的爱好者,喜欢或者想要学习,那么一个学习基地适合你,欢迎每一位想要学习、学好C/C++的朋友。(见评论区),大量资源、干货、大佬离你更近。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容

  • 为什么在头文件中有的是使用前置声明,而有的是包含头文件? 如下代码: 前置声明(forward declarati...
    Joe_HUST阅读 1,286评论 0 6
  • 信号和槽(Signals and Slots) Qt库第一个认识到在几乎所有情况下,程序员不需要或甚至不想知道所有...
    珞珈村下山阅读 9,832评论 0 23
  • 信号和槽是Qt特有的信息传输驱动机制,是Qt程序设计中的重要基础,它可以让程序员在互不相干的对象之间建立联系。槽本...
    NiceBlueChai阅读 1,132评论 0 5
  • 转自:作者简介作者:唐新华 (xhsmart@263.net)软件工程师    信号和槽作为QT的核心机制在QT编...
    njukay阅读 1,338评论 0 49
  • 我一直在想作为姐姐我能为她做些什么 我以为我足够勇敢可以让爱的人不受苦 然而天灾人祸总是那么猝不及防 所以无论生活...
    张瑞201612阅读 198评论 0 0