• Roson的Qt之旅 #121 Qt信号和槽详细介绍


    CSDN话题挑战赛第2期
    参赛话题:Qt应用程序开发

    1.Signals & Slots 

    信号和槽用于对象之间的通信。信号和槽机制是Qt的一个核心特征,可能是与其他框架提供的特征差别最大的部分。信号和槽是由Qt的元对象系统实现的。

    在GUI编程中,当我们改变一个小部件时,我们经常希望另一个小部件得到通知。更广泛地说,我们希望任何类型的对象都能相互沟通。例如,如果一个用户点击了一个关闭按钮,我们可能希望窗口的close()函数被调用。

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


    在Qt中,我们有一个回调技术的替代品——信号和槽。当一个特定的事件发生时,一个信号就会被发射出来。Qt的widget有许多预定义的信号,但我们总是可以对widget进行子类化,为它们添加我们自己的信号。一个槽是一个响应特定信号而被调用的函数。Qt的widget有许多预定义的槽,但通常的做法是对widget进行子类化并添加自己的槽,这样你就可以处理你感兴趣的信号。

     

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

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

    槽可以用来接收信号,但它们也是正常的成员函数。就像一个对象不知道是否有东西接收它的信号一样,一个槽也不知道它是否有任何信号连接到它。这确保了可以用Qt创建真正独立的组件。
    你可以在一个槽上连接任意多的信号,而一个信号可以连接到任意多的槽上。甚至可以将一个信号直接连接到另一个信号。(这将在第一个信号发出时立即发出第二个信号)。

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

    1.1信号

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

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

    如果几个槽连接到一个信号,当信号发射时,这些槽将按照连接的顺序一个接一个地执行。

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

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


    1.2槽

    当与之相连的信号被发出时,槽就被调用。槽是普通的C++函数,可以被正常调用;它们唯一的特点是可以将信号连接到它们。

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

    你也可以将槽定义为虚拟的,我们发现这在实践中相当有用。

    与回调相比,信号和槽的速度稍慢,因为它们提供了更多的灵活性,尽管对于实际应用来说,这种差异是微不足道的。一般来说,发射一个连接到一些槽的信号,比直接调用接收器,用非虚拟函数调用要慢十倍左右。这是定位连接对象、安全地遍历所有连接(即检查随后的接收者在发射过程中是否被破坏)以及以通用方式调集任何参数所需的开销。虽然十个非虚拟函数调用听起来很多,但这比任何新建或删除操作的开销小得多,例如。只要你执行一个字符串、向量或列表操作,在幕后需要new或delete,信号和槽的开销就只占整个函数调用成本的很小一部分。每当你在槽中进行系统调用时,情况也是如此;或者间接调用超过十个函数。信号和槽机制的简单性和灵活性是非常值得的,你的用户甚至不会注意到这些开销。
    请注意,当与基于Qt的应用程序一起编译时,其他定义了称为signals或者slots的变量的库可能会引起编译器的警告和错误。要解决这个问题,可以#undef违规的预处理器符号。

    一个小例子
    一个最小的C++类声明可能是这样的:

    1.   class Counter
    2.   {
    3.   public:
    4.       Counter() { m_value = 0; }
    5.       int value() const { return m_value; }
    6.       void setValue(int value);
    7.   private:
    8.       int m_value;
    9.   };

    一个基于QObject的小类,比如:

    1.   #include
    2.   class Counter : public QObject
    3.   {
    4.       Q_OBJECT
    5.   public:
    6.       Counter() { m_value = 0; }
    7.       int value() const { return m_value; }
    8.   public slots:
    9.       void setValue(int value);
    10.   signals:
    11.       void valueChanged(int newValue);
    12.   private:
    13.       int m_value;
    14.   };

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

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

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

    1.   void Counter::setValue(int value)
    2.   {
    3.       if (value != m_value) {
    4.           m_value = value;
    5.           emit valueChanged(value);
    6.       }
    7.   }

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

    在下面的代码片段中,我们创建了两个计数器对象,并使用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的自动连接功能来实现。


    一个真实的例子
    下面是一个简单的小部件的例子:

    1.   #ifndef LCDNUMBER_H
    2.   #define LCDNUMBER_H
    3.   #include
    4.   class LcdNumber : public QFrame
    5.   {
    6.       Q_OBJECT
    7. LcdNumber通过QFrame和QWidget继承了QObject,它拥有大部分的信号槽知识。它与内置的QLCDNumber widget有些类似。
    8. Q_OBJECT宏被预处理器扩展来声明几个成员函数,这些函数由moc实现;如果你得到类似于 "undefined reference to vtable for LcdNumber "的编译器错误,你可能忘记运行moc或者在链接命令中包含moc的输出。
    9.   public:
    10.       LcdNumber(QWidget *parent = 0);
    11. 这与moc没有明显的关系,但是如果你继承了QWidget,你几乎肯定希望在你的构造函数中拥有父参数,并将其传递给基类的构造函数。
    12. 这里省略了一些析构器和成员函数;moc忽略了成员函数。
    13.   signals:
    14.       void overflow();
    15. 当LcdNumber被要求显示一个不可能的值时,它会发出一个信号。
    16. 如果你不关心溢出,或者你知道溢出不可能发生,你可以忽略overflow()信号,也就是说,不要把它连接到任何槽。
    17. 另一方面,如果你想在数字溢出时调用两个不同的错误函数,只需将该信号连接到两个不同的槽。Qt将调用这两个函数(按照它们被连接的顺序)。
    18.   public slots:
    19.       void display(int num);
    20.       void display(double num);
    21.       void display(const QString &str);
    22.       void setHexMode();
    23.       void setDecMode();
    24.       void setOctMode();
    25.       void setBinMode();
    26.       void setSmallDecimalPoint(bool point);
    27.   };
    28.   #endif

    槽是一个接收函数,用来获取其他部件中状态变化的信息。LcdNumber使用它,如上面的代码所示,用来设置显示的数字。由于display()是该类与程序其他部分的接口的一部分,槽是公共的。
    有几个例子程序将QScrollBar的valueChanged()信号连接到display()槽,所以LCD数字持续显示滚动条的值。

    注意,display()是重载的;当你把信号连接到槽上时,Qt会选择合适的版本。如果这里用的是回调,你就必须要自己跟踪参数类型,自己判定选择调用哪个槽函数。

    本例中省略了一些不相关的成员函数。

    2.带有默认参数的信号和槽

    信号和槽的签名可以包含参数,而且参数可以有默认值。考虑一下QObject::destroy():

      void destroyed(QObject* = 0);

    当一个QObject被删除时,它会发出这个QObject::destroy()信号。我们想要捕捉这个信号,无论在哪里,我们都可能有一个对被删除的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()和SIGNAL和SLOT宏。关于是否在SIGNAL()和SLOT()宏中包含参数的规则,如果参数有默认值,那么传递给SIGNAL()宏的签名不能比传递给SLOT()宏的签名少。
    所有这些都可以工作:

    1.   connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
    2.   connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
    3.   connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

    但这一招并不奏效:

      connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

    因为槽将期待一个信号不会发送的QObject。这个连接将报告一个运行时错误。
    注意,当使用这个QObject::connect()重载时,信号和槽的参数不会被编译器检查。

    高级信号和槽的用法

    对于你可能需要信号发送者的信息的情况,Qt提供了QObject::sender()函数,它返回一个指向发送信号的对象的指针。

    QSignalMapper类是为许多信号被连接到同一个槽,而槽需要以不同的方式处理每个信号的情况提供的。

    假设你有三个按钮,决定你要打开哪个文件。"税务文件"、"账户文件 "或 "报告文件"。

    为了打开正确的文件,你使用QSignalMapper::setMapping()将所有的QPushButton::clicked()信号映射到一个QSignalMapper对象。然后你把文件的QPushButton::clicked()信号连接到QSignalMapper::map()槽。

    1.       signalMapper = new QSignalMapper(this);
    2.       signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
    3.       signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
    4.       signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));
    5.       connect(taxFileButton, &QPushButton::clicked,
    6.           signalMapper, &QSignalMapper::map);
    7.       connect(accountFileButton, &QPushButton::clicked,
    8.           signalMapper, &QSignalMapper::map);
    9.       connect(reportFileButton, &QPushButton::clicked,
    10.           signalMapper, &QSignalMapper::map);

    然后,你把mapped()信号连接到readFile(),在那里,一个不同的文件将被打开,这取决于哪个按钮被按下。

    1.       connect(signalMapper, SIGNAL(mapped(QString)),
    2.           this, SLOT(readFile(QString)));

    在第三方信号和槽中使用Qt

    可以将Qt与第三方信号/插槽机制一起使用。你甚至可以在同一个项目中使用两种机制。只需在你的qmake项目(.pro)文件中加入以下一行。

      CONFIG += no_keywords

    它告诉Qt不要定义moc关键字signals、slots和emit,因为这些名字会被第三方库使用,例如Boost。那么要继续使用带有no_keywords标志的Qt信号和槽,只需用相应的Qt宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOTS(或Q_SLOT)和Q_EMIT替换你的源代码中对Qt moc关键字的所有使用。
    另请参见元对象系统和Qt的属性系统。

  • 相关阅读:
    32 | 基于LoadRunner实现企业级服务器端性能测试的实践(上)
    18-1、k8s 对外服务之ingress
    使用SSM为学校医务室开发一套管理系统
    C++学习6-类和对象
    搜索留痕推广引流软件的作用#川圣SEO#蜘蛛池
    React中Toast 库推荐
    SpringBoot定时任务 - Spring自带的定时任务是如何实现的?有何注意点?
    JavaScript之事件
    PyCharm 虚拟环境搭建
    HTML期末作业:基于html+css+javascript+jquery实现古诗词网页 学生网页设计作品 web前端开发技术 web课程设计 网页规划与设计
  • 原文地址:https://blog.csdn.net/jolin678/article/details/126960423