信号槽

信号槽被用于对象间的通讯。信号槽机制是 Qt 的核心机制,可能也是 Qt 与其他框架的最大区别。


简介

GUI 编程中,当我们改变了一个组件,我们经常需要通知另外的一个组件。更一般地,我们希望任何类型的对象都能够与另外的对象通讯。例如,如果用户点击了关闭按钮,我们希望窗口的 close() 函数被调用。

早期工具库对这种通讯使用回调实现。回调是一个指向一个函数的指针,所以如果你希望某种事件发生的时候,处理函数获得通知,你就需要将指向另外函数的指针(也就是这个回调)传递给处理函数。这样,处理函数就会在合适的时候调用回调函数。回调有两个明显的缺点:第一,它们不是类型安全的。我们不能保证处理函数传递给回调函数的参数都是正确的。第二,回调函数和处理函数紧密地耦合在一起,因为处理函数必须知道哪一个函数被回调。


信号

当一个对象中某些可能会有别的对象关心的状态被修改时,将会发出信号。只有定义了信号的类及其子类可以发出信号。

当一个信号被发出时,连接到这个信号的槽立即被调用,就像一个普通的函数调用。当这种情况发生时,信号槽机制独立于任何 GUI 事件循环。emit 语句之后的代码将在所有的槽返回之后被执行。这种情况与使用连接队列略有不同:使用连接队列的时候,emit 语句之后的代码将立即被执行,而槽在之后执行。

如果一个信号连接了多个槽,当信号发出时,这些槽将以连接的顺序一个接一个地被执行。

信号由 [[moc]] 自动生成,并且不能够在 .cpp 文件中被实现。它们不能有返回值(例如使用 void)。

使用参数的注意事项:我们的经验是,不使用特定类型参数的信号和槽更容易被重用。如果 QScrollBar::valueChanged() 使用了特殊的类型,比如 QScrollBar::Range,那么它就只能被 [[QScrollBar]] 的特定的槽连接到,不可能同时连接不同的输入组件。


当连接到的信号发出时,槽就会被调用。槽是普通的 C++ 函数,能够被正常的调用。它们的唯一特点是能够与信号连接。

既然信号就是普通的成员函数,当它们像普通函数一样调用的时候,遵循标准 C++ 的规则。但是,作为槽,它们又能够通过信号槽的连接被任何组件调用,不论这个组件的访问级别。这意味着任意类的实例发出的信号,都可以使得不相关的类的私有槽被调用。

你也能把槽定义成虚的,这一点在实际应用中非常有用。

相比回调,信号槽要慢一些,因为它们提供了更大的灵活性,虽然对于正式的应用而言,这一点微不足道。一般来说,发出一个连接到槽的信号,要比直接调用接收函数慢大约十倍。它的开销主要在于定位连接对象,安全遍历所有连接(例如,检查信号发出期间,所有剩余的接收者没有被销毁),以一种普适的方式扫描所有参数。虽然是个非虚函数调用听起来很多,但是其开销仍然比 new 或者 delete 小很多。只要你需要操作字符串,向量或者链表,这些操作背后都有着 new 或者 delete,信号槽的开销只占总开销的很小一部分。

当你在槽中进行一次系统调用,或者间接调用十次函数,情况也是类似的。在 i586-500 上面,你可以向一个连接的接收者每秒发出 2,000,000 个信号,或者是向两个接收者每秒发出 1,200,000 个信号。信号槽机制的简单性和灵活性完全值得付出这些开销,这些开销用户是很难注意到的。

注意,如果在基于 Qt 的应用程序中混用了定义有同名变量 signals 或者 slots 的库的时候,可能会引起编译器警告和错误。在这种情况下,使用 #undef 接触预编译符号。


元对象信息

元对象编译器(moc)处理 C++ 文件中的类声明,并且生成初始化元对象的 C++ 代码。元对象包含有所有信号槽的名字,以及这些函数的指针。

元对象还含有额外的信息,例如对象的类名。你也可以检查一个对象是否QObject#inherits继承自某一特定的类,例如:

