Hello Qt(十三)——QT信号与槽机制

一、QT消息模型

QT封装了具体操作系统的消息机制,遵循经典的GUI消息驱动事件模型。

QT定义了与操作系统消息相关的自己的概念,即信号与槽。

当某个事件发生后,某个QObject对象会检测到事件并发出一个信号(signal),信号的发出是没有目的的;如果某个对象对发出的信号感兴趣,会使用连接(connect)函数,用自己的一个函数(slot)来处理这个信号。当信号发出时,被连接的槽函数会自动被回调。

信号signal是由操作系统产生的消息。

槽slot是程序中的消息处理函数。

connect将系统消息绑定到消息处理函数。

信号到槽的连接必须发生在两个QT对象间。

bool QObject::connect ( const QObject * sender, //发生对象
                        const char * signal, //消息名
                        const QObject * receiver, //接收对象
                        const char *method,  //接收对象的成员函数
                        Qt::ConnectionType type = Qt::AutoConnection )

在QT中消息使用字符串进行描述,connect函数在消息名和处理函数之间建立映射。

QT中的关键字

SIGNAL 用于指定消息名

SLOT 用于指定消息处理函数名

Q_OBJECT 所以自定义槽的类必须在类声明的开始处加上Q_OBJECT

slots 用于在类中声明消息处理函数

二、信号与槽机制

信号和槽机制是QT的核心机制,是一种高级接口,应用于QT对象之间的通信,是QT的核心特性,也是QT区别于其它工具包的重要地方。信号和槽是QT自行定义的一种通信机制,独立于标准的C/C++语言,要正确的处理信号和槽,必须借助一个称为moc(Meta Object Compiler)的QT工具,MOC工具是一个C++预处理程序,为高层次的事件处理自动生成所需要的附加代码。

在QT中信号和槽取代了传统GUI框架中的回调函数,信号和槽能携带任意数量和任意类型的参数,是类型完全安全的。所有从QObject或其子类(如Qwidget)派生的类都能够包含信号和槽。当对象改变其状态时,信号就由对象发射(emit)出去,但对象不知道另一端是谁在接收信号。槽用于接收信号,但槽是普通的对象成员函数。一个槽并不知道是否有任何信号与自己相连接,而且对象并不了解具体的通信机制。

1、信号

信号是一个特殊的成员函数声明,返回值类型为void,只能声明不能定义实现。信号必须用signals关键字声明,访问属性为protected,只能通过emit关键字调用(发射信号)。

当某个信号对其客户或所有者发生的内部状态发生改变,信号被一个对象发射。只有定义过这个信号的类及其派生类能够发射这个信号。当一个信号被发射时,与其相关联的槽将被立刻执行,就象一个正常的函数调用一样。信号-槽机制完全独立于任何GUI事件循环。只有当所有的槽返回以后发射函数(emit)才返回。 如果存在多个槽与某个信号相关联,当这个信号被发射时,这些槽将会一个接一个地执行,但执行的顺序将会是随机的,不能人为地指定哪个先执行、哪个后执行。

信号的声明是在头文件中进行的,QT的signals关键字指出进入了信号声明区,随后即可声明自己的信号。

signals:
     void overflow();

signals是QT的关键字,而非C/C++的。信号可以重载,但信号却没有函数体定义,并且信号的返回类型都是void,不要指望能从信号返回什么有用信息。

信号由moc自动产生,不应该在.cpp文件中实现。

2、槽

槽是普通的C++成员函数,可以被正常调用,唯一的特殊性就是很多信号可以与其相关联。当与其关联的信号被发射时,信号关联的槽就会被调用。槽可以有参数,但槽的参数不能有缺省值。

槽是普通的成员函数,也有访问权限。槽的访问权限决定了谁能够与其相关联。同普通的C++成员函数一样,槽函数也分为三种类型,即public slots、private slots和protected slots。

public slots:使用public slots声明的槽表示任何对象都可将信号与之相连接。在组件编程中,可以创建彼此互不了解的对象,将它们的信号与槽进行连接以便信息能够正确的传递。

protected slots:使用protected slots声明的槽表示当前类及其子类可以将信号与之相连接。适用于那些槽,它们是类实现的一部分,但是其界面接口却面向外部。

private slots:使用private slots声明的槽表示只有类自己可以将信号与之相连接,适用于联系非常紧密的类。

槽也能够声明为虚函数。

槽的声明也是在头文件中进行的。

public slots:
     void setValue(int value);

自定义槽

