Signals & Slots

信号和槽用于对象间通信. 信号和槽机制是Qt的核心特性, 也是与其他框架最大的不同之处. Qt元对象系统是基础 .

介绍

在GUI编程中, 我们更改一个widget时, 我们经常希望另一个widget也收到通知. 进一步地讲, 我们希望任意类型对象都能通信. 例如, 用户单击 关闭 按钮时, 我们希望调用窗口的 close() 函数.

其他GUI工具包采用回调方式实现通信. 回调是一个函数指针, 因此, 如果你想知道一个函数过程中的某些事件, 你可以向函数传递一个回调函数(另一个函数指针). 处理函数在恰当时机调用回调函数. 虽然使用这种机制的成功框架确实存在, 但是回调函数不够直观, 且确保回调参数类型正确性存在问题.

信号和槽

在Qt中, 我们提供一种回调技术的替代方案: 信号和槽. 当特定事件发生时, 信号被发出. Qt的widget有许多预定义信号, 但是我们总是可以子类化widget, 增加自定义信号. 槽函数是响应特定信号而被调用的函数. Qt的widget有许多预定义的槽函数, 更通用的方式是我们子类化widget, 增加自定义槽函数, 处理感兴趣的信号.

信号和槽函数机制是安全的: 信号的签名和接收槽函数的签名必须匹配. (事实上, 槽函数的签名可能比它关联的信号更短, 因为它可以忽略多余参数.) 由于签名是兼容的, 所以使用基于函数指针的语法, 编译器可以帮助我们检测类型是否匹配. 基于字符串的信号和槽函数语法, 在运行时检测类型是否匹配. 信号和槽函数是松散耦合的: 一个类发出信号, 不必关心哪个槽函数接收这个信号. Qt的信号和槽函数机制保证: 如果你将信号连接到槽函数上, 槽函数在正确的时间传入信号参数, 并被调用. 信号和槽函数可接收任意数量, 任意类型的参数. 它们是完全类型安全的.

继承自 QObject 的所有类都可以包含信号和槽函数. 当对象改变其他对象感兴趣的状态时, 它会发出信号. 这就是它为通信所做的工作. 它不知道, 也不关心是否有对象接收它发出的信号. 这是真正的信息封装, 确保对象可以作为软件组件使用.

槽函数可以接收信号, 但是它们也是普通的成员函数. 跟信号类似, 槽函数也不知道, 不关心是否有信号连接它. 这确保使用Qt可以创建真正独立的组件.

你可以连接任意数量的信号到一个槽函数, 也可将一个信号连接到任意数量的槽函数. 甚至, 你可以将一个信号连接到另一个信号. ( 一个信号发出时, 立即发出第二个信号.)

综上所述, 信号和槽函数构成一个强大的组件编程机制.

信号

当对象内部状态(客户端或所有者感兴趣)改变时, 它会发出信号. 信号是公共访问函数, 可以从任何地方发出, 但是我们推荐仅从定义信号的类或子类发出信号.

信号发出时, 连接到它的槽函数立即执行, 就像正常的函数调用. 这种情况发生时, 信号和槽函数机制完全独立于GUI事件循环. emit 语句之后的代码将在所有槽函数返回后执行. 略微不同的是, 如果连接采用 队列方式(Qt::QueuedConnection); emit 语句之后的代码立即执行, 而槽函数稍后执行.

如果多个槽函数连接一个信号, 当信号发出时, 这些槽函数会依照连接顺序依次执行.

信号是 moc 工具自动生成的, 不必在 .cpp 文件实现. 它们没有返回类型 (即. 返回 void).

关于参数的注意事项: 经验表明, 如果信号和槽函数不适用特殊类型, 它们可重用性更强. 如果 QScrollBar::valueChanged() 参数适用特殊类型, 如QScrollBar::Range, 它只能连接为 QScrollBar设计的槽函数. 它将不能连接不同类型的输入widget.

槽函数

信号发出时, 连接到信号上槽函数会被调用. 槽函数是普通的C++函数, 可以被正常调用; 它们唯一特殊的地方是可以与信号连接.

由于槽函数是普通的成员函数, 所有它们直接调用时, 遵循C++规则. 但是, 作为槽函数, 不管访问级别如何定义, 任何组件都可以通过信号和槽函数连接调用它们. 这意味着从任意类实例发出的信号, 可能调用不相关类实例的私有槽函数.

你还可以将槽函数定义为虚函数, 这在实践中非常有用.

对比回调机制, 信号和槽函数速度稍慢. 这是因为它们提供更多的灵活性. 对于实际的应用程序而言, 这点差别不明显. 一般而言, 发送一个信号调用槽函数, 比直接调用非虚函数慢大约10倍. 这是查找连接对象, 安全遍历所有连接(即. 检查在信号发出和调用之间, 接收方对象是否被销毁), 通用方式处理参数所需的开销. 10个非虚函数调用听着耗时很多, 但是它们比newdelete操作开销小很多. 一旦你后台执行newdelete一个string, vector或list, 信号和槽函数的开销占整个函数的开销很低. 槽函数中执行系统调用或者调用超过10个函数同样如此. 相比信号和槽函数机制带来的简单及灵活性, 这些开销是值得的, 况且用户也不会注意到.

