第二课:QT的信号和槽机制

简介
信号和槽
信号

一个小例子
一个实际例子
带有默认参数的信号和槽
信号和槽的进一步使用
和第三方库信号槽使用Qt

简介

今天整合了一下QT的信号和槽机制。总的来说信号和槽用于对象间的通讯。信号/槽机制是Qt的一个核心特征,也是Qt与其它框架提供的特性中最不相同的部分。

在GUI编程中,当我们改变一个部件时,经常想要其他部件被通知。更一般化,我们希望任何一类的对象可以和其它对象进行通讯。例如,如果我们点击一个关闭按钮,我们可能想要窗口的close()函数被调用。

其他工具包通过回调实现了这种通信。回调是一个函数指针,所以如果你希望一个处理函数通知你一些事件,你可以把另一个函数(回调)的指针传递给处理函数。处理函数在适当的时候调用回调。尽管一些成功的框架使用了这个方法,但是回调可能是不直观的,并可能在确保回调参数类型正确性上存在问题。

信号和槽

在Qt中我们有一种可以替代回调的技术:我们使用信号和槽。当一个特定事件发生的时候,一个信号被发射。Qt的小部件有很多预定义的信号,但是我们总是可以通过继承来加入我们自己的信号。槽就是一个对应于特定信号的被调用的函数。Qt的部件有很多预定义的槽,但同样可以通过子类化加入自己的槽来处理感兴趣的信号。

当指定信号 signal 时必须使用 Qt 的宏 SIGNAL()包装,当指定槽函数时必须使用宏 SLOT()包装

信号和槽机制是类型安全的:一个信号的签名(参数类型)必须与它的接收槽的签名相匹配。(实际上,一个槽的签名可能比它接收到的信号短,因为它可以忽略额外的参数。)因为签名是兼容的,当使用基于函数指针的语法时,编译器就可以帮助我们检测类型不匹配。基于字符串的信号和槽语法将在运行时检测类型不匹配。信号和插槽是松散耦合的:一个发射信号的类不用知道也不用关心哪个槽要接收这个信号。Qt的信号和槽的机制可以保证如果你把一个信号和一个槽连接起来,槽会在正确的时间使用信号的参数被调用。信号和槽可以使用任何数量、任何类型的参数。它们是完全类型安全的:不会再有回调核心转储(core dump)。

从QObject类或者它的子类(如QWidget类)继承的所有类可以包含信号和槽。当改变它们的状态的时候,信号被发送。从某种意义上说,即它们对其他对象感兴趣。这就是所有的对象通讯时所做的一切。它不知道也不关心是否有其他对象接收到了它发射的信号。这就是真正的信息封装,并且确保对象可以用作一个软件组件使用。

槽可以用来接收信号,但它们也是普通的成员函数。正如一个对象不知道有其他对象收到它的信号一样,一个槽也不知道它是否被任意信号连接。因此,这种方式保证了Qt创建组件的完全独立。

你可以连接多个信号到一个槽,同样一个信号可以被你需要的多个槽连接。甚至可以一个信号连接到另一个信号。(该情况下,只要第一个信 号被发射时,第二个信号立即被发射。)

总体来看,信号和槽构成了一个强有力的组件编程机制。

信号

当一个对象内部状态发生改变时发射信号,信号就被发射,在某些方面对象的客户端和所有者可能感兴趣。信号是一个public访问函数并可以在任何地方发射,但是我们建议仅在定义了信号的类及他们的子类中发射。

当一个信号被发射,它所连接的槽会被立即执行,类似于一个普通函数的调用。在该情况下,信号/槽机制在任何GUI事件循环中是完全独立的。一旦所有的槽返回了,emit语句随后的代码将会执行。这与queued connections方式有些许不同,queued connections方式下emit关键字随后的代码会立即继续执行,槽在随后执行(相当于异步,参考信号与槽的连接类型)。