只有QObject的子类才能自定义槽

定义槽的类必须在类声明的最开始处使用Q_OBJECT

类中声明槽是需要使用slots关键字

槽与所处理的信号在函数签名上必须一致

SIGNAL与SLOT指定的名称中可以包含参数类型,不能包含具体的参数名

错误Object::connect:No such slot

A、检查类对象是否继承自QObject

B、检查类声明的开始处是否添加Q_OBJECT

C、检查是否使用slots对槽进行声明

D、检查槽的名称拼写是否错误

E、重新编译工程

3、信号与槽的关联

一个信号可以连接到多个槽函数,多个信号可以连接到以个槽函数,一个信号可以连接到另一个信号。

通过调用QObject对象的connect函数来将某个对象的信号与另外一个对象的槽函数相关联,当发射者对象发射信号时,接收者对象的槽函数将被调用。connect函数的定义如下:

bool QObject::connect ( const QObject * sender, const char * signal, 
                        const QObject * receiver, const char *method, 
                        Qt::ConnectionType type = Qt::AutoConnection ) 

connect函数的作用就是将发射者sender对象中的信号signal与接收者receiver中的method槽函数联系起来。当指定信号signal时必须使用QT的宏SIGNAL(),当指定槽函数时必须使用宏SLOT()。如果发射者与接收者属于同一个对象的话,那么在connect 调用中接收者参数可以省略。

一个信号能够与另一个信号相关联,此时发射者发出信号后接收者的信号也会接着发射。

当信号与槽没有必要继续保持关联时,可以使用disconnect函数来断开连接。

bool QObject::disconnect ( const QObject * sender, const char * signal, 
                            const QObject * receiver, const char *method ) 

disconnect函数断开发射者中的信号与接收者中的槽函数之间的关联。

在disconnect函数中0可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数。但是发射者sender不能为0,其它三个参数的值可以等于0。

以下三种情况需要使用disconnect()函数断开信号与槽的关联:

A、断开与某个对象相关联的任何对象

disconnect(sender, 0, 0, 0);
sender->disconnect();

B、断开与某个特定信号的任何关联

disconnect(sender, SIGNAL(mySignal()), 0, 0);
sender->disconnect(SIGNAL(mySignal()));

C、断开两个对象之间的关联

disconnect(sender, 0, receiver, 0);
sender->disconnect(receiver);

Qt利用信号与槽(signals/slots)机制取代传统的callback来进行对象之间的沟通。当操作事件发生的时候,对象会发提交一个信号(signal);而槽(slot)则是一个函数接受特定信号并且运行槽本身设置的动作。信号与槽之间,则通过QObjectQObjectQObject的静态方法connect来链接。

信号在任何运行点上皆可发射,甚至可以在槽里再发射另一个信号,信号与槽的链接不限定为一对一的链接,一个信号可以链接到多个槽或多个信号链接到同一个槽,甚至信号也可连接到信号。

以往的callback缺乏类型安全,在调用处理函数时,无法确定是传递正确型态的参数。但信号和其接受的槽之间传递的数据型态必须要相符合,否则编译器会提出警告。信号和槽可接受任何数量、任何型态的参数,所以信号与槽机制是完全类型安全。

信号与槽机制也确保了低耦合性,发送信号的类的并不知道是哪个槽会接受,也就是说一个信号可以调用所有可用的槽。此机制会确保当在"连接"信号和槽时,槽会接受信号的参数并且正确运行。

4、元对象工具

元对象编译器moc(meta object compiler)对C++文件中的类声明进行分析并产生用于初始化元对象的C++代码,元对象包含全部信号和槽的名字以及指向槽函数的指针。

moc读C++源文件,如果发现有Q_OBJECT宏声明的类,就会生成另外一个C++源文件,新生成的文件中包含有该类的元对象代码。例如,假设我们有一个头文件mysignal.h,在这个文件中包含有信号或槽的声明,那么在编译之前 moc 工具就会根据该文件自动生成一个名为mysignal.moc.h的C++源文件并将其提交给编译器;对应于mysignal.cpp文件moc工具将自动生成一个名为mysignal.moc.cpp文件提交给编译器。

元对象代码是signal/slot机制所必须的。用moc产生的C++源文件必须与类实现一起进行编译和连接,或者用#include语句将其包含到类的源文件中。moc并不扩展#include或者#define宏定义,只是简单的跳过所遇到的任何预处理指令。