if(widget->inherits("QAbstractButton")){
    QAbstractButton *button =static_cast(widget);
    button->toggle();
}

元对象信息也被用于 qobject_cast(),这个宏类似于 QObject::inherits(),但是相对较少出现错误:

if(QAbstractButton *button = qobject_cast(widget))
    button->toggle();




信号和槽

Qt 中,我们有回调技术之外的选择:信号槽。当特定事件发出时,一个信号会被发出。Qt 组件有很多预定义的信号,同时,我们也可以通过继承这些组件,添加自定义的信号。槽则能够响应特定信号的函数。Qt 组件有很多预定义的槽,但是更常见的是,通过继承组件添加你自己的槽,以便你能够按照自己的方式处理信号。

Qt信号槽机制_第1张图片

信号槽机制是类型安全的:信号的签名必须同接受该信号的槽的签名一致(实际上,槽的参数个数可以比信号少,因为槽能够忽略信号定义的多出来的参数)。既然签名都是兼容的,那么编译器就可以帮助我们找出不匹配的地方。信号和槽是松耦合的:发出信号的类不知道也不关心哪些槽连接到它的信号。Qt 的信号槽机制保证了,如果你把一个信号同一个槽连接,那么在正确的时间,槽能够接收到信号的参数并且被调用。信号和槽都可以有任意类型的任意个数的参数。它们全部都是类型安全的。

所有继承自 QObject 或者它的一个子类(例如 QWidget都可以包含信号槽。信号在对象改变其状态,并且这个状态可能有别的对象关心时被发出。这就是这个对象为和别的对象交互所做的所有工作。它并不知道也不关心有没有别的对象正在接收它发出的信号。这是真正的信息封装,保证了这个对象能够成为一个组件。

槽能够被用于接收信号,也能够像普通函数一样使用。正如一个对象并不知道究竟有没有别的对象正在接收它的信号一样,一个槽也不知道有没有信号与它相连。这保证了使用 Qt 可以创建真正相互独立的组件。

你可以将任意多个信号连接到同一个槽上,也可能将一个信号连接任意多个槽。同时,也能够直接将一个信号与另一个信号相连(这会使第一个信号发出时,马上发出第二个信号)。

总值,信号槽建立起一种非常强大的组件编程机制。


一个简单的例子

一个最简单的 C++ 类:

class Counter
{
public:
    Counter(){ m_value =0;}
 
    int value()const{return m_value;}
    void setValue(int value);
 
private:
    int m_value;
};

一个基于 QObject 的最简单的类:

#include
 
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, SIGNAL(valueChanged(int)),
                 &b, SLOT(setValue(int)));
 
a.setValue(12);     // a.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 valueChanegd() 信号上面,因此该信号将被忽略。

注意,当且仅当 value != m_value 的时候,setValue() 才设置新的值,并且发出信号。这避免的无限循环的出现(否则的话,如果 b.valueChanged() 再与a.setValue() 相连,就会出现无限循环)。

默认情况下,一个连接发出一个信号,重复的连接将发出两个信号。你可以用过调用 disconnect() 函数来去除这些连接。如果你传入 Qt::UniqueConnection 类型,则当连接不是重复的时候才会被建立。如果已经存在重复的连接(所谓重复的连接,指的是同一个对象的相同的信号连接到相同的槽上面),这个连接将会失败,connect 返回 false

这个例子解释了对象可以在一起工作,而不需要知道彼此的任何信息。为了达到这一目的,只需要将它们连接起来,而这一操作通过简单地调用 QObject::connect() 函数,或者是 uic 自动连接特性即可实现。


构建例子

C++ 预处理器将使用标准 C++ signalsslots emit 关键字替换掉。

通过对含有信号槽的类运行 moc,将生成一个供应用程序中其他目标文件编译、链接的标准 C++ 源文件。如果你使用 qmakemakefile 将自动添加运行 moc 的规则。



一个真实的例子

这里有一个组件信号槽的例子:

#ifndefLCDNUMBER_H
#defineLCDNUMBER_H
 
