Qt学习笔记1.1.3 Qt Core 核心功能之信号与槽


Qt Core模块
来源: https://doc.qt.io/archives/qt-5.12/signalsandslots.html

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

引言

在GUI编程中,当我们更改一个小部件时,我们通常希望通知另一个小部件。更普遍地说,我们希望任何种类的物体都能够相互交流。例如,如果用户单击Close按钮,我们可能希望调用窗口的Close()函数。

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

信号与槽

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

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

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

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

您可以将任意数量的信号连接到单个插槽,也可以将信号连接到任意数量的插槽。甚至可以将一个信号直接连接到另一个信号。(无论何时发出第一个信号,都会立即发出第二个信号。)

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

信号

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

当一个信号发出时,连接到它的插槽通常会立即执行,就像正常的函数调用一样。当这种情况发生时,信号和插槽机制完全独立于任何GUI事件循环。一旦所有插槽都返回,就会执行emit语句后面的代码。使用排队连接时,情况略有不同;在这种情况下,emit关键字后面的代码将立即继续,稍后将执行插槽。

信号与槽的连接方式通过connect函数的第5个参数指定,类型为Qt::ConnectionType枚举

链接类型说明
Qt::AutoConnection0(默认)如果接收器位于发出信号的线程中,则使用Qt::DirectConnection。否则,将使用Qt::QueuedConnection。连接类型是在发出信号时确定的。
Qt::DirectConnection1在发出信号时会立即调用插槽。插槽在发射信号的线程中执行。
Qt::QueuedConnection2当控制返回到接收器线程的事件循环时,将调用slot。插槽在接收器的线程中执行。
Qt::BlockingQueuedConnection3与Qt::QueuedConnection相同,只是信号线程会阻塞,直到插槽返回。如果接收器位于发射信号的线程中,则不得使用此连接,否则应用程序将死锁。
Qt::UniqueConnection0x80这是一个可以与上述任何一种连接类型组合的标志,使用按位“或”。设置Qt::UniqueConnection时,如果连接已存在(即,如果同一信号已连接到同一对对象的同一插槽),则QObjec::connect()将失败。

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

关于参数的注意事项:我们的经验表明,如果信号和插槽不使用特殊类型,它们将更易于重用。如果QScrollBar::valueChanged()使用的是一种特殊类型,如假设的QScrollBar::Range,那么它只能连接到专门为QScrollBar设计的插槽。将不同的输入小部件连接在一起就是不可能的了。

当连接到插槽的信号发出时,就会调用该插槽。插槽是正常的C++函数,可以正常调用;它们唯一的特点是信号可以连接到它们。

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

您还可以将插槽定义为虚拟插槽,我们发现这在实践中非常有用。与回调相比,信号和插槽的速度稍慢,因为它们提供了更大的灵活性,尽管实际应用程序的差异并不显著。通常,发射连接到某些插槽的信号比直接调用接收器(使用非虚拟函数调用)慢大约十倍。这是定位连接对象、安全地迭代所有连接(即检查后续接收器在发射过程中是否未被破坏)以及以通用方式整理任何参数所需的开销。虽然十个非虚拟函数调用听起来可能很多,但它的开销比任何new操作或delete操作都要小得多。一旦执行了一个字符串、向量或列表操作,而该操作在后台需要新建或删除,则信号和插槽开销只占整个函数调用成本的一小部分。每当您在插槽中进行系统调用时,情况也是如此;或者间接调用十多个函数。信号和插槽机制的简单性和灵活性非常值得开销,而您的用户甚至不会注意到这一点。

请注意,当与基于Qt的应用程序一起编译时,定义signals或slots变量的其他库可能会导致编译器警告和错误。为了解决这个问题,#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);
    }
}

发射行从对象中发射信号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(2)。然后b发出相同的valueChanged()信号,但由于没有插槽连接到b的valueChanged()信号上,因此该信号被忽略。

请注意,setValue()函数仅当value!=m_value时设置值。这在循环连接的情况下防止了无限循环(例如,如果b.valueChanged()连接到a.setValue())。

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

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

uic,User Interface Compiler。uic读取Qt Designer生成的XML格式用户界面定义(.ui)文件,并创建相应的C++头文件。

一个真实案例

下面是一个没有成员函数的简单小部件类的头部示例。目的是展示如何在自己的应用程序中使用信号和插槽

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
    Q_OBJECT

LcdNumber继承自QObject并通过QFrame和QWidget获取了大量信号槽。它有点类似于内置的QLCDNumber小部件。

预处理器对Q_OBJECT宏进行扩展,以声明几个被moc实现的成员函数;如果您在“undefined reference to vtable for LcdNumber”这一行中遇到编译器错误,那么您可能忘记了运行moc或在链接命令中包含moc输出。

public:
    LcdNumber(QWidget *parent = nullptr);

signals:
    void overflow();

在类构造函数和公共成员之后,我们声明类信号。当LcdNumber类被要求显示一个不可能的值时,它会发出一个信号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

插槽是一种接收功能,用于获取其他小部件中状态变化的信息。正如上面的代码所示,LcdNumber使用它来设置显示的数字。由于display()是类与程序其余部分接口的一部分,因此插槽是公共的。

一些示例程序将QScrollBar的valueChanged()信号连接到display()插槽,因此LCD数字可以连续显示滚动条的值。

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

使用默认参数的信号与槽

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

void destroyed(QObject* = nullptr);

当一个QObject被删除时,它会发出这个QObject::destroyed()信号。我们想捕捉这个信号,无论我们在哪里可能有对已删除QObject的悬挂引用,这样我们就可以清理它。合适的插槽签名可能是:

void objectDestroyed(QObject* obj = nullptr);

要将信号连接到插槽,我们使用QObject::connect()。有几种方法可以连接信号和插槽。第一种是使用函数指针:

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

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

您还可以连接到仿函数(functors)或C++11 lambdas函数:

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

在这两种情况下,我们都在调用connect()时将this指针作为上下文提供。上下文对象提供关于接收器应该在哪个线程中执行的信息。这一点很重要,因为提供上下文可以确保接收器在上下文线程中执行。

当发送方或上下文被破坏时,lambda将断开连接。您应该注意,在发出信号时,仿函数内部使用的任何对象都是活动的。

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

所有这些都会起作用:

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()函数,该函数返回一个指向发送信号的对象的指针。

Lambda表达式是将自定义参数传递到槽的一种方便方法:

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

和第三方信号和插槽一起使用Qt

可以将Qt与第三方信号/插槽机制一起使用。您甚至可以在同一个项目中使用这两种机制。为此,请在CMake项目文件中写入以下内容:

target_compile_definitions(my_app PRIVATE QT_NO_KEYWORDS)

在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。

基于Qt的库中的信号和槽

基于Qt的库的公共API应该使用关键字Q_SIGNALS和Q_SLOTS,而不是signals和slots。否则,很难在定义QT_NO_KEYWORDS的项目中使用这样的库。

为了强制执行此限制,库创建者可以在构建库时设置预处理器定义QT_NO_SIGNALS_SLOTS_KEYWORDS。

这个定义排除了信号和槽,而不影响是否可以在库实现中使用其他Qt特定的关键字。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值