如果几个槽被连接到一个信号,当信号被发射时,这些槽就会按它们连接的顺序挨个执行。

信号会由moc自动生成并一定不要在.cpp文件中实现。它们也不能有任何返回类型(也就是 只能使用void)。

关于参数需要注意的:我们的经验表明,如果不使用特殊类型,信号和插槽就可重用。如果QScrollBar::valueChanged() 使用了一个特殊的类型,比如hypothetical QRangeControl::Range,它就只能被连接到给QScrollBar设计的特定槽。连接到其他不同的输入部件是不可能的。

信号可以通过关键字emit来发射

emit clicked();

当与槽连接的信号被发射时,该槽被调用。槽是普通的C++函数可以正常调用;它们唯一的特点就是可以被信号连接。

因为槽是普通成员函数,所以被直接调用时遵循一般的C++规则。然而,作为槽,它们能被任何组件调用,通过信号槽连接,无需考虑访问级别。这意味着从一个任意类的实例发出的一个信号可以在一个不相关的类的实例中调用一个私有槽。

同样还可以定义虚拟槽,我们在实践中发现它非常有用。

与回调相比,信号和插槽的速度稍慢,因为它们增加了灵活性,尽管在实际应用程序中的差异是微不足道的。

信号和槽的机制是非常有效的,但是它不像“真正的”回调那样快。信号和槽稍微有些慢,这是因为它们所提供的灵活性造成,尽管在实际应用中这些性能可以被忽略。通常,发射一个和槽相连的信号,大约比直接调接收器非虚函数的调用慢十倍。这是定位连接对象所需的开销,为了安全地遍历所有连接(e.g检查随后的接收器在发射过程中没有被销毁),并以通用的方式安排任意参数。虽然10个非虚函数调用听起来可能很多,但开销比任意new或delete 操作少得多。当执行一个字符串时,vector或list操作在后面的场景中需要new或delete, 但信号和槽在完成函数调用的开销中只占很小的一部分。在一个槽中进行系统调用或者间接地调用超过10个函数,情况也是相同的;。信号/插槽机制的简单和灵活性对于时间的开销是值得,用户甚至不会注意到这点。

注意,与基于qt的应用程序一起编译时,定义变量的其他库调用信号和槽可能会造成编译器警告和错误。要解决这个问题,#undef这些预处理器的冲突符号即可。

一个小例子

一个小的C++类声明如下:

	     class Counter
	       {
	      public:
	          Counter() { m_value = 0; }
	    
	          int value() const { return m_value; }
	          void setValue(int value);
	    
	      private:
	          int m_value;
	        }

一个小的基于QObject类如下:

    #include <QObject>
    
      class Counter : public QObject
      {
          Q_OBJECT
    
      public:
          Counter() { m_value = 0; }
    
          int value() const { return m_value; }
    
      public slots:
          void setValue(int value);
    
      signals:
          void valueChanged(int newValue);
    
      private:
          int m_value;
      };

这个基于QObject的类有同样的内部状态,并提供公有方法来访问状态。除此之外,这个类可以通过发射一个valueChanged()信号来告诉外部世界关于它的状态改变,并且它有一个槽,其它对象可以发送信号给这个槽。

所有包含信号和/或者槽的类必须在它们的声明中提到Q_OBJECT。它们也必须从QObject继承(直接或间接)。

槽由应用程序的编写者来实现。这里是Counter::setValue()可能的一个槽实现:

void Counter::setValue(int value)
  {
      if (value != m_value) {
          m_value = value;
          emit valueChanged(value);
      }
  }

emit这一行从对象中发射valueChanged()信号,以一个新值作为参数。

在下面的小片断中,我们创建了两个Counter对象,并使用QObject::connect()将第一个对象的valueChanget()信号连接到第二个对旬的setValue槽。
下面是把两个对象连接在一起的一种方法:

  Counter a, b;
        QObject::connect(&a, &Counter::valueChanged,
                         &b, &Counter::setValue);
  
        a.setValue(12);     // a.value() == 12, b.value() == 12
        b.setValue(48);     // a.value() == 12, b.value() == 48    

