信号和槽 译文

原文:https://doc.qt.io/qt-5/signalsandslots.html

信号和槽

信号和槽用于对象间通信。他们的原理是Qt的主要核心特性,并且可能是Qt和其他框架产生最大不同的特性。通过Qt的原对象系统,实现了信号和槽的合理性。


简介

在图形化用户界面编程中,当我们修改了一个部件,我们通常希望另一个部件得到响应。一般来说,我们希望任何类型的对象间都可以通信。例如,用户点击关闭按钮,我们需要window的close()函数被调用。

其他的工具包通过回调函数实现这种通信。回调函数就是一个指向函数的指针,所以如果你想要一个处理函数,来通知你一些事件,你可以传递一个指向另一个函数(回调函数)的指针给处理函数。处理函数可以在适当的时候调用回调函数。确实有很多成功的框架在使用这种方式,回到函数可能不直观,而且在确保回调函数参数类型正确性方面存在问题。


信号与槽

在Qt当中,对于回调函数,我们有一种替代方案:信号和槽。当一个特定事件发生时,就会发射一个信号。Qt的部件有许多预定义信号,但我们总是可以子类化小部件来添加我们自己的信号。槽是响应特定信号而调用的函数。Qt的部件有很多预定义槽,但惯例的操作是,子类化部件并添加自己的槽,以便操作你感兴趣的信号。

信号和槽 译文_第1张图片

 

信号和槽的原理是类型安全的:信号的签名必须和接收槽的签名一致。(事实上,槽的签名比信号的签名短,因为槽可以忽略接收到的额外参数。)因为参数类型是可兼容的,所以我们使用函数指针语法时,编译器可以帮我们检测参数类型不匹配问题。基于字符串语法的 SIGNAL和SLOT,会在运行时检测类型不匹配。信号和槽是松耦合的:发射信号的类既不知道也不关心哪个类接收信号。Qt的信号和槽机制确保当你连通一个信号到槽,这个槽会使用信号的参数并在正确时机被调用。信号和槽可以使用任意类型的,任意数量参数。它们是完全类型安全的。

所有从QObject或者它的子类(例如 QWidget)继承的类,都可以包含信号和槽。当一个对象关注其他对象并改变状态时,就会发射信号。这就是对象通信所做的一切。对象并不知道也不关心,其所发射的信号是否被接收中。这是真正的信息封装,并且确保对象可以作为软件组件被应用。

槽可以用于接收信号,但槽也是普通的成员函数。正如一个对象不知道它的信号是否被接收,一个槽同样也不知道是否有对象和它保持连接。这就确保了Qt可以创建真正独立的组件。

你可以对一个槽,随意连接多个信号,你也可以把一个信号连接给所需的任意个槽。甚至可以做到一个信号直接连接另一个信号。(这会导致发射第一个信号时,接连发射第二个信号)。

综上,信号和槽组成了强大的组件化变成机制。


信号

当一个对象内部状态以某种方式发生变化,对象所属客户端或所有者恰巧需要关注这个变化,对象就会发射信号。信号是可以在任意位置发射的公共访问功能,但我们推荐仅在信号定义类和它的子类发射信号。

当一个信号发出时,连接该信号的槽通常会立即被执行,就像是一个普通的函数调用。此时,信号和槽机制是完全独立与任何GUI事件循环的。一旦所有的slots都返回,emit声明后面的代码才会执行。但使用queued connections时,会略有不同:在此情形,emit关键字后面的代码会立即继续执行,而槽会稍后被执行。

如果多个槽连接同一个信号,一旦信号发射,那么槽会按照它们连接的顺序依次执行。

信号是通过moc(Meta-Object Compiler)——处理Qt的C++扩展程序,自动生成的,所以不会在.Cpp文件中有实现代码。它们也不会有返回类型(即使用void)。

有关参数的一个提示:经验表明,如果在信号和槽中不使用特殊类型参数的话,更易于可复用。如果 QScrollBar::valueChanged() 假设使用了特殊类型,例如 QScrollBar::Range, 那么它只能连接专门为 QScrollBar 设计的槽。将不同的插入控件连接在一起是不可能的。


当一个槽所连接的信号发射时,槽被执行。槽是普通的C++函数,可以正常调用:它们唯一的特性就是可以连接信号。

因为槽是普通成员函数,当直接调用槽时,遵循普通的C++规则。然而,作为槽,则可以被任意组件调用,而可以忽略组件的访问级别,通过信号和槽的连接即可。这意味着任意一个类的实例发射信号时,都可以导致另一个不相干的类实例的私有槽被调用。

你也可以把槽定义成virtual,在实践中非常有用。

和回调函数相比,信号和槽会稍微慢一些,因为它们提供了增强的灵活性,然而在实际应用中差异微不足道。通常来讲,定义非virtual函数的槽,并且发射一个连接多个槽的信号,近乎比直接调用接收器慢10倍。定位连接对象,安全的遍历所有连接器,并按照通用样式打包所有参数,这些都是日常开销。(例如检查后续接收器在发射信号期间是否被摧毁)

虽然十倍的非虚函数调用听起来开销很大,但是仍然比new或delete操作要小很多。例如,比方你执行了一个string,那么后台就需要使用new或delete来操作vector或者list这些数据结构。而信号和槽的开销仅占用全部函数调用非常小的比例。无论你在槽中做系统调用,还是间接调用,都比10个函数开销的多。信号和槽所具有的简单性和灵活性是值得这些开销的,甚至你的用户都不会注意到开销。

