Qt信号槽

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wyy626562203/article/details/82760866

Qt信号槽

Qt 5.10.1

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

简介

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

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

信号和槽

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

在这里插入图片描述

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

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

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

您可以将任意数量的信号连接到单个插槽,并且可以根据需要将信号连接到任意数量的槽。甚至可以将信号直接连接到另一个信号。 (每当发射第一个信号时,这将立即发出第二个信号。)
信号和插槽共同构成了一个强大的组件编程机制。

信号

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

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

如果多个槽连接到一个信号,则在发出信号时,槽将按照它们已连接的顺序依次执行。
信号由moc自动生成,不得在.cpp文件中实现。他们永远不会有返回类型(即使用void)。
关于参数的说明:我们的经验表明,如果信号和槽不使用特殊类型,则它们可重用。如果QScrollBar::valueChanged()使用特殊类型,例如假设的QScrollBar::Range,则它只能连接到专门为QScrollBar设计的槽。将不同的输入部件连接在一起是不可能的。

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

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

您还可以将槽定义为虚拟槽,我们发现这些槽在实践中非常有用。
与回调相比,信号和槽稍微慢一些,因为它们提供了更大的灵活性,尽管实际应用的差异是微不足道的。通常,发射一个和槽相连的信号,大约比直接调接收器非虚函数的调用慢十倍。这是定位连接对象所需的开销,安全地遍历所有连接(即检查后续接收器在发射期间没有被破坏)以及以通用方式安排任意参数所需的开销。虽然十个非虚函数调用可能听起来很多,但开销比任意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()将第一个对象的valueChanged()信号连接到第二个对象的setValue()槽:

Counter a, b;
QObject::connect(&a, &Counter::valueChanged,&b, &Counter::setValue);

a.setValue(12);     // ac++.value() == 12, b.value() == 12
b.setValue(48);     // a.value() == 12, b.value() == 48

调用a.setValue(12)使得发出valueChanged(12)信号,b将在其setValue()槽中接收,即调用b.setValue(12)。然后b发出相同的valueChanged()信号,但由于没有插槽连接到b的valueChanged()信号,因此忽略该信号。
请注意,setValue()函数仅在值!= m_value时设置值并发出信号。这可以防止在循环连接的情况下无限循环(例如,如果b.valueChanged()连接到a.setValue())。
默认情况下,对于您进行的每个连接,都会发出一个信号;发出两个信号用于重复连接。您可以通过一次disconnect()调用来中断所有这些连接。如果传递Qt::UniqueConnection类型,则只有在不重复时才会建立连接。如果已经有重复(完全相同的信号到相同对象上的完全相同的插槽),连接将失败并且connect将返回false

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

一个实际的例子

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

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>
class LcdNumber : public QFrame
{
    Q_OBJECT

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

预处理器扩展Q_OBJECT宏以声明由moc实现的几个成员函数; 如果您在编译中遇见“未定义的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()信号连接到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 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()重载时,编译器不会检查signalslot参数。

信号和槽的进一步使用

对于您可能需要有关信号发送者的信息的情况,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。

猜你喜欢

转载自blog.csdn.net/wyy626562203/article/details/82760866