QT之信号槽(一)

来源:互联网 发布:虫虫钢琴软件 编辑:程序博客网 时间:2024/05/18 01:55

        学习一门语言最好的方式就是看文档。本文翻译于 Qt 帮助文档:Signals & Slots


Signals & Slots

        信号和槽被用于两个对象之间的通讯。信号槽机制是 Qt 核心的特征,并且可能是与其他框架提供的特征最大不同之处。Qt meta-object system 使信号槽成为可能。


Introduction

        在GUI编程,当我们改动一个部件,我们经常希望另外一个部件可以被通知到。更一般的来讲,我们希望任何类型的对象都可能与其他对象通讯。例如,一个用户点击一个 Close 按钮,我们可能希望窗口 close() 函数被调用。

        其它工具包使用回调函数实现了这种通讯。回调是指向函数的指针,因此如果你想一个处理函数去通知你所关心的一些事件,你要传递指向另外一条函数的指针到这条处理函数。然后处理函数在适当的时候就会调用回调函数。虽然成功的框架使用这种方式确实存在,回调函数是直观的并且可以容忍一些来自确保回调参数类型正确性的问题。


Signals and Slots

        在 Qt 里面,我们有一个替代回调的技巧: 我们可以用信号槽。当一个特别的事件发生的时候,一个信号就会被发射。Qt 部件有很多的预定义的信号,但是我们总是可以子类化部件并添加我们自己的信号至里面。一个槽是一个函数,它被响应特定信号所调用。Qt 部件有许多的预定义槽,一个共同的惯例是子类化部件并添加你自己的槽,以至于你可以处理你感兴趣的信号。

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

        所有的类都继承自 QObject 及它的子类包括信号和槽。当它以一种方式改变其状态,其他对象对其感兴趣,它就会发射信号。这是所有对象进行通信的目的。它不知道或不关心是否有任何东西接受它所发出的信号。这是真正的信息封装,并且确保对象可以作为一个软件组件去使用。

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

        你可以连接许多的信号到一个槽中,并且一个信号可以连接到许多你需要的槽中,甚至可以直接连接一个信号到另外一个信号。(第一个信号发射后,第二个信号会立即发出)

        同时,信号和槽确保了一个强而有力的组件编程机制。


Signal

        当内部状态发生改变时,一个对象会发射信号,在某种意义上可能被对象客户端或拥有者感兴趣。信号是一个公共的函数并且可以在任何地方发射,但是我们推荐仅仅是在定义它们的类和子类里面发射。

        当一个信号被发射后,连接的槽通常会立即被执行,就像一个正常的函数调用。当它发生的时候,信号槽机制总是独立于任何的 GUI 事件循环。执行 emit 后面的代码,所有的槽都会返回一次。这种情况略有不同于当使用 queued connecttions;在这种情况下,emit 关键字后面的代码立刻继续执行,并且槽稍后将会被执行。

        如果若干个槽连接到一个信号上,当信号发射时,这些槽将会一个接一个执行,按照它们连接的顺序。

        信号通过 moc 自动生成并且必须不能实现在 .cpp 文件里面,它们从来没有返回类型(例如,使用 void)

        一个关于参数的备注:我们的经验显示如果信号和槽没有使用特殊的类型,它们很多可以重复使用。如果 QScrollBar::valueChanged() 使用特殊的类型正如假想的 QScrollBar::Range,它仅可以连接 QScrollBar 设计特殊的槽。不同的输入部件是不可能的。


Slots

        当一个连接它的信号发射,一个槽就会被调用。槽是一个正常的 C++ 函数并且可以被正常调用;它们仅有的特征是信号可以连接到它们。

        既然槽是正常的成员函数,当你直接调用它们时就遵循正常的 C++ 规则。无论如何,作为槽,经过一个信号槽连接,它们可以被任何的组件调用,不管它访问级别。这意味着一个任意类的实例发出信号可以引起一个不相关类的实例的私有槽被调用。

        你也可以把槽定义为虚拟的,我们在实践中发现这是十分有用的。

        与回调函数相比,信号和槽要稍微慢一些,因为它们提供、增强了灵活性,但在实际中的差异是无关紧要的。一般来说,发射一个信号到连接它的一些槽,它接近比直接调用接受者们和非虚函数一起调用慢 10 倍(例如,检查它随后的接受者在发射期间是否有被销毁)。这些开销用于定位连接对象,安全的遍历所有连接,并且在通用的使用中调度任何参数。10 个非虚函数调用可能听起来有点多,但它的开销要少于任何的 new 或 delete 操作。例如,只要你执行一个字符串,向量或列表操作它后面的场景需要 new 或 delete,信号槽的开销只占整个函数调用很小的一部分。当你在槽中系统调用也是如此;或间接呼叫超过 10 个函数。信号槽机制简单和灵活是值得开销的,你的用户甚至不会注意到。

        请注意,当编译一个基于 Qt 的应用时,库或变量名叫 signals 或 slots 可能引起编译警告和错误。#undef 违反预处理符号解决这个问题。  