#include
 
class LcdNumber :public QFrame
{
    Q_OBJECT

LcdNumber 通过 QFrame  QWidget 继承了 QObject,信号槽的很大一部分代码都是在这里实现的。这个类很像内置的 QLCDNumber 组件。

O_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();
    voidsetSmallDecimalPoint(bool point);
};

槽就是能够在别的组件改变状态时接收到信息的接收函数。正如上面的代码说明的那样,LcdNumber 使用槽来显示数字。因为 display() 是程序其他部分的接口,因此槽是公有的。

在这个例子里面,程序将 QScrollBar  valueChanged() 信号同 display() 槽连接在一起,于是这个 LCD 数字就可以连续地显示滚动条的数值了。

注意 display() 被重载了,当你向这个槽连接信号的时候, Qt 会自动选择合适的版本。如果使用回调的话,你不得不选择五个不同的名字,并且注意它们的类型。

这个例子已经省略了许多不相关的函数。


具有默认参数的信号和槽

信号和槽的签名可以包含参数,而这些参数也可以有默认值。请看 QObject::destroyed()

void destroyed(QObject*=0);

当一个 QObject 被删除的时候,它就会发出 QObject::destroyed() 信号。不论我们在哪里用到了这个 QObject 的引用,我们都希望能够捕获这个信号,以便做出清理。一个合适的槽可能是:

void objectDestroyed(QObject* obj =0);

为了将信号与槽连接起来,我们使用 QObject::connect() 函数以及 SIGNAL()  SLOT() 宏。是否需要在 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 的参数,而信号并没有发出这个参数。这个连接将出现一个运行时错误。


信号槽的高级使用

有些情况下,你可能需要知道信号发送者的信息,Qt 提供了 QObject::sender() 函数,其返回值是发送这个信号的对象的指针。

QSignalMapper 类可以用于解决很多信号连接到同一个槽,并且这个槽需要针对每一个信号做出不同的处理。

假如你有三个按钮,“Tax File”“Accounts File”“ReportFile”,来决定你将要打开哪一个文件。

为了打开正确的文件,你可以使用 QSignalMapper::setMapper() 函数,将所有的 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,SIGNAL(clicked()),
    signalMapper, SLOT (map()));
connect(accountFileButton,SIGNAL(clicked()),
    signalMapper, SLOT (map()));
connect(reportFileButton,SIGNAL(clicked()),
    signalMapper, SLOT (map()));

然后,你将 mapped() 信号同 readFile() 槽连接到一起。这个槽则通过查看哪一个按钮被点击,来打开不同的文件。

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


混合使用 Qt 和第三方信号槽

混合使用 Qt 同第三方库的信号槽机制也是可能的。你甚至可以在用一个项目中同时使用这两种机制。只需要在你的 qmake 文件(.pro)中添加下面一行:

CONFIG += no_keywords

这句告诉 Qt 不要定义 moc 关键字 signalsslots  emit,因为这些名字可能被第三方库使用,例如 Boost。为了在定义了 no_keywords 标记之后继续使用 Qt 的信号槽,需要将你的代码中的所有 moc 关键字替换为 Qt 的宏 Q_SIGNALS(或者是 Q_SIGNAL),Q_SLOTS(或者是 Q_SLOT),以及 Q_EMIT


转载处:http://qtdocs.sourceforge.net/index.php/信号槽



应注意的问题

信号与槽机制是比较灵活的,但有些局限性我们必须了解,这样在实际的使用过程中做到有的放矢,避免产生一些错误。下面就介绍一下这方面的情况。

1 .信号与槽的效率是非常高的,但是同真正的回调函数比较起来,由于增加了灵活性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的,通过在一台 i586-133 的机器上测试是 10 微秒(运行 Linux),可见这种机制所提供的简洁性、灵活性还是值得的。但如果我们要追求高效率的话,比如在实时系统中就要尽可能的少用这种机制。

2 .信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能产生死循环。因此,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射所接收到的同样信号。例如 , 在前面给出的例子中如果在 mySlot() 槽函数中加上语句 emit mySignal() 即可形成死循环。