Note that other libraries that define variables called signals or slots may cause compiler warnings and errors when compiled alongside a Qt-based application. To solve this problem, #undef the offending preprocessor symbol.

一个简单示例

一个最小的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(), 这个信号valueChanged()没有连接的槽函数, 被忽略.

注意: setValue() 函数仅当 value != m_value时才更改变量并发出信号. 这可以防止在循环连接时出现无限循环调用 (如., 将 b.valueChanged() 连接到 a.setValue()).

默认情况下, 你每连接一次, 就发出一个信号; 连接2次, 发出两个信号. 你可以调用 disconnect() 中断这些连接. 如果传递 Qt::UniqueConnection, 只有当它不是副本时才建立连接. 如果已经有一个副本 (相同的对象, 相同的信号, 相同的槽函数), 连接将失败, connect返回false.

这个例子说明对象可以不知道彼此任何信息而一起工作. 要实现这一点, 你只需要将对象连接在一起, 这可以通过 QObject::connect() 简单调用或采用 uic自动连接 功能.

一个真实用例

下面示例是没有成员函数的简单widget的头文件, 目的是展示如何在应用程序中使用信号和槽函数.


  #ifndef LCDNUMBER_H
  #define LCDNUMBER_H

  #include <QFrame>

  class LcdNumber : public QFrame
  {
      Q_OBJECT

LcdNumber 继承自 QObject, QFrame 继承自 QWidget. 这个类类似Qt内置 QLCDNumber.

Q_OBJECT 宏告诉moc工具扩展信号和槽函数; 如果你在编译器中出现错误 "undefined reference to vtable for LcdNumber", 你可能忘记 运行 moc 或包含moc生成的文件.


  public:
      LcdNumber(QWidget *parent = 0);

It's not obviously relevant to the moc, but if you inherit QWidget you almost certainly want to have the parent argument in your constructor and pass it to the base class's constructor.

Some destructors and member functions are omitted here; the moc ignores member functions.


  signals:
      void overflow();

LcdNumber emits a signal when it is asked to show an impossible value.

如果你不关心overflow, 或者你确保不会发生overflow, 你可以忽略这个信号 overflow(), 即. 不连接任何槽函数.

另一方面, 如果你需要在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

槽函数是一个接收函数, 用于获取其他widget的状态改变信息. 正如上面代码所示, LcdNumber 用它设置显示数字. display() 是其他类接口的一部分, 所有是公有的.

有多个示例程序将QScrollBarvalueChanged() 信号连接到 display() 槽函数. 因此, LcdNumber显示滚动条的值.

注意: display() 是重载函数; 连接信号和槽函数时, Qt将选择合适的版本. 如果使用回调, 你必须定义5个名称, 并自己跟踪类型.

Some irrelevant member functions have been omitted from this example.

带默认值的信号和槽函数

信号和槽函数的签名包含参数, 参数可以有默认值. 考虑 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++的lambda表达式:

这两种情况下, 我们提供this作为connect()的上下文. 上下文对象提供关于接收者应该在哪个线程执行. 这一点很重要, 提供上下文可以确保接收者在上下文线程执行.

发送方或上下文销毁时, lambda表达式断开连接. 你应该注意: 信号发出时, 函数内部使用的任何对象都是有效的.


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

另一种将信号和槽连接方式是参数是SIGNALSLOT宏的 QObject::connect(). 采用 SIGNAL() and 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 的信号, sender对象不会发送. 这个连接会在运行时报错.

注意: 采用SIGNAL和SLOT宏方式的 QObject::connect(), 编译器不会检查信号和槽函数的参数.

信号和槽函数的高级使用

对于可能需要知道信号的发送方, Qt提供 QObject::sender() 函数, 这个函数返回指向信号发送对象的指针.

The QSignalMapper class is provided for situations where many signals are connected to the same slot and the slot needs to handle each signal differently.

Suppose you have three push buttons that determine which file you will open: "Tax File", "Accounts File", or "Report File".

In order to open the correct file, you use QSignalMapper::setMapping() to map all the QPushButton::clicked() signals to a QSignalMapper object. Then you connect the file's QPushButton::clicked() signal to the QSignalMapper::map() slot.


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

Then, you connect the mapped() signal to readFile() where a different file will be opened, depending on which push button is pressed.


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

在Qt中使用三方信号和槽函数

使用三方信号/槽函数机制是可能的. 你甚至能在同一项目中使用两种机制. 仅需在pro文件中加入下一行.


  CONFIG += no_keywords

它告诉Qt不要使用moc的关键字 signals, slotsemit, 因为这些名字将被三方库使用, 如. Boost. 若定义 no_keywords 标识后, 还想继续使用Qt的信号和槽函数, 仅需将源文件中所有Qt的关键字使用Qt的宏定义: Q_SIGNALS (或 Q_SIGNAL), Q_SLOTS (或 Q_SLOT), 和 Q_EMIT.

参见 Meta-Object SystemQt's Property System.