【Qt】Qt中信号与槽

00. 目录

01. 信号与槽

​ 信号和槽用于对象之间的通信。信号和插槽机制是Qt的核心功能,可能是与其他框架提供的功能最不同的部分。Qt的元对象系统使信号和槽成为可能。

02. 介绍

​ 在GUI编程中,当我们更改一个小部件时,我们经常需要通知另一个小部件。更一般地说,我们希望任何类型的对象能够彼此通信。例如,如果用户单击“ **关闭”**按钮,我们可能希望调用窗口的close()函数。

​ 其它工具包使用回调实现这种通信。回调是指向函数的指针,因此如果您希望处理函数通知您某些事件,则将指针传递给处理函数的另一个函数(回调)。然后,处理函数在适当时调用回调。虽然确实存在使用此方法的成功框架,但回调可能不直观,并且可能在确保回调参数的类型正确性方面存在问题。

03. 信号与槽

​ 在Qt中,我们有一种替代回调技术:我们使用信号和槽。发生特定事件时会发出信号。Qt的小部件有许多预定义信号,但我们总是可以将部件子类化为了向它们添加我们自己的信号。槽是响应于特定信号而被调用的函数。Qt的部件中有许多预定义的槽函数,但通常的做法是将部件子类化并添加自己的槽,以便您可以处理您感兴趣的信号。

在这里插入图片描述

​ 信号和槽机制是类型安全的:信号的签名必须与接收槽的签名匹配。(事实上,一个槽可能比它收到的信号具有更短的签名,因为它可以忽略额外的参数。)由于签名是兼容的,编译器可以帮助我们在使用基于函数指针的语法时检测类型不匹配。基于字符串的SIGNAL和SLOT语法将在运行时检测类型不匹配。信号和槽是松散耦合:发出信号的类既不知道也不用关心哪个槽接收该信号。Qt的信号和槽机制确保如果您将信号连接到槽,将在适当的时间使用信号的参数调用槽。信号和槽可以采用任何类型的任意数量的参数。它们完全是类型安全的。

​ 从QObject或其子类之一(例如,QWidget)继承的所有类都可以包含信号和槽。当它们以某种方式改变它的状态时, 信号就会被发送, 其它对象可能对该信号感兴趣。这是的话所有的对象都可以通讯。它不知道或关心是否有任何东西正在接收它发出的信号。这是真正的信息封装,并确保该对象可以用作软件组件。

​ 槽可用于接收信号,但它们也是普通的成员函数。就像一个对象不知道是否有任何东西接收到它的信号一样,一个槽函数不知道它是否有任何信号连接到它。这确保了可以使用Qt创建真正独立的组件。

​ 可以将任意数量的信号连接到一个槽,并且可以根据需要将信号连接到任意数量的插。甚至可以将信号直接连接到另一个信号。(每当发射第一个信号时,将立即发出第二个信号。)

信号和插共同构成了一个强大的组件编程机制。

04. 信号

​ 当对象的内部状态以某种可能对对象的客户端或所有者感兴趣的方式发生更改时,对象会发出信号。信号是公共访问函数,可以从任何地方发出,但我们建议只从定义信号及其子类的类中发出它们。

​ 当信号发出时,通常会立即执行与其连接的槽函数,就像正常的函数调用一样。发生这种情况时,信号和槽机制完全独立于任何GUI事件循环。emit发出信号之后, 所有与它关联的槽函数将会被执行。这种情形与使用排队连接时情况略有不同; 在这种情况下,emit关键字后面的代码将立即继续,并且稍后将执行槽函数。

​ 如果多个槽连接到一个信号,则在发出信号时,槽函数将按照它们已连接的顺序依次执行。

​ 信号由moc自动生成,不得在.cpp文件中实现。它们永远不会有返回类型(即使用void)。

关于参数的说明

我们的经验表明,如果信号和槽不使用特殊类型,则它们可以重复使用。如果QScrollBar :: valueChanged()使用特殊类型,例如假设的QScrollBar :: Range,则它只能连接到专门为QScrollBar设计的槽函数。将不同的输入控件连接在一起是不可能的。

05. 槽

​ 当连接的信号被发送时, 对应的槽函数将会被执行。槽函数是普通的C ++函数,可以正常调用; 它们唯一的特点是信号可以连接到它们。

​ 由于插槽是普通的成员函数,因此它们在直接调用时遵循正常的C ++规则。但是,作为槽,它们可以通过信号和槽连接由任何组件调用,而不管其访问级别如何。这意味着从任意类的实例发出的信号可以导致在不相关的类的实例中调用私有槽。

​ 我们发现这些槽在实践中非常有用时, 还可以将槽函数定义为虚槽函数。

