信号和槽
信号和槽被用于两个对象之间的通信. 信号和槽的机制是Qt最重要的特征,或许是与其他架构的特性最不同的一个部分.
在图形界面编程中,当我们改变一个窗口部件的时候,我们经常希望另一个窗口部件被告知。 一般来说,我们希望任何类型的对象都能够去与另外一个进行通信。比如,如果一个用户点击一个Close按钮,我们大概希望窗口的close()函数被调用。
老的工具包通过回调来完成这种通信。一个回调即是一个函数的指针,因此如果你希望一个处理函数通知你一些事件,你可以传递一个函数(回调函数)的指针给这个处理函数。这个处理函数就会在适当的时候调用回调函数。回调有两大缺点:第一,它们不是类型安全的。我们从来不敢确定处理函数会用正确的参数来调用回调函数。第二,回调函数被强力和处理函数联系着,因为处理函数必须知道去调用哪个回调函数。
信号和槽
在Qt中,我们有一种替换回调的技术:我们使用信号和槽。当一个特定的事件发生的时候一个信号就被发射了。Qt的窗口部件有许多预定义的信号,但是我们通常自定义子类窗口部件来添加我们自己的信号给它们。一个槽就是一个被调用来响应一个特定信号的函数。Qt的窗口部件有许多预定义的槽,但是自定义子类窗口部件并添加你自己的槽因此你可以操作你所感兴趣的信号是常见的习惯。
信号和槽的机制是类型安全的:一个信号的签名必须与接收槽的签名相匹配。(实际上一个槽可能有一个比它所接收到的信号的签名更短的签名因为它能够忽略额外的参数。)因为签名是一致的,所以编译器能够帮助我们发现类型不匹配。信号和槽是松散的联系在一起的:一个发射信号的类从来不知道也不关心哪个槽接收这个信号。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;
};
一个小的基于Qt的类如下:
#include <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;
};
基于Qt 的版本有同样的内部状态,并提供公有的方法来访问状态, 但是另外它也支持用信号和槽的组件编程. 这个类可以通过发射一个信号, valueChanged(), 来告诉外界它的状态发生了变化,并且它还有一个其他对象可以对其发送信号的一个槽.
所有包含信号和槽的类必须在声明的顶部提到Q_OBJECT.
槽由应用程序的编写者来实现。这里是Counter::setValue()槽的一个可能的实现:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
emit这一行从对象中发射valueChanged()信号,而把新值作为参数。
在随后的代码段中,我们创建了两个Counter对象并把第一个对象的valueChanged()信号与第二个对象的setValue()槽用QObject::connect()连接起来:
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)导致a发射了一个valueChanged(12)信号,b的setValue()槽将收到这个信号:例如b.setValue(12)被调用。然后b会发射同样的valueChanged()信号,但是因为没有槽与b的valueChanged()信号相连,这个信号将被忽略。
注意只在value != m_value的时候setValue()函数改变数值并发射信号。这防止了循环连接(比如,b.valueChanged()与a.setValue()相连)导致的死循环的发生。
你做的每一个连接都会有一个信号发射,如果你复制一个里连接,就会有两个信号被发射。你也可以用QObject::disconnect()来破坏连接。
这个例子对象阐明了对象可以一起工作而不需要知道任何其他对象的信息。为达到它,对象只需要连接在一起,连接只需要调用一些简单的QObject::connect()函数,或者说有uic的automatic connections特点。
编译这个例子
C++预处理器改变或者移除signals,slots和emit这些关键词以使标准的C++C呈递给编译器。
在包含信号或槽的类定义上运行 moc,生成一个需要编译和与应用程序的其他对象文件连接的C++源文件。如果你使用qmake,makefile规则会自动把moc调用添加到你工程的makefile中。
信号
信号会在对象的内部状态发生某些改变并且这个改变可能被它的客户或者所有者感兴趣的时候被这个对象发射。只有定义了一个信号的类或者它的子类能够发射这个信号。
当一个信号被发射,与它相连的槽通常会立即执行,就像一个普通的函数调用一样。当这个发生的时候,信号和槽机制是完全独立于任何GUI事件循环的。一旦所有的槽都已经返回,emit之后的代码的执行就会发生。当使用queued connections 的时候情况会稍有不同;在这种情况下,关键词emit后的代码会立即继续,并且槽函数会在稍后执行。
如果很多槽都与一个信号连接,当信号发射的时候,槽函数会以任意的顺序相继的执行。
信号自动的由moc参数并且必须不在.cpp文件中实现。他们永远也返回不了类型(例如使用void)。
一个关于参数的备忘: 我们的经验告诉我们如果信号和槽不用专门的类型它们就更能重用。如果QScrollBar::valueChanged()使用特定的类型比如QScrollBar::Range,它只能与专门为QScrollBar设计的槽连接。像教程5中这种简单的程序将是不可能的。
槽
当连接到槽的信号被发射,槽就会被调用。槽函数是普通的C++函数并且能够被正常的调用;它们唯一的特点是信号能够与之相连。
因为槽是普通的成员函数,所以当它们被直接调用的时候它们遵循普通的C++规则。但是,作为槽,通过一个信号-槽连接,它们可以被任何部分调用,无论它的存取级别如何。这意味着从一个任意类的实例对象发射的信号可以引起一个私有的槽函数被一个毫无相关的类的实例对象所调用。
你也可以定义槽为虚函数,我们在实践中发现这非常有用。
与回调相比,信号和槽要稍微慢一些,因为它们提供增加的灵活性,但是这点区别在实际的应用中是无关紧要的。一般地,与非虚函数调用相比,发射一个与一些槽连接的信号,大概要比其立即调用接收函数慢十倍。这是定位连接对象需要的总开销,去反复安全地扫过所有连接(比如,检验后来的接收者在发射期间没有被销毁),以及用一种通用的方式扫遍任意的参数。例如十个非虚函数的调用可能听起来像一个槽,它的总开销比任何new或者delete操作要少。当你在需要new和delete的情形下,一旦你调用执行一个string,vector或者list操作,信号和槽的总开销只占整个函数调用所花费的一个很小的比例。
同样的,无论什么时候你在槽中进行一个系统调用或者间接的调用十个以上的函数。一台 i586-500的机器,你可以每秒钟发射2,000,000个左右与一个接收者连接的信号,或者每秒钟1,200,000个左右与两个接收者连接的信号。信号和槽机制的简单和灵活性是非常值得这点用户根本就不会注意到的开销的。
注意其他的定义变量称为signals or slots的库可能导致编译器在编译基于Qt的应用程序的时候发出警告或者出错。要解决这个问题,#undef这个惹是生非的符号。
元对象信息
元对象编译器(moc)在一个C++文件中解析这个类的声明并生成初始化这个元对象的代码。元对象包含这些函数的指针,以及所有信号和槽成员的名字。
元对象包含额外的信息比如对象的class name。你还可以检查一个对象是否继承一个特定的类,例如:
if (widget->inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget);
button->toggle();
}
元对象信息也可以用qobject_cast<T>(),与QObject::inherits()相似但是不没有那么容易出错:
if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
button->toggle();
更多内容,请参看Meta-Object System。
一个简单的例子
这里是一个简单的窗口部件注释.
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber通过QFrame and QWidget继承QObject, 它含有绝大多数signal-slot的知识. 这与内建的QLCDNumber窗口部件有某些相似.
Q_OBJECT宏由预处理器扩展来声明许多由moc实现的成员函数;如果你得到编译错误"undefined reference to vtable for LcdNumber",你很可能是忘记了 运行moc或者包含moc output在link命令行中.
public:
LcdNumber(QWidget *parent = 0);
这不与moc明显相关,但是如果你继承QWidget,你差不多确实希望在你的构造函数中有parent参数并且把它传递给基类的构造函数。
许多析构函数和成员函数在这里被省略了,moc忽略成员函数。
signals:
void overflow();
LcdNumber在被要求显示一个不可能的值的时候发射一个信号.
如果你不关心溢出,或者你知道溢出不可能发生,你可以忽略overflow()信号,比如不将它与任何槽连接。
如果另一方面你想在数字溢出的时候调用两个不同的error函数,仅仅需要连接这个信号给两个不同的槽。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 number持续的显示滚动条的值.
注意display()是过载的,当你连接一个信号给这个槽的时候,Qt将会选择适当的版本.使用回调,你得选择五个不同的名字并且自己跟踪这些类型.
一些不相关的成员函数在这个示例中予以省略.
信号与槽的高级用法
为了你可能需要信号发射者的信息的情况, Qt提供了QObject::sender()函数, 返回一个指向信号发射对象的指针.
QSignalMapper类被提供用于多个信号与同一槽相连并且这个槽需要不同的处理每一个信号的情形。
假设你有三个决定你将打开那个文件的按钮: "Tax File", "Accounts File", or "Report File".
为了打开正确的文件,你连接它们的QPushButton::clicked()信号给 readFile().然后,用QSignalMapper的setMapping()来映射所有的clicked() 信号到一个QSignalMapper对象.
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()。
用第三方的信号和槽使用Qt
用第三方的信号/槽机制来使用Qt也是可以的.你甚至可以在同一工程中使用两种机制.仅仅需要加上如下语句在你的qmake项目(.pro)文件中.
CONFIG += no_keywords
它告诉Qt不要定义moc关键词signals, slots,和emit,因为这些名字会在一个第三方库中被使用,比如. Boost.然后继续用带no_keywords标志的信号和槽, 简单的用对应的Qt 宏Q_SIGNALS, Q_SLOTS,和Q_EMIT替换你的源代码中所有的Qt moc关键词.
详细信息请同时参看 Meta-Object System 和Qt's Property System.
注:本文由roli翻译,转载请注明。
原文见http://doc.trolltech.com/4.4/signalsandslots.html