Qt教程(3) : 信号与槽

​    信号与槽是用于对象之间的通信的,这是 Qt 的核心。为此 Qt 引入了一些关键字,他们是slots、signals、emit,这些都不是 C++关键字,是 Qt 特有的,这些关键字会被 Qt 的 moc转换为标准的 C++语句。

    Qt 的部件类中有一些已经定义好了的信号和槽,通常的作法是子类化部件类,然后添加自已的信号和槽。

    因为信号和槽与函数相似,所以通常把信号称为信号函数,槽称为槽函数。

 元对象是指用于描述另一个对象结构的对象。使用编程语言具体实现时,其实就是一个类的对象,只不过这个对象专门用于描述另一个对象而已,比如 class B{…}; class A{…B mb;…};假设 mb 是用来描述类 A 创建的对象的,则 mb 就是元对象。

一、信号与槽的原理

    C++虽然是面象对象的语言,但程序的具体实现代码仍然是由函数来实现的,因此所谓的对象之间的通信,从程序设计语言语法角度来看就是函数调用的问题,只不过是某个对象的成员函数调用另一个对象的成员函数而已。信号和槽其实是观察者模式的一种实现。

    当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。

void g() {
  //执行内容
}
void f() {
  //让g()执行的内容
}

    函数 f 需要 让g 执行,有以下几种处理方式:

    (1)最简单的方式就是直接调用函数 g,但这种方式有一个明显的缺点,必须知道函数 g的名称“g”以及函数 g 的参数类型。但是若 f 只需要 g 的处理结果就可以了,而 g的处理结果不一定需要函数 g 来完成,它也可以是 x、y 或其他函数来完成,那么这种直接调用函数的方式就无法胜任了,因为系统不知道用户会使用哪个函数来完成这种处理结果,也就是系统不知道调用的函数名究竟是 g、x 或其他名称。

    (2)另一种方式就是回调函数,即在函数 f 中使用一个指向函数的指针去调用需要的函数,这样就可以调用任意名称的函数(只要函数类型与指针相同即可),此时只要是完成了函数 g 功能的函数都可以作为函数 f 的结果被调用,这样就不会被函数名称所限制。比如

void (*p)(int i,int j); //假设这是系统内部代码
void g(int,int) {       //假设 g 是由程序员实现的代码
} 
void h(int,int) {       //原理同 g
} 
void f() {              //假设 f 也是系统内部源代码函数
  p=g;
  p(1,2);//调用g
  p=h;
  p(3,4);//调用h
}

    (3)Qt 使用的信号和槽机制:

        创建一个信号,其中创建信号需要一些规则。当需要调用外部函数时,发送一个信号,此时与该信号相关联的槽便会被调用,槽其实就是一个函数,当然要使函数成为槽是有一定规则的。槽与信号的关联需要由程序员来完成。在 Qt 中,信号和槽都需要位于一个类之中。

void x(int,int) {//信号
}
void g(int,int) {//槽
}
void h(int,int) {//槽
}
关联: (x, g);
关联: (x, h);
void f() {       //槽
  发送: x(1,2);   //会调用g,h
}

    几个关键字的原型:

  • signals 关键字:最终被#define 置换为一个访问控制符,其简化后为 #define signals public

  • slots 关键字:最终被#define 置换为一个空宏,即简化后为 #define slots

  • emit 关键字:同样被#define 置换为一个空宏,即简化后为 #define emit

    由以上各关键字原型可见,使用 emit 发射信号,其实就是一个简单的函数调用。