信号和槽函数的声明一般位于头文件中,同时在类声明的开始位置必须加上Q_OBJECT语句,Q_OBJECT语句将告诉编译器在编译之前必须先应用moc工具进行扩展。关键字signals是对信号的声明,siganls默认为protected等属性,slots是对槽函数的声明,slots有public、private、protected等属性。signals、slots关键字是QT 自己定义的,不是C++中的关键字。

信号的声明类似于函数的声明而非变量的声明,左边要有类型,右边要有括号,如果要向槽中传递参数的话,在括号中指定每个形式参数的类型,当然,形式参数的个数可以多于一个。

关键字slots指出随后开始槽的声明,这里slots用的也是复数形式。

槽的声明与普通函数的声明一样,可以携带零或多个形式参数。既然信号的声明类似于普通C++函数的声明,那么,信号也可采用C++中虚函数的形式进 行声明,即同名但参数不同。例如,第一次定义的void mySignal()没有带参数,而第二次定义的却带有参数,从这里我们可以看到QT的信号机制是非常灵活的。

信号与槽之间的联系必须事先用connect函数进行指定。如果要断开二者之间的联系,可以使用函数disconnect。

5、信号与槽机制的局限

信号与槽是一种高效灵活的通信机制,但有其缺陷:

A、信号与槽的非常高效的,但是与回调函数相比,由于增加了灵活性,因此在速度上有所损失,这种损失相对来说是比较小的,通过在一台i586-133的机器上测试是10微秒(运行Linux),可见这种机制所提供的简洁性、灵活性还是值得的。一般来说,在实时系统中就要尽可能的少用信号与槽机制。

B、信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能产生死循环。因此,在定义槽函数时一定要避免在槽函数中再次发射所接收到的同样信号。

C、如果一个信号与多个槽相关联,那么当这个信号被发射时,与之相关的槽函数被调用的顺序将是随机的。

D、宏定义不能用在信号和槽的参数中。

moc工具不扩展#define,在信号和槽的参数中使用宏将不能正确地工作,不带参数是可以的。

E、构造函数不能用在signals或者slots声明区域内。

F、函数指针不能作为信号或槽的参数。

函数指针作为参数是不合语法,但可以使用typedef将函数指针类型重命名,使用函数指针类型作为参数是合语法的。

G、信号与槽不能有缺省参数。

H、信号与槽也不能使用模板类参数

可以使用typedef重命名模板类,重命名后的类型名可以作为信号与槽的参数

I、嵌套的类不能位于信号或槽区域内,也不能有信号或者槽。

J、友元声明不能位于信号或者槽声明区内,应该在普通C++的private、protected或者public区内进行声明。

6、信号与槽的使用规定

A、QT类只能在头文件中声明

B、信号与槽的原型应该完全相同

C、信号参数多余槽参数时多余参数被忽略

D、槽函数的返回值必须是void

E、槽函数可以像普通成员函数一样使用

F、信号和槽的访问属性对于connect/disconnect函数无效

三、自定义信号槽

reader.h文件:

#ifndef READER_H
#define READER_H

#include <QObject>
#include <QDebug>

class Reader : public QObject
{
    Q_OBJECT
public:
    Reader() {}
    void receiveNewspaper(const QString & name) const
    {
        qDebug() << "Receives Newspaper: " << name;
    }
};

#endif // READER_H

newspaper.h文件:

#ifndef NEWSPAPER_H
#define NEWSPAPER_H

#include <QObject>

class Newspaper : public QObject
{
    Q_OBJECT
public:
    Newspaper(const QString & name):m_name(name)
    {

    }
    void send() const
    {
        emit newPaper(m_name);
    }
signals:
    void newPaper(const QString &name) const;

private:
    QString m_name;
};

#endif // NEWSPAPER_H

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();
}

main()函数中,connect()函数将newspaper对象的newPaper()信号与reader对象的receiveNewspaper()槽函数连接。当newspaper发出newPaper()信号时,reader相应的槽函数就会自动被调用。QT5支持使用函数指针使用信号与槽,connect函数使用Newspaper::newPaper()信号的地址,Reader::receiveNewspaper()槽函数地址。编译器能够利用信号与槽的地址,在编译期对信号与槽的连接操作进行检查,如果有任何错误(包括对象没有这个信号,或者信号参数不匹配等),编译时就会发现。

同时如果有重载的信号或槽函数如:

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

可以使用如下:

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

QT5中引入了信号槽的新语法:使用函数指针能够获得编译期的类型检查。

QT4的信号槽本质上是字符串匹配,在处理时会忽略参数默认值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值