调用a.setValue(12)会使a发射一个valueChanged(12) 信号,b将会在它的setValue()槽中接收这个信号,也就是b.setValue(12) 被调用。接下来b会发射同样的valueChanged()信号,但是因为没有槽被连接到b的valueChanged()信号,所以该信号被忽略。

注意只有当value!= m_value的时候setValue()函数才会设置这个值并发射信号。这样就避免了存在环connections的情况下无限循环。(比如b.valueChanged() 和a.setValue()连接在一起)。

默认情况下,对于你做出的每一个连接,一个信号被发射;重复连接下两个信号被发射。你可以调用disconnect()中断所有连接。如果传递了Qt::UniqueConnection类型,连接只有在不重复的情况下才建立。如果已经存在重复连接(同一个对象上确定的信号到确定的槽),则连接将会失败,connect返回fase。

这个例子说明了对象可以协同工作而不需要知道彼此的任何信息。为达到这种效果,对像仅需要相互连接,并用一个简单的QObject::connect()函数调用实现,或者使用uic的自动连接特性。

一个实际例子

这是一个注释过的简单的例子。

 #ifndef LCDNUMBER_H
 #define LCDNUMBER_H

  #include <QFrame>

  class LcdNumber : public QFrame
  {
      Q_OBJECT

QLcdNumber继承于QObject,经由QFrame和QWidget,包含有大多数信号和槽知识,它有几分类似于内置的QLCDNumber小部件。

Q_OBJECT是由预处理器展开,声明几个由moc实现的成员函数,如果你得到了几行 “undefined reference to vtable for LcdNumber”这样的编译器错误信息,你也许忘记运行moc或者在链接命令中包含moc输出。

public:
      LcdNumber(QWidget *parent = 0);

它明显和moc不相关,但是如果你继承了QWidget,几乎肯定希望在构造函数中拥有父参数,并将其传递给基类的构造函数。
这里省略了一些析构函数和成员函数,moc忽略了这些成员函数。

   signals:
        void    overflow();

当QLCDNumber被要求显示一个不可能值时,它发射一个信号。

如果你不关心溢出,或者你认为溢出不会发生,你可以忽略overflow()信号, 也就是说你可以不必将该信号连接到任何槽。

另一方面,当数字溢出时,你想调用两个不同的错误函数,只需简单地将信号连接到两个不同的槽。Qt将调用它个槽(按连接顺序)。

public slots:
void display(int num);
void display(double num);
void display(const QString &str);
void setHexMode();
void setDecMode();
void setOctMode();
void setBinMode();
void setSmallDecimalPoint(bool point);
};
#endif

一个槽就是一个接收函数,用于获得其它窗口部件状态改变的信息。LcdNumber 使用它,就像上面的代码一样,设置显示的数字。因为display()是这个类和程序其它的部分的一个接口,所以这个槽是公有的。

几个例程把QScrollBar的valueChanged()信号连接到display()槽,所以LCD数字可以连续显示滚动条的值。

请注意代码中display()被重载了,当你把一个信号和这个槽相连的时候,Qt将会选择适当的版本。如果使用回调,你不得不查找五个不同的名称并且自己记录类型。

一些不相关的成员函数已从例子中省略。

带有默认参数的信号和槽

信号和槽的签名可能包含参数,参数可以有默认值。考虑QObject::destroyed()函数。

void destroyed(QObject* = 0);

当一个QObject对象被删除,它发射QObject::destroyed()信号。我们想捕获此信号,无论在我们可能有一个悬空的引用指向删除的QObject对象的情况下,因此我们可以把它清理干净。一个合适的槽签名可能是这样的:

void objectDestroyed(QObject* obj = 0);

