原文: http://doc.trolltech.com/4.0/signalsandslots.html
**********************************************************************
信号与槽
信号与槽是用于对象之间通信的。信号-槽机制是Qt的核心特征,可能也是与其它编程架构的特性最不同的地方。
介绍
在GUI编程里,我们改变一个widget时,常常希望另外一个widget得到通知。更一般的,我们希望任何类型的对象都可以彼此之间通信。比如,用户点击按钮Close的时候,我们可能希望窗口的close()函数被调用。
之 前的工具包通过回调(callback)来实现这个功能。一个callback就是一个函数指针。因为如果你希望某个处理函数(processing function)通知你某个事件,你可以将另外一个函数的指针(callback)传给这个函数。处理函数在适当的时候调用callback。 Callback有两个基本的缺陷:首先,不是类型安全的(type-safe)。我们永远无法确认处理函数调用callback时是否传入正确的参数。 其次,因为处理函数必须知道去调用哪个callback,所以callback与处理函数有高度的耦合。
信号与槽
在 Qt里,我们不用callback方式,我们用信号与槽(signals and slots)。某个事件发生时,一个信号被抛出。Qt的widget有很多已经定义的信号,不过我们可以通过继承来加入我们自己的信号。槽是相应于某个特 定信号而被调用的函数。Qt的widget已经定义了很多槽,但通过继承,加入自己的槽,以处理我们感兴趣的信号,这也是十分常见的。
信 号与槽机制是类型安全的:信号与接收信号的槽必须是参数表匹配的(The signature of a signal must match the signature of the receiving slot)——事实上,槽的参数表可以比它所响应的信号的参数表短,因为槽可以忽略多余的参数。因为参数表是匹配的,所以编译器可以帮助我们发现类型不匹 配。信号与槽是松耦合的:抛出信号的类永远不知道也不关心有哪个槽来接受该信号。Qt的信号与槽机制保证:如果信号被联系到某个槽上,则槽会在正确的时 间,以正确的信号参数,被调用。信号与槽可以带有任意个数和任意类型的参数。他们是绝对类型安全的。
QObject的 派生类或间接派生类(比如Qwidget的子类)都可以包含信号和槽。如果一个对象的状态改变可能引起其他对象的注意,那它就可以在此时抛出信号。所有的 对象都通过这样的方式来通信。它并不知道,也不关心它抛出的信号是否会被接收。这是真正的信息封装,保证了对象可以被用作软件组件。
槽是用来接收信号的,但它们也是普通的成员函数(member function)。正如对象不知道它的信号是否会被接收,槽也不知道它是否会连接到哪个信号。这保证了Qt可以产生真正独立的组件。
你可以将任意多的信号连接到一个槽上,或者将一个信号连接到任意多的槽上。甚至将一个信号直接连接到另一个信号上也是可以的(只要第一个信号被抛出,第二个信号立刻就抛出)。
信号与槽相结合,形成了一个强大的组件编程机制。
一个小例子
最小的C++类的声明可能是这样的:
class Counter
{
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
void setValue(int value);
private:
int m_value;
};
基于Qobject的微型的类可以是这样的:
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的信号valueChanged(),所以这个信号被忽略了。
注意,setValue()函数设置value,仅在value != m_value时才抛出信号。这避免了在环形连接(这个例子里也就是说b.valueChanged()与a.setValue()被连接)时的无限循环。
你作出的每个连接都抛出一个信号;如果你重复一个连接,则会抛出两个信号。你也随时可以通过Qobject::disconnect()来打破连接。
这个例子表明,对象可以在彼此不了解一点信息的情况下协同工作。为达到这一点,对象只需要被连接在一起,这通过几个简单的Qobject::connect()函数就可以实现,或者使用uic的自动连接(automatic connection)特性也可以实现。
编译这个例子
C++预处理器改变或去除signals,slots和emit等关键字,来使得编译器得到的是标准的C++。
对包含有信号或/和槽的类定义,使用moc,可以产生一个C++源文件。这个源文件可以编译并链接到程序的其他object文件。如果使用qmake,可以将自动调用moc的makefile规则加入到工程的makefile文件里。
信号
如果对象的内部状态改变对它的客户或者拥有者可能会有一定的意义,那这个对象就可以抛出信号。只有定义了信号的类和它的子类,可以抛出这个信号。
信 号抛出时,通常,与之连接的槽会立刻执行,就像普通的函数调用。在这种情况下,信号与槽机制完全独立于任何的GUI时间循环。在emit语句之后的代码会 在所有的槽返回之后执行。使用连接队列(queued connection)的话,情况会有一些不同:在这种情况下,emit语句后的代码会立即顺序执行,而所有的槽会在稍后才执行。
如果几个槽连接到同一个信号,那么信号抛出时,槽会一个接一个执行,执行顺序是任意的。
信号由moc自动处理,绝不可以在.cpp文件中给出实现。信号永远不会有返回类型(也就是说,使用void)。
关 于参数,需要注意一点:经验表明,如果不使用特殊的参数类型,信号和槽会具有更好的可重用性。如果QscrollBar::valueChanged() 使用一个特殊的类型,比如说假定的QscrollBar::Range,那么它就只能与专门针对的QscrollBar的槽连接了。那么,要跟教程5里那 样的简单,是不可能了。
槽
槽在与之连接的某个信号抛出时被调用。槽是普通的C++函数,可以被正常调用;他们唯一特殊的地方在于可以把信号与他们相连接。槽的参数不可以有默认值,同时,与信号一样,在槽的参数中使用自定义的类型也往往是不明智的。
既然槽是只有一点点不一样的普通成员函数,所以他们与普通成员函数一样有访问权限问题。槽的访问权限决定了谁可以连接到它:
对公共(public)槽,任何对象都可以将信号连接到它。这对组件编程十分有用:你创建彼此完全不知情的对象,连接他们的信号和槽,信息就可以正确的传递,就像一个模型铁路,建立起来之后就可以让它自己运行了。
对保护(protected)槽,只有这个类及它的子类可以将信号连接到它。这种槽往往只是类实现的一部分,而不是类对外部世界的接口。
对私有(private)槽,只有这个类自己才可以将信号连接到它。这可以用在联系非常紧密的类之间(阿瓦理解:友元?),甚至连子类都不足以信任以得到这个连接的权利。
也可以将槽定义为虚(virtual)函数,在实际应用中也是十分有用的。
相 对于回调(callback),信号与槽机制因为其增加的灵活性,因此稍微慢一点,不过这种区别对实际应用而言很不明显。一般来说,抛出一个连接到几个槽 的信号,大约比直接调用槽函数(不使用虚函数的情况下)要慢10倍左右。这是以下一系列动作所需要的耗费(overhead):定位连接对象,安全地遍历 所有连接(也就是,检查在信号抛出期间没有被销毁的信号接收者——阿瓦理解:信号抛出时,所有连接的槽函数都返回以后,抛出信号处才继续往下执行,这里有 一个时间的间隔,在此间隔里,可能有某些连接槽函数的对象会被销毁。),以一种普适的样式(generic fashion)扫描(marshall)所有参数(to marshall any parameters in a generic fashion——阿瓦:没学过编译,不知道应该怎么翻译-_-## )。尽管10个非虚函数(non-virtual function)调用听起来很多,但实际上,这耗费总的来说比任何一个new或delete操作要少得多。一旦你需要操作字符串,向量或者列表(这些情 况下,都是需要背后调用new或delete的),在整个函数调用的耗费中,与信号和槽相关的只占很小的一部分。
在槽 函数中做一个系统调用的时候,或者间接调用超过10个函数的时候,情况也是一样的(阿瓦:与new或delete一样)。在一个i586-500上面,每 秒可以抛出2,000,000个只有一个接收者的信号,或者大概1,200,000个有两个接收者的信号。信号与槽的机制的简单性和灵活性,使得它的耗费 是值得的,何况程序的用户对这些耗费甚至都感觉不到。
注意,编译一个基于Qt的程序,在使用其他库时,如果其中定义了变量叫signals或者slots,可能会导致编译器的警告和错误。要解决这个问题,用#undef解除冲突的预编译符号。
元对象(Meta-Object)信息
元对象编译器(moc) parse在C++文件里的类声明,产生初始化元对象的C++代码。元对象包含所有信号与槽的名字,以及指向它们的指针。
之外,元对象包含其它的信息,比如对象的类名(class name)。你也可以检查一个对象是否继承了某个类,比如这样:
if (widget->inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget);
button->toggle();
}
qobject_cast<T>同样使用元对象信息,这个函数与QObject::inherits()很相像,但相对比较少产生错误(less error-prone):
if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
button->toggle();
在Meta-Object System里有更多信息。
真实的例子
下面简单对一个widget的类做讲解:
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber通过QFrame和QWidget间接继承了QObject。大部分信号-槽的信息都在QObject里。LcdNumber与内建的QLCDNumber类有点相似。
预 编译器打开宏Q_OBJECT,声明若干个由moc处理(implement)的成员函数。如果得到类似于“undefined reference to vtable for LcdNumber”的编译错误(if you get compiler errors along the lines of "undefined reference to vtable for LcdNumber"),你可能是忘记了执行moc,或者忘记了将moc输出加入到link命令里。
public:
LcdNumber(QWidget *parent = 0);
这 看起来与moc没什么直接关系,不过如果你继承了QWidget,你几乎是当然需要在构造函数里加上parent参数,并将其传给基类的构造函数。 (but if you inherit QWidget you almost certainly want to have the parent argument in your constructor and pass it to the base class's constructor.)
这里省略了析构函数(destructor)和几个成员函数;moc忽略成员函数。
signals:
void overflow();
在需要显示一个不可能的数值时,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
槽,是用来得到其他widget状态改变的接收函数。如上所示,LcdNumber用它来显示数值。因为display()是这个类对程序其他部分的接口,所以槽是公共的(public)。
在几个例子里,将QScrollBar对象的valueChanged()信号连接到上面提到的display()槽,因此LCD连续地显示出滚动条(scroll bar)的数值。
注意,display()是被重载(overload)了的;在你连接信号到槽时,Qt会选择正确的版本。用回调(callback)的话,你不得不使用5个不同的名字,并得自己留意类型的问题。
这个例子省略去几个不相关的成员函数。
请参考Meta-Object System和Qt’s Property System。
======================================================
原文来自Qt Assistant, Copyright © 2005 Trolltech Qt 4.0.0
转自:http://www.qtcn.org/bbs/read.php?tid=2404