Qt 核心功能(04):信号和槽【官翻】

文章目录

  • 介绍
  • Signals 和 Slots
    • Signals
    • Slots
  • 一个小例子
  • 一个真实的例子
  • 带有默认参数的信号和槽
  • 高级信号和插槽的使用
  • 使用Qt与第三方信号和插槽

信号和槽

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

介绍

GUI编程中,当我们更改一个小部件时,我们通常希望另一个小部件得到通知。更一般地,我们希望任何类型的对象都能够相互通信。例如,如果用户单击关闭按钮,我们可能希望调用窗口的Close()函数。

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

Signals 和 Slots

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

Qt 核心功能(04):信号和槽【官翻】_第1张图片

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

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

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

可以将任意多的信号连接到一个插槽上,而一个信号也可以连接到任意多的插槽上。甚至可以将一个信号直接连接到另一个信号。(当第一个信号发出时,它将立即发出第二个信号。)

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

Signals

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

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

果几个插槽连接到一个信号上,当信号发出时,这些插槽将按照它们连接的顺序依次执行。

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

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

Slots

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

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

还可以将槽定义为虚拟的,这在实践中非常有用。

回调相比,信号和插槽的速度稍微慢一些,因为它们提供了更高的灵活性,尽管对于实际应用程序来说,这种差别并不显著。一般来说,发送一个连接到某些插槽的信号,比直接调用非虚函数要慢大约10倍。这是定位连接对象、安全地遍历所有连接(即检查后续接收方在发射过程中没有被销毁)以及以通用方式marshall任何参数所需的开销。虽然10个非虚函数调用听起来很多,但是它比任何新操作或删除操作的开销要小得多。一旦您在后台执行一个需要新建或删除的字符串、向量或列表操作,信号和插槽开销只占整个函数调用开销的很小一部分。在槽中执行系统调用时也是如此;或间接调用超过十个函数。信号和插槽机制的简单性和灵活性是值得的,这些开销用户甚至不会注意到。

意,当与基于qt的应用程序一起编译时,定义称为信号或槽的变量的其他库可能会导致编译器警告和错误。要解决这个问题,请用#undef来定义出错的预处理器符号。

一个小例子

一个最小的c++类声明可能是:

class Counter {
public:
    void setValue(int val){
        if(m_val != val){
            m_val = val;
        }
    }
    int m_val=0;
};

上面的普通类如果需要使用信号和槽机制,需要继承QObject类,如下:

class Counter : public QObject{
    Q_OBJECT
//增加信号函数,只有定义无实现
signals:
    void valueChanged(int newVal);

public:
    void setValue(int val){
        if(m_val != val){
            m_val = val;
            // 发出信号,带参数
            emit valChanged(val);
        }
    }
    int m_val=0;
};

于qobject的版本具有相同的内部状态,并提供了访问该状态的公共方法,此外,它还支持使用信号和槽进行组件编程。这个类可以通过发送一个信号valueChanged()告诉外部世界它的状态已经改变,并且它有一个槽,其他对象可以将信号发送到这个槽。

有包含信号或槽的类都必须在声明的顶部提到Q_OBJECT。它们还必须(直接或间接)从QObject派生。

槽是由应用程序程序员实现的。下面是Counter::setValue()槽的一个可能实现:

    void setVal(int val){
        if(m_val != val){
            m_val = val;
            // 发出信号
            emit valChanged(val);
        }
    }

emit行从对象中发出信号valueChanged(),并将新值作为参数。

下面的代码片段中,我们创建了两个计数器对象,并使用QObject::connect()将第一个对象的valueChanged()信号连接到第二个对象的setValue()插槽:

    Counter a,b;
    QObject::connect(&a,&Counter::valChanged,&b,&Counter::setVal);
    a.setVal(12);     qDebug("%d",b.m_val);
    a.setVal(100);    qDebug("%d",b.m_val);
    a.setVal(1000);   qDebug("%d",b.m_val);

用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的自动连接特性来实现。

在main.c文件中定义类并使用信号和槽的完整代码

#include 
class Counter : public QObject{
    Q_OBJECT

signals:
    void valChanged(int newVal);
public:
    void setVal(int val){
        if(m_val != val){
            m_val = val;
            // 发出信号
            emit valChanged(val);
        }
    }
    int m_val=0;
};

int main( )
{
    Counter a,b;
    QObject::connect(&a,&Counter::valChanged,&b,&Counter::setVal);
    a.setVal(10);     qDebug("%d",b.m_val);
    a.setVal(100);    qDebug("%d",b.m_val);
    a.setVal(1000);   qDebug("%d",b.m_val);
    return 0;
}
// 注意添加这句代码,强制使用moc工具
#include "main.moc"  

工程文件 .pro 使用console控台运行

CONFIG += c++11 console
SOURCES += \
        main.cpp

一个真实的例子

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

 #ifndef LCDNUMBER_H
 #define LCDNUMBER_H

 #include 

 class LcdNumber : public QFrame
 {
     Q_OBJECT

LcdNumber通过QFrame和QWidget继承了QObject,后者拥有大部分的信号插槽知识。它有点类似于内置的QLCDNumber小部件。
预处理器对Q_OBJECT宏进行扩展,以声明几个由moc实现的成员函数;如果编译器错误出现在“未定义引用vtable for LcdNumber”这一行,那么您可能忘记运行moc或在link命令中包含moc输出。

 public:
     LcdNumber(QWidget *parent = nullptr);

 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()是类与程序其余部分接口的一部分,因此槽是公共的。

有几个示例程序将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()与函数指针一起使用有几个优点。首先,它允许编译器检查信号的参数是否与槽的参数兼容。如果需要,编译器还可以隐式地转换参数。

您也可以连接到仿函数或c++ 11 lambdas:

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

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

当发送方或上下文被销毁时,lambda将断开连接。您应该注意,当信号发出时,函数内部使用的任何对象仍然是活的。

另一种将信号连接到插槽的方法是使用QObject::connect()以及信号和插槽宏。关于是否在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。您甚至可以在同一个项目中使用这两种机制。只需将下面一行添加到您的qmake项目(.pro)文件中。

 CONFIG += no_keywords

告诉Qt不要定义moc关键字信号、插槽和emit,因为这些名称将被第三方库使用,例如Boost。然后,要继续使用no_keywords标志使用Qt信号和槽,只需将源中Qt moc关键字的所有使用替换为对应的Qt宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOT(或Q_SLOT)和Q_EMIT。

见QLCDNumber、QObject::connect()、数字时钟示例、Tetrix示例、元对象系统和Qt的属性系统。

你可能感兴趣的:(#,Qt,Core)