​ 与回调相比,信号和槽稍微慢一些,因为它们提供了更大的灵活性,尽管实际应用的差异是微不足道的。通常,发射连接到某些槽的信号比使用非虚函数直接调用槽函数大约慢十倍。这是定位连接对象,安全地遍历所有连接(即检查后续槽函数在发射期间没有被销毁)以及以通用方式编组任何参数所需的开销。虽然十个非虚函数调用可能听起来很多,但它的开销比任何newdelete操作都少得多,例如。只要执行场景后面的字符串,vector或list操作,就需要newdelete,信号和槽开销仅仅是函数调用开销的一小部分。无论何时在槽函数中进行系统调用,情况都是如此; 或间接调用十多个函数。由于信号和槽机制的简单性和灵活性, 这一点开销是非常值得的,用户甚至都不会注意到。

​ 请注意,定义变量的其他库在与基于Qt的应用程序一起编译时调用signalsslots可能导致编译器警告和错误。要解决这个问题,#undef违规的预处理器符号。

06. 一个小例子

最小的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()将第一个对象的valueChanged()信号连接到第二个对象的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()的信号,但由于没有槽函数已经被连接到bvalueChanged()`信号,该信号被忽略。

请注意

setValue()函数仅设置值并发出信号value != m_value。这可以防止在循环连接的情况下无限循环(例如,如果b.valueChanged()连接到a.setValue())。

​ 默认情况下,对于您所做的每个连接,都会发出一个信号; 发出两个信号用于重复连接。您可以通过一次disconnect()调用来中断所有这些连接。如果传递Qt :: UniqueConnection 类型,则只有在不是重复的情况下才会建立连接。如果已经有重复(完全相同的信号到相同对象上的完全相同的槽),连接将失败并且connect将返回false

​ 此示例说明对象可以一起工作,而无需了解彼此的任何信息。为了实现这一点,对象只需要被连接在一起,并且这可以用一些简单的实现的QObject ::连接()函数调用,或者与uic自动连接功能。

07. 一个真实的例子

这是一个小部件的简单评论示例。


  #ifndef LCDNUMBER_H
  #define LCDNUMBER_H

  #include <QFrame>

  class LcdNumber : public QFrame
  {
      Q_OBJECT

LcdNumber通过QFrameQWidget继承具有大部分信号槽知识的QObject。它有点类似于内置的QLCDNumber小部件。

Q_OBJECT宏由预处理器扩展来声明由moc实现的几个成员函数; 如果您在“未定义的vtable引用”中遇到编译器错误LcdNumber,您可能忘记运行moc或在链接命令中包含moc输出。

  public:
      LcdNumber(QWidget *parent = 0);

​ 它与moc显然不相关,但是如果你继承了QWidget,你几乎肯定希望parent在构造函数中使用该参数并将其传递给基类的构造函数。

​ 这里省略了一些析构函数和成员函数; 该moc忽略成员函数。


  signals:
      void overflow();

​ 当要求显示不可能的值时LcdNumber发出信号。

​ 如果您不关心溢出,或者您知道不会发生溢出,则可以忽略该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()该类是与该程序其余部分的接口的一部分,因此该槽是公共的。

​ 有几个示例程序将QScrollBarvalueChanged()信号连接到插槽,因此LCDNumber会连续显示滚动条的值。

​ 注意display()重载; 当您将信号连接到槽时,Qt将选择适当的版本。使用回调,您必须找到五个不同的名称并自己跟踪类型。

​ 此示例中省略了一些不相关的成员函数。

08. 信号和槽使用默认参数

信号和槽的签名可以包含参数,参数可以具有默认值。考虑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 lambdas:

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

将信号连接到槽的另一种方法是使用QObject :: connect()和SIGNALSLOT宏。规则是否包含参数在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()重载时,编译器不会检查signal和slot参数。

09. 信号与槽高级用法

​ 对于您可能需要有关信号发送者的信息的情况,Qt提供QObject :: sender()函数,该函数返回指向发送信号的对象的指针。

​ 所述QSignalMapper类提供了一种用于有许多信号被连接到相同的槽和槽需要不同的方式处理每一个信号的情况。

​ 假设您有三个按钮,用于确定要打开的文件:“税务文件”,“帐户文件”或“报告文件”。

​ 要打开正确的文件,可以使用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()将打开其他文件的位置,具体取决于按下的按钮。

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

10. 使用Qt与第三方信号和插槽

可以将Qt与第三方信号/插槽机制一起使用。您甚至可以在同一个项目中使用这两种机制。只需将以下行添加到qmake项目(.pro)文件即可。

CONFIG += no_keywords

它告诉Qt的不要定义moc关键字signalsslotsemit,因为这些名称将由第三方库可以使用,例如boost。然后继续使用带有no_keywords标志的Qt信号和插槽,只需将源中Qt moc关键字的所有使用替换为相应的Qt宏Q_SIGNALS(或Q_SIGNAL),Q_SLOTS(或Q_SLOT)和Q_EMIT

另请参阅元对象系统Qt的属性系统

© 版权声明
THE END
喜欢就支持一下吧
点赞677 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容