请注意,如果自定义变量命名为signals或slots,当与其他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 

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的版本有同样的内部状态,并提供了public方法访问内部状态。另外使用信号和槽可以支持组件化编程。通过发射信号可以告诉类外部的世界,其内部皇台已经发生改变——valueChanged(),同时它还有一个槽可以接收其他对象发生给它的信号。

所有包含信号和槽的类必须在其声明区顶部标注Q_OBJECT。并且直接或间接派生自QObject。

槽的具体功能都是由应用程序员实现,下面就是Counter::setValue() slot 的一个简单实现方式。

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

emit关键字所在行从对象发射valueChanged()信号,并将新的value作为参数。

以下代码片段,我们创建两个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() 信号。但是因为 b 的 valueChanged() 信号没有被任何槽接收,所以这个信号被忽略了。

注意 setValue() 函数中,赋值和发射信号的判断条件是 value != m_value。这样即使 b.valueChanged() 连接到 a.setValue()之后,循环连接。条件判断依然可以预防无限循环赋值。

默认情况下,你建立的每个连接都会发射一个信号,重复的连接会发射两个信号。你可以调用一次disconnect()函数来中断所有连接。如果你传递Qt::UniqueConnection类型,那么只有在无重复情况下,连接才会创建。如果已经存在一个副本(相同对象中,相同信号连接相同槽),则连接失败,connect返回false。

这个例子阐明了对象间可以同时工作,并且无需了解对方任何信息。诀窍就是,对象仅需彼此连接,通过一些简单的QObject::connect()函数调用,或者uic's automatic connections 属性,就可以实现.


一个真实的例子

下面的例子,是一个简单的没有成员方法工具类头文件示例。目的是展示如何在自己的应用程序里使用信号和槽。

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include 

class lcdNumber : public QFrame
{
    Q_OBJECT

LcdNumber 通过QFrame 和 QWidget继承自QObject,它拥有大部分的信号和槽知识。它有点类似与内置的QLCDNumber小部件。

 

预处理器展开QOBJECT宏来声明一些由moc实现的成员函数,如果你遇到编译错误"undefined reference to vtable for LcdNumber",可能是因为忘记运行moc run the moc 或者没有在链接命令中导入moc 输出。

public:
    LcdNumber(QWidget *parent = nullptr);

signals:
    void overflow();

在构造函数和公有成员函数之后,我们声明了类信号。当LcdNumber类被要求显示一个非法的值时,会发射一个overflow()信号。

 

如果你不关心值溢出,或者你知道值溢出不会发生,你可以忽略overflow信号,就是不把信号连接给任何槽。

另一方面,当值溢出时,你希望出发两个不同的错误函数,简单的方式就是把信号连接两个不同的槽。Qt会调用(按照connect的顺序)它们。

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会选择匹配的重载函数槽。而对于回调函数,你只能自己调用5个不同名的函数,并匹配合适的参数类型。


带有默认参数的信号和槽

信号和槽可以包含一些参数,并且参数可以有默认值。参考QObject::destroyed()。

void destroyed(QObject* = nullptr);

当一个QObject被销毁时,会发射QObject::destroyed()信号。我们想要捕获到这个信号,无论在哪里,我们只需一个销毁对象的悬空引用,就可以清理它。一个合理的槽实现如下:

void objectDestroyed(QObject* obj = nullptr);

我们使用QObject::connect()连接这个销毁信号到自定义槽。如下几种方式,第一种是使用函数指针:

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

使用函数指针方式有有个优点。第一,它会允许编译器检测信号和槽参数是否兼容。如果有必要的话,参数会被编译器隐式转换。

你可以使用C++11的lambdas泛式函数:

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

在这两种例子中,我们提供this作为调用connect()执行期的上下文。上下文对象提供接收者应该在哪个线程中执行期的信息。这一点很重要,这确保接收者在提供的上下文线程中执行。

当发送方或者上下文对象被销毁时,lambda会断开连接。你应该注意,当信号发射时,在泛式函数中的任何对象可以持续保持活性。

另外一种使用QObject::connect()连接信号和槽的方式是使用SIGNAL和SLOT宏定义。关于是否在SIGNAL宏 和 SLOT 宏中包含参数的规则是,如果参数有默认值,那么传递给SIGNAL宏的签名参数必须不少于传递给SLOT宏的签名参数。(简言之,如果有参数默认值,SIGNAL宏参数要多于SLOT宏参数)。

以下这些都可以:

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(QObject*)));
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()函数,可以返回一个指向信号发射者的指针。

lambda表达式是一种可以传递这种自定义参数到槽的便利的方式。

connect(action, &QAction::triggered, engine, [=](){ engine->processAction(action->text()); });

使用Qt与第三方信号和槽

可以使用Qt和一个第三方的信号和槽机制。你甚至可以在一个项目中使用这两种机制。只需要在你的qmake项目.pro文件中加入下面这行:

CONFIG += no_keywords

这会告诉Qt不要定义moc的信号和槽关键字——signals, slots,emit。因为这些名字会在第三方库库中使用。例如: Boost。接下来要继续使用带有no_keywords标签的Qt 信号和槽,仅仅把你项目中所有 Qt moc 关键字用Qt宏Q_SIGNALS(或Q_SIGNAL),Q_SLOTS(或Q_SLOT),还有 Q_EMIT 替换即可。

 

另外请参阅: QLCDNumber, QObject::connect(), Digital Clock Example, Tetrix Example, Meta-Object System, 和 Qt's Property System.

你可能感兴趣的:(Qt,c++,qt,qt5)