二、创建信号和槽

    只有 QObject 及其派生类才能使用信号和槽机制,且在类之中还需要使用 Q_OBJECT 宏。

    信号创建规则:

  • 信号使用 signals 关键字声明,在其后面有一个冒号“:”,在其前面不能有 public、private、protected 访问控制符,信号默认是 public 的。

  • 信号只需像函数那样声明即可,其中可以有参数,参数的主要作用是用于和槽的通信,这就像普通函数的参数传递规则一样。信号虽然像函数,但是对他的调用方式不一样,信号需要使用 emit 关键字发射。

  • 信号只需声明,不能对其进行定义,信号是由 moc 自动生成的。

  • 信号的返回值只能是 void 类型的。

    槽创建规则:

  • 声明槽需要使用 slots 关键字,在其后面有一个冒号“:”,且槽需使用 public、private、protected 访问控制符之一。

  • 槽就是一个普通的函数,可以像使用普通函数一样进行使用,槽与普通函数的主要区别是,槽可以与信号关联。

    发射信号规则:

  • 发射信号需要使用 emit 关键字,注意,在 emit 后面不需要冒号。

  • emit 发射的信号使用的语法与调用普通函数相同,比如有一个信号为 void f(int),则发送的语法为:emit f(3); 

  • 当信号被发射时,与其相关联的槽函数会被调用(注意:信号和槽需要使用

  • QObject::connect 函数进行关联之后,发射信号后才会调用相关联的槽函数)。

  • 因为信号位于类之中,因此发射信号的位置需要位于该类的成员函数中或该

  • 类能见到信号的标识符的位置。

    信号和槽的关系:

  • 槽的参数的类型需要与信号参数的类型相对应,

  • 槽的参数不能多余信号的参数,因为若槽的参数更多,则多余的参数不能接收到信号传递过来的值,若在槽中使用了这些多余的无值的参数,就会产生错误。

  • 若信号的参数多余槽的参数,则多余的参数将被忽略。

  • 一个信号可以与多个槽关联,多个信号也可以与同一个槽关联,信号也可以关联到另一个信号上。

  • 若一个信号关联到多个槽时,则发射信号时,槽函数按照关联的顺序依次执行。

  • 若信号连接到另一个信号,则当第一个信号发射时,会立即发射第二个信号。

//头文件 m.h 的内容
class A : public QObject{ //信号和槽必须继承自 QObject 类
  Q_OBJECT                    //必须添加该宏
  
  //public signals:void s1(int);  //错误 signals 前不能有访问控制符。
  
  signals:void s();//使用 signals 关键字声明信号,信号的语法与声明函数相同。
  signals:void s(int,int);//正确,信号可以有参数,也可以重载。
  
  //void s2(){} //错误,信号只需声明,不能定义。
  
  void s3(); //注意:这仍是声明的一个信号
public://信号声明结束后,重新使用访问控制符,表示以下声明的是成员函数。
  void g(){
    emit s3(); //发射信号
  }
};
class B:public QObject{
  Q_OBJECT
public slots:               //使用 slots 关键字声明槽
  void x(){
    cout<<"X"<<endl;
  }
  //slots: void x(){}     //错误,声明槽时需要指定访问控制符。
  public:
  void g(){ 
    // emit s3();       //错误,在类 B 中对于标识符 s3 是不可见的
  }
};
​
//源文件的内容
int main(int argc, char *argv[]){
  A ma; 
  B mb;
  QObject::connect(&ma,&A::s3,&mb,&B::x); //关联信号和槽
  ma.g(); //调用g发送信号
  return 0;
}

三、连接信号与槽

    信号和槽使用 QObject 类中的成员函数 connect 进行关联,该函数有多个重载版本。

    方式1:

static QMetaObject::Connection connect(
  const QObject *sender,   const char *signal, 
  const QObject *receiver, const char *method, 
  Qt::ConnectionType type = Qt::AutoConnection)