A Small Example

        一个小的 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_OBJECTpublic:    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() 从一个对象,并用一个新的值作为参数。

        在接下来的代码片段,我们创建了 2 个计数器对象,并使用 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 类型,如果它不是一个重复连接,连接才会被创建。如果这里已经是一个重复连接(完全相同的信号至完全相同的槽在相同的对象上),连接将会失败并且连接函数将会返回失败。

        这个例子阐明了一个对象可以和其他对象一起工作而不需要知道彼此的任何信息。为了支持它,对象仅需要去彼此连接,并且调用一些简单的 QObject::connect 函数就可以实现,或者使用 uic 之{自动连接} 功能。


A Real Example

        这里是一个部件简单的注释:

#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 = 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() 和程序剩余部分是接口的一部分,那么槽就是公共的。

        若干个程序样例连接 QScrollBar 的 valueChange() 信号到 display() 槽,因此 LCD 数字连续性显示滚动条的数值。

        注意,display() 是重载的。当你连接一个信号到一个槽,Qt 将会选择合适的版本。通过回调,你有5个不同的名称并保持你自己的类型追踪。

        这个例子里一些不相干的成员函数有被省略。


Signals And Slots With Default Arguments

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

    void destroyed(QObject* = 0);
        当一个对象被删除,它就会发射 QObject::destroyed() 这个信号。我们想捕获这个信号,我们可能有一个悬空的引用到这个删除对象,因此我们可以清除它。一个合适的槽签名可能是:

    void objectDestroyed(QObject* obj = 0);
        连接信号到槽,我们可以用 QObject::connect()。这里有若干种方式连接信号和槽。第一种是使用函数指针:

    connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
        这里有若干高级方式去使用 QObject::connect 和函数指针一起。首先,它允许编译器检查信号的参数和槽参数的兼容性。如果你需要,参数也可以通过编译器隐式转换。
        你也可以连接到函数因子或者 C++11 lambda 表达式:

    connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); });
        注意,如果你的编译器不支持 C++11 可变模板,如果你的信号和槽有6个或以下的参数,这个语法才能工作。

        另外一种方式连接一个信号和一个槽是使用 QObject::connect 和 SIGNAL 和 SLOT 宏。这个规则有关在 SIGNAL() 和 SLOT() 宏是否包含有参数,如果参数有默认值,必须不能有比传递到 SLOT() 宏参数签名更少的参数签名传递到 SIGNAL() 宏。

        下面这些可以工作:

    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 重载时, 信号和槽的参数将不会被编译器所检查。


Advanced Signals and Slots Usage

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

        QSignalMapper类是为多个信号连接到相同的槽的情况提供的,并且槽需要去处理每个不同的信号。

        假设你有3个按钮,它们决定你将打开哪个文件:“Tax File”,“Accounts File”,或“Report File”。

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

    signalMapper = new QSignalMapper(this);    signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));    signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));    signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));    connect(taxFileButton, &QPushButton::clicked, signalMapper, &QSignalMapper::map);    connect(accountFileButton, &QPushButton::clicked, signalMapper, &QSignalMapper::map);    connect(reportFileButton, &QPushButton::clicked, signalMapper, &QSignalMapper::map);

        然后,你连接 mapped() 信号到 readFile() ,不同的文件将会被打开,取决于哪个按钮有按压。

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


Using Qt With 3rd Party Signals and Slots

        使用 Qt 支持第三方信号槽机制是可能的。你可以甚至使用两个机制在相同的工程。只需要添加下面一行到你的 qmake project(.pro) 文件。

    CONFIG += no_keywords
        它告诉 Qt 不要定义 moc 关键字 signals, slots, 和 emit , 因为这些名字将被第三方库所使用,譬如,Boost。然后继续使用 Qt 信号和槽和 no_keywords 标记,简单的替换你源代码里所有 Qt moc 关键字使用相对应 Qt 宏 Q_SIGNALS(或 Q_SIGNAL), Q_SLOTS(或Q_SLOT),和 Q_EMIT。


        也请看 Meta-Object System 和 Qt's Property System。