3 .如果一个信号与多个槽相联系的话,那么,当这个信号被发射时,与之相关的槽被激活的顺序将是随机的。

4. 宏定义不能用在 signal 和 slot 的参数中。

既然 moc 工具不扩展 #define,因此,在 signals 和 slots 中携带参数的宏就不能正确地工作,如果不带参数是可以的。例如,下面的例子中将带有参数的宏 SIGNEDNESS(a) 作为信号的参数是不合语法的:

						 #ifdef ultrix 
    #define SIGNEDNESS(a) unsigned a 
    #else 
    #define SIGNEDNESS(a) a 
    #endif 
    class Whatever : public QObject 
    { 
    [...] 
    signals: 
        void someSignal( SIGNEDNESS(a) ); 
    [...] 
    };

5. 构造函数不能用在 signals 或者 slots 声明区域内。

的确,将一个构造函数放在 signals 或者 slots 区内有点不可理解,无论如何,不能将它们放在 private slots、protected slots 或者 public slots 区内。下面的用法是不合语法要求的:

						 class SomeClass : public QObject 
    { 
        Q_OBJECT 
    public slots: 
        SomeClass( QObject *parent, const char *name ) 
            : QObject( parent, name ) {}  // 在槽声明区内声明构造函数不合语法
    [...] 
    };

6. 函数指针不能作为信号或槽的参数。

例如,下面的例子中将 void (*applyFunction)(QList*, void*) 作为参数是不合语法的:

						 class someClass : public QObject 
    { 
        Q_OBJECT 
    [...] 
    public slots: 
        void apply(void (*applyFunction)(QList*, void*), char*); // 不合语法
    };

你可以采用下面的方法绕过这个限制:

      typedef void (*ApplyFunctionType)(QList*, void*); 
    class someClass : public QObject 
    { 
        Q_OBJECT 
    [...] 
    public slots: 
        void apply( ApplyFunctionType, char *); 
    };

7. 信号与槽不能有缺省参数。

既然 signal->slot 绑定是发生在运行时刻,那么,从概念上讲使用缺省参数是困难的。下面的用法是不合理的:

						 class SomeClass : public QObject 
    { 
        Q_OBJECT 
    public slots: 
        void someSlot(int x=100); // 将 x 的缺省值定义成 100,在槽函数声明中使用是错误的
    };

8. 信号与槽也不能携带模板类参数。

如果将信号、槽声明为模板类参数的话,即使 moc 工具不报告错误,也不可能得到预期的结果。 例如,下面的例子中当信号发射时,槽函数不会被正确调用:

						 [...] 
   public slots: 
       void MyWidget::setLocation (pair location); 
   [...] 
   public signals: 
       void MyObject::moved (pair location);

但是,你可以使用 typedef 语句来绕过这个限制。如下所示:

						 typedef pair IntPair; 
   [...] 
   public slots: 
       void MyWidget::setLocation (IntPair location); 
   [...] 
   public signals: 
       void MyObject::moved (IntPair location);

这样使用的话,你就可以得到正确的结果。

9. 嵌套的类不能位于信号或槽区域内,也不能有信号或者槽。

例如,下面的例子中,在 class B 中声明槽 b() 是不合语法的,在信号区内声明槽 b() 也是不合语法的。

						 class A 
    { 
        Q_OBJECT 
    public: 
        class B 
    { 
        public slots:   // 在嵌套类中声明槽不合语法
            void b(); 
        [....] 
        }; 
    signals: 
        class B 
    { 
        // 在信号区内声明嵌套类不合语法
        void b(); 
        [....] 
        }: 
    };

10. 友元声明不能位于信号或者槽声明区内。

相反,它们应该在普通 C++ 的 private、protected 或者 public 区内进行声明。下面的例子是不合语法规范的:

						 class someClass : public QObject 
    { 
        Q_OBJECT 
    [...] 
    signals: // 信号定义区
        friend class ClassTemplate; // 此处定义不合语法
    };