Qt信号与槽

信号与槽

源文档链接

介绍

在GUI编程中,当我们改变一个窗口小部件,我们经常想通知到其它小部件。更通用的说,我们想任何类型的对象都能相互通信。例如,如果用户点击Close按钮,我们大概想要窗口的close()函数被调用。


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

信号与槽

在Qt中,我们有一个回调函数技术的替代方案。我们使用信号与槽。当特定事件发生时,会发出一个信号。Qt的窗口小部件有许多预定义的信号,但是我们经常子类化窗口小部件添加我们自己的信号。槽是回应特定信号的函数。Qt的窗口小部件有许多预定义的槽函数,但是通常的做法是子类化窗口小部件并添加自己的槽函数,这样就可以处理感兴趣的信号。

在这里插入图片描述

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


所有继承自QObject或它的子类(例如,QWidget)的类都拥有信号与槽。当对象以其它对象可能感兴趣的方式改变其状态时,就会发出信号。这就是对象所做的全部工作。它不知道也不关心是否有其它对象接收它发出的信号。这是真正的信息封装,并确保对象可以作为软件组件使用。


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


你可以连接许多信号到一个槽函数,也可以连接一个信号到许多槽函数。甚至可以直接连接一个信号到另一个信号。(这将在第一个信号发出时立即发出第二个信号。)


信号与槽一起构成了强大的组件编程机制。

信号

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


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

如果多个槽连接到同一个信号,当信号发出时,这些槽函数将按照连接的顺序依次执行。

信号通过moc自动生成,并且一定不能在.cpp文件中实现。

关于参数的注意事项:我们的经验表明信号与槽如果不适用特殊类型,则它们的可重用性更强。如果QScrollBar::valueChanged()使用特殊类型,例如QScrollBar::Range,它将只能连接到专门为QScrollBar设计的槽。将不同部件连接到一起将不可能。

当连接到该槽的信号发出时,该槽函数被调用。槽函数是普通的C++函数,能被直接调用;它们唯一的特征是可以与信号建立连接。

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

你可以定义槽为虚函数,我们发现在实践中相当有用。

与回调函数相比,信号与槽的速度要稍慢一些,因为它们提供了更大的灵活性,尽管在实际应用中差别不大。一般来说,发送一个连接到某些槽的信号,比直接调用接收器(使用非虚函数调用)大约慢10倍。这是定位连接对象所需的开销,安全遍历所有连接(即检测后续的连接者在发射期间没有被销毁),以及以通用方式按序传递任何参数所需的开销。虽然10个非虚函数调用听起来很多,举例来说,它比任何newdelete操作的开销要小得多。当你在后台执行需要new或"delete"的字符串、数组或列表操作时,信号与槽的开销只占整个函数调用开销的很小一部分。每当你在槽函数中调用一个系统调用或直接调用超过10个函数也是一样的情况。相对于信号与槽机制的简单性和灵活性来说,这点开销是值得的,用户甚至注意不到。

注意在与基于Qt的应用程序一起编译时,其它定义了变量名为singlsslots的库可能报警或错误。使用*#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

// Note. The Q_OBJECT macro starts a private section.
// To declare public members, use the 'public:' access modifier.
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)发出一个valueChagned(12)信号,b将在槽函数setValue()接收到,即b.setValue(12)被调用。然后b发出相同的valueChanged()信号,但由于没有槽函数连接到b的valueChanged()信号,所以该信号被忽略。

注意函数setValue()只有在value != m_value时设置值和发出信号。这防止了环形连接的无限循环(例如,如果b.valueChanged() 连接到a.setValue())。

默认情况下,你建立的每个连接都会发出一个信号;对于两个连接会发出两个信号。你可以调用disconnect断开连接。如果传递Qt::UniqueConnection类型,则仅当连接不重复连接时才建立成功。如果已是重复连接(相同信号连接到相同对象的相同槽),则连接失败并返回false。

这个例子说明了对象可以一起工作,而不需要知道彼此任何信息。要实现这一点,只需要将对象连接到一起,这可以通过简单的QObejct::connect函数调用或使用uic自动连接特性。

一个真实的例子

下面是一个没有成员函数的简单窗口类的头文件的示例。目的是展示如何在你的应用程序中使用信号与槽。

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
    Q_OBJECT

LcdNumber通过QFrame和QWidget继承了QObject,后者具有大部分的信号槽知识。它有点类似内置的QLCDNumber窗口小部件。

Q_OBJECT宏由预处理器展开,用于申明几个由moc实现的成员函数;如果你得到了编译器类似于"undefined reference to vtable for LcdNumber"的错误,你可能忘记去运行moc或忘记在链接命令中包含moc输出。

public:
    LcdNumber(QWidget *parent = nullptr);

signals:
    void overflow();

在构造函数和公共成员函数之后,我们声明类的signals。当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将选择适当的版本。使用回调函数时,你必须找到5个不同的名称,并跟踪它们的类型。

带默认参数的信号与槽

信号与槽的签名可以包含参数,并且参数可以有默认值。查看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()与函数指针一起使用有几个优点。首先,它允许编译器检测信号的参数是否与槽函数的参数兼容。如果需要,编译器也可以隐式转换参数。
你可以连接functorsC++11 lambdas

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

在这两种情况下,我们都在connect()调用中提供了this为上下文。上下文对象提供了接收者在哪个线程中执行的信息。这很重要,因为提供的上下文确保接收者在上下文线程中执行。

当发送者或上下文被销毁时,lambda将断开连接。你需要注意在发出信号时,functor中使用的任何对象都是活着的。

另一种连接信号与槽的方法是使用QObject::connect以及SIGNALSLOTS宏。关于是否在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()重载时,编译器不会检测信号与槽的参数。

信号与槽的高级用法

如果你需要知道信号发送者的信息,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的项目中使用这样的库。

为了强制执行这一限制,库的创建者可以在构建库时设置预处理器define QT_NO_SIGNALS_SLOTS_KEYWORDS。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值