为连接一个信号到此槽,我们使用QObject::connect。这里有几个方式连接。第一种方式是使用函数指针:

connect(sender,&QObject::destroyed,this,
        &MyObject::objectDestroyed);

使用QObject::connect()和函数指针有几个优点。首先,它允许编译器检查信号的参数是否与槽的参数兼容。如果需要,参数也可以由编译器隐式转换。

也能连接到仿函数或C++11匿名函数:

connect(sender, &QObject::destroyed, \
    [=](){ this-  >m_objects.remove(sender); });

需要注意的是,如果编译器不支持C++11变量模板,那么这个语法只有在信号和槽有6个参数或更少的情况下才有效。

连接信号到槽的另一种方法是使用QObject::connect(),SIGNAL和SLOT宏。关于是否在SIGNAL()和SLOT()宏中包含参数的规则,如果参数有默认值,传递给SIGNAL()宏的签名必须不能少于传递给SLOT()宏签名的参数。

下述这些都会生效:

connect(sender,SIGNAL(destroyed(QObject*)),this,
        SLOT(objectDestroyed(Qbject*)));

connect(sender,SIGNAL(destroyed(QObject*)),this,
       SLOT(objectDestroyed()));

connect(sender, SIGNAL(destroyed()),this,
        SLOT(objectDestroyed()));

以下这个不会生效:

connect(sender,SIGNAL(destroyed()),this,
        SLOT(objectDestroyed(QObject*)));

因为槽期望一个QObject,而信号并不会发送此参数。该连接将会报运行错误。

注意,当使用这个QObject::connect()重载时,编译器不会检查信号和槽参数。

信号和槽的进一步使用

对于可能需要信号发送者信息的情况, Qt提供了QObject::sender()函数,它返回一个指向发送信号的对象的指针。

QSignalMapper类为一种情形提供,该情况下多个信号连接到同一个槽,而槽需要以不同的方式处理每个信号。

假设有三个按钮来决定将打开哪个文件: “Tax File”, “Accounts File”, “Report File”.

为了打开正确的文件,使用QSignalMapper:setMapping()来映射所有QPushButton:clicked()到QSignalMapper对象。然后将文件的QPushButton::clicked()信号连接到

QSignalMapper::map()槽。

signalMapper = new QSignalMapper(this);

signalMapper->setMapping(taxFileButton,   QString("taxfile.txt"));

signalMapper->setMapping(accountFileButton,
                        QString("accountsfile.txt"));

signalMapper->setMapping(reportFileButton,
                        QString("reportfile.txt"));

connect(taxFileButton, &QPushButton::clicked,
          signalMapper, &QSignalMapper::map);

connect(accountFileButton, &QPushButton::clicked,
          signalMapper, &QSignalMapper::map);

connect(reportFileButton, &QPushButton::clicked,
          signalMapper, &QSignalMapper::map);

然后,将mapped()信号连接到readFile(),根据按下的按钮,readFile()打开不同的文件。

connect(signalMapper, SIGNAL(mapped(QString)),
this, SLOT(readFile(QString)));

和第三方库信号槽使用Qt

和Qt使用3rd party信号槽机制是可能的。甚至可以在同一个项目中使用这两种机制。只需添加下述行到你的qmake工程的(.pro)文件中。
CONFIG += no_keywords

它告诉Qt不要定义moc关键字 signals, slots,和emit,因为这些名称将被第三方库所使用,如Boost。然后以no_keywords标志继续使用Qt信号和槽机制,然后在你的源码中以对应的Qt宏Q_SIGNALS(或Q_SIGNAL),Q_SLOTS(或Q_SLOT),和Q_EMIT。

同时也可以参考Meta-Object System 和 Qt’s Property System.

https://blog.csdn.net/lsfreeing/article/details/78575246#简介
https://blog.csdn.net/qq769651718/article/details/79357902

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值