class A:public QObject {
  Q_OBJECT 
singals: 
  void s(int i);
};
class B:public QObject{
  Q_OBJECT 
public slots: 
  void x(int i){}
};
A ma; B mb; 
QObject::connect (&ma, SIGNAL(s(int)), &mb, SLOT(x(int));

    信号的指定必须使用宏 SIGNAL()和槽必须使用宏 SLOT(),这两个宏能把括号中的内容转换为与形参相对应的 const char*形式。在指定函数时,只能指定函数参数的类型,不能有参数名,也不能指定函数的返回类型。比如 SLOT( x(int i)),是错误的,因为指定了参数名 i,正确形式为 SLOT(x(int) )。

各参数意义如下

  • sender:表示需要发射信号的对象。

  • signal:表示需要发射的信号,该参数必须使用 SIGNAL()宏。

  • receiver:表示接收信号的对象。

  • method:表示与信号相关联的槽函数,这个参数也可以是信号,从而实现信号与信号的关联。该参数若是槽,需使用 SLOT()宏,若是信号需使用 SIGNAL 宏。

  • 返回值的类型为 QMetaObject::Connection,如果成功将信号连接到槽,则返回连接的句柄,否则,连接句柄无效,可通过将句柄转换为 bool 来检查该句柄是否有效。该返回值可用于 QObject::disconnect()函数的参数,以断开该信号和槽的关联。

  • type:用于指明信号和槽的关联方式,它决定了信号是立即传送到一个槽还是在稍后时间排队等待传送。关联方式使用枚举 Qt::ConnectionType 进行描述,下表为其取值及意义:

常量描述

Qt::AutoConnection

(自动关联,默认值)。若接收者驻留在发射信号的线程中(即信号和槽在同一线程中),则使用 Qt :: DirectConnection,否则,使用 Qt :: QueuedConnection。当信号发射时确定使用哪种关联类型。

Qt::DirectConnection

直接关联。当信号发射后,立即调用槽。在槽执行完之后,才会执行发射信号之后的代码(即 emit 关键字之后的代码)。该槽在信号线程中执行。

Qt::QueuedConnection

队列关联。当控制权返回到接收者线程的事件循环后,槽才会被调用,也就是说 emit 关键字后面的代码将立即执行,槽将在稍后执行,该槽在接收者的线程中执行。

Qt::BlockingQueuedConnection

阻塞队列关联。和 Qt :: QueuedConnection 一样,只是信号线程会一直阻塞,直到槽返回。如果接收者驻留在信号线程中,则不能使用此连接,否则应用程序将会死锁。

Qt::UniqueConnection

唯一关联。这是一个标志,可使用按位或与上述任何连接类型组合。当设置 Qt :: UniqueConnection 时,则只有在不重复的情况下才会进行连接,如果已经存在重复连接(即,相同的信号指向同一对象上的完全相同的槽),则连接将失败,此时将返回无效的 QMetaObject::Connection

    方式2:

QMetaObject::Connection connect(
  const QObject *sender, 
  const char *signal, const char *method, 
  Qt::ConnectionType type = Qt::AutoConnection) const
  A ma; 
  B mb; 
  mb.connect(&ma, SIGNAL(s(int)), SLOT(x(int));

    此函数是形式 1 的简化版本,相当于是 connect(sender, signal, this, method, type)

    方式3:

static QMetaObject::Connection connect(
  const QObject *sender, PointerToMemberFunction signal, 
  const QObject *receiver, PointerToMemberFunction method, 
  Qt::ConnectionType type = Qt::AutoConnection)
  A ma; 
  B mb; 
  //正常连接
  QObject::connect(&ma, &A::s, &mb, &B::x );
  //Lambda表达式
  QObject::connect(&ma, &A::s, [=](int result) {result+=1;});

    该函数对信号和槽函数的指定方式不是使用的宏。该形式的函数其实是一个模板函数,其完整原型类似如下:

template<typename PointerToMemberFunction> 
         static QMetaObject::Connection connect(……)

    方式4:

static QMetaObject::Connection connect(const QObject *sender, 
       PointerToMemberFunction signal, Functor functor)
  A ma; 
  QObject::connect(&ma, &A::s, &B::x );

    该函数的第三个参数支持仿函数、全局函数、静态函数、Lambda 表达式,但是不能是类的非静态成员函数。

    该形式的函数其实是一个模板函数,其完整原型类似如下:

template<typename PointerToMemberFunction , typename Functor> 
         static QMetaObject::Connection connect(……)

    方式5:

static QMetaObject :: Connection QObject :: connect(
  const QObject * sender, const QMetaMethod&signal,
  const QObject * receiver, const QMetaMethod& method,
  Qt :: ConnectionType type = Qt :: AutoConnection)

    此函数的工作方式与形式 1 相同,只是它使用 QMetaMethod 指定信号和槽。

形式 3 与形式 1 的区别

  • 形式 1 的 SINGAL 和 SLOT 宏实际是把该宏的参数转换为字符串,当信号和槽相关联时,使用的是字符串进行匹配,因此,信号和槽的参数类型的名字必须在字符串意义上相同,所以信号和槽无法使用兼容类型的参数,也因此也不能使用 typedef 或namespace 的类型,虽然他们的实际类型相同,但由于字符串名字不同,从而无法使用形式 1。

  • 形式 3 的信号和槽函数的参数类型不需完全一致,可以进行隐式转换。形式 3 还支持typedef 和命名空间。

  • 形式 3 以指针的形式指定信号和槽函数,不需再使用 SIGNAL()和 SLOT 宏。

  • 形式 3 的槽函数可以不使用 slots 关键字声明,任意的成员函数都可以是槽函数。形式 1 的槽函数必须使用 slots 修饰。

  • 形式 1 的槽函数不受 private 的限制,也就是说即使槽是 private 的,仍可通过信号调用该槽函数,而形式 3 则在使用 connect 时就会发生错误。

  • 当信号或槽函数有重载的形式时,使用形式 3 可能会产生二义性错误,此时可使用函数指针的形式指定信号或槽函数,或者使用形式 1,比如

class A:public QObject {
  Q_OBJECT 
singals:
  void s(int i);
};
class B:public QObject{
  Q_OBJECT
public slots:
  void x(){} 
  void x(int i){}
};
A ma; B mb; 
QObject::connect(&ma, &A::s, &mb, &B::x ); //二义性错误。
//可使用如下方式解决(对于信号类似)
QObject::connect(&ma, &A::s, &mb, 
                 static_cast<void (B::*)(int)> (&B::x));

四、断开信号与槽连接

    信号和槽使用 QObject 类中的成员函数 disconnect 函数断开其关联,该函数有多个重载版本。

    方式1:

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

  • 此函数指定 signal 和 method 时需要使用 SIGNAL 和 SOLT 宏。

  • 如果连接成功断开,则返回 true;否则返回 false。

  • 当所涉及的任何一个对象被销毁时,信号槽连接被移除。

  • 0可以用作通配符,分别表示“任意信号”、“任何接收对象”或“接收对象中的任何插槽”。

  • sender永远不会是 0。

  • signal为 0,则将 receiver 对象中的槽函数 method 与所有信号断开。否则,则只与指定的信号断开,此方法可断开槽与所有信号的关联。

  • receiver为 0,此时 method 也必须为0。

  • method 为 0,则断开与连接到 receiver 的任何连接。

  • 除此之外,还有以下几种常用的用法

  • disconnect(&ma, 0, 0, 0); 断开与对象 ma 中的所有信号相关联的所有槽。

  • disconnect(&ma , SIGNAL(s()), 0, 0);断开与对象 ma 中的信号 s 相关联的所有槽。

  • disconnect(&ma, 0, &mb, 0); 断开 ma 中的所有信号与 mb 中的所有槽的关联

    方式2:

static bool QObject::disconnect (
  const QMetaObject::Connection &connection)

    该函数断开使用 connect 函数返回的信号和槽的关联,若操作失败则反回 false。

    方式3:

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

    此方法与形式 1 相同,只是指定函数的方式是使用函数指针。

    该函数不能断开信号连接到一般函数或 Lambda 表达式之间的关联,此时需要使用形式 2 来断开这种关联。

    方式4:

static bool QObject::disconnect(
  const QObject *sender, const QMetaMethod &signal, 
  const QObject*receiver, const QMetaMethod &method)

    该函数与形式 1 相同,只是指定函数的方式是使用 QMetaMethod 类。

    方式5:

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

    该函数是非静态的,该函数是形式 1 的重载形式。

    方式6:

bool QObject::disconnect(
  const QObject *receiver, 
  const char *method = Q_NULLPTR) const

    该函数是非静态的,该函数是形式 1 的重载形式。

    注意:若关联信号和槽时使用的是函数指针形式,则在断开信号和槽时,最好使用相对应的函数指针形式的 disconnect 版本,以避免产生无法断开关联的情形。

 

 

  • 10
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值