zz QT: Signals & Slots
QT的信号和信号槽
前言:
刚看到Qt中的这个东西,觉得很有意思,就把它翻译成中文了。
原文地址在:http://doc.qt.nokia.com/4.7/signalsandslots.html。
另:在youtube上有两个视频是专门讲这个的,可以作为辅助资料看看:
* Qt signals and slots - http://www.youtube.com/watch?v=yPhbgrApNx0
* C++ Qt 04 - Signals and Slots - http://www.youtube.com/watch?v=JtyCM4BTbYo
信号和信号槽被用来在对象之间通信。信号和信号槽机制是QT的核心特性,也是与其它类似框架的不同之处。
简介
在GUI编程中, 当我们改变一个控件时,通常我们也想让其它的控件被通知。一般情况下,我们想让不同种类的对象可以自由与其它对象进行通信。举个例子,当一个用户点击一个关闭按钮,我们可能想让windows的close()方法被调用。
老的图形工具库是使用callback来实现这类通信的。一个callback就是一个函数指针,所以当你想一个处理方法要通知你有一些事件需要处理时,你得传一个另外的函数的指针(其实就是callback)。然后处理方法会在合适的时候调用callback。Callback有两个很严重的瑕疵:首先,它们不是类型安全(type-safe)的。我们不能确保处理方法能用正确的参数调用callback。其次,callback过于强耦合-处理方法必需要知道哪个callback被调用。
信号和信号槽
在QT中,我们有一个callback技术的替代方案: 我们使用信号(signal)和信号槽(slot)。当一个特定的事件触发时一个对应的信号就会被发射。QT的控件(widget)拥有许多的预定义信号,当然我们也可以通过在widget的继承子类中添加自己的信号给它们。在一个特定的信号响应里一个被称之为信号槽(slot)的方法被调用。同样的,QT的控件也拥有许多的预定义信号槽,你知道的,我们也可以增加自己的信号槽来处理(handle)那些我们关注的信号。
abstract connections
|
信号和信号槽机制是类型安全的(type-safe):一个信号签名(signature)必需匹配一个接收信号槽的签名。(事实上,一个信号槽可能有一个比它接收到的信号短的签名,因为它可以过忽略掉额外的参数)因为签名是兼容的,那么编译器就可以帮我们找到类型不匹配。信号和信号槽是松耦合的:一个类发射一个信号不用知道也不想知道是哪个信号槽会接收它。Qt的信号和信号槽机制可保证的是当你连接(connect)一个信号到信号槽,在正确时间里信号槽肯定会被调用,带上信号的参数。信号和信号槽可以搞定任何类型的任意数量参数。它们全部都是类型安全的。
所有继承自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 <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版本的类有着同样的内部状态(state),也提供了public方法来访问状态,只是增加了信号和信号槽以便为组件式编程提供支持。这个类可以通过发射一个信号 valueChanged()来告诉外面的世界它的状态已经改变,并且它还有一个其它对象可以向它发送信号的信号槽。
所有的类要想包含信号和信号槽必需在它们的声明头部加上Q_OBJECT。也必需得继承自(直接或间接)QObject。
信号槽需要程序开发者来实现。这有一个Counter::setValue信号槽的大概实现:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
“发射行(emit line)”从对象中发射一个valueChanged信号,以新的值作为参数。在下面的代码片断里,我们创建两个Counter对象然后用QObject::connect方法将第一个对象的valueChanged信号连接(connect)到第二对象的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
Calling a.setValue(12) makes a emit a valueChanged(12) signal, which b will receive in its setValue() slot, i.e. b.setValue(12) is called. Then b emits the same valueChanged() signal, but since no slot has been connected to b's valueChanged() signal, the signal is ignored.
Note that the setValue() function sets the value and emits the signal only if value != m_value. This prevents infinite looping in the case of cyclic connections (e.g., if b.valueChanged() were connected to a.setValue()).
By default, for every connection you make, a signal is emitted; two signals are emitted for duplicate connections. You can break all of these connections with a single disconnect() call. If you pass the Qt::UniqueConnection type, the connection will only be made if it is not a duplicate. If there is already a duplicate (exact same signal to the exact same slot on the same objects), the connection will fail and connect will return false
This example illustrates that objects can work together without needing to know any information about each other. To enable this, the objects only need to be connected together, and this can be achieved with some simple QObject::connect() function calls, or with uic's automatic connections feature.
构建示例程序
在那些包含signals或slots关键字的类定义上运行moc,一个C++源代码文件会先编译成中间格式然后连同其它的对象一些编译和链接成一个程序。如果你使用qmake,那makefile的规则会自动调用moc。
信号
Signals are emitted by an object when its internal state has changed in some way that might be interesting to the object's client or owner. Only the class that defines a signal and its subclasses can emit the signal.
When a signal is emitted, the slots connected to it are usually executed immediately, just like a normal function call. When this happens, the signals and slots mechanism is totally independent of any GUI event loop. Execution of the code following the emit statement will occur once all slots have returned. The situation is slightly different when using queued connections; in such a case, the code following the emit keyword will continue immediately, and the slots will be executed later.
If several slots are connected to one signal, the slots will be executed one after the other, in the order they have been connected, when the signal is emitted.
Signals are automatically generated by the moc and must not be implemented in the .cpp file. They can never have return types (i.e. use void).
A note about arguments: Our experience shows that signals and slots are more reusable if they do not use special types. If QScrollBar::valueChanged() were to use a special type such as the hypothetical QScrollBar::Range, it could only be connected to slots designed specifically for QScrollBar. Connecting different input widgets together would be impossible.
信号槽
A slot is called when a signal connected to it is emitted. Slots are normal C++ functions and can be called normally; their only special feature is that signals can be connected to them.
Since slots are normal member functions, they follow the normal C++ rules when called directly. However, as slots, they can be invoked by any component, regardless of its access level, via a signal-slot connection. This means that a signal emitted from an instance of an arbitrary class can cause a private slot to be invoked in an instance of an unrelated class.
You can also define slots to be virtual, which we have found quite useful in practice.
Compared to callbacks, signals and slots are slightly slower because of the increased flexibility they provide, although the difference for real applications is insignificant. In general, emitting a signal that is connected to some slots, is approximately ten times slower than calling the receivers directly, with non-virtual function calls. This is the overhead required to locate the connection object, to safely iterate over all connections (i.e. checking that subsequent receivers have not been destroyed during the emission), and to marshall any parameters in a generic fashion. While ten non-virtual function calls may sound like a lot, it's much less overhead than any new or delete operation, for example. As soon as you perform a string, vector or list operation that behind the scene requires new or delete, the signals and slots overhead is only responsible for a very small proportion of the complete function call costs.
The same is true whenever you do a system call in a slot; or indirectly call more than ten functions. On an i586-500, you can emit around 2,000,000 signals per second connected to one receiver, or around 1,200,000 per second connected to two receivers. The simplicity and flexibility of the signals and slots mechanism is well worth the overhead, which your users won't even notice.
Note that other libraries that define variables called signals or slots may cause compiler warnings and errors when compiled alongside a Qt-based application. To solve this problem, #undef the offending preprocessor symbol.
元对象信息(Meta-object information)
The meta-object compiler (moc) parses the class declaration in a C++ file and generates C++ code that initializes the meta-object. The meta-object contains the names of all the signal and slot members, as well as pointers to these functions.
The meta-object contains additional information such as the object's class name. You can also check if an object inherits a specific class, for example:
if (widget->inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget);
button->toggle();
}
The meta-object information is also used by qobject_cast<T>(), which is similar to QObject::inherits() but is less error-prone:
if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
button->toggle();
See Meta-Object System for more information.
真实的例子
这是一个widget的带注释的例子
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber inherits QObject, which has most of the signal-slot knowledge, via QFrame and QWidget. It is somewhat similar to the built-in QLCDNumber widget.
The Q_OBJECT macro is expanded by the preprocessor to declare several member functions that are implemented by the moc; if you get compiler errors along the lines of "undefined reference to vtable for LcdNumber", you have probably forgotten to run the moc or to include the moc output in the link command.
public:
LcdNumber(QWidget *parent = 0);
It's not obviously relevant to the moc, 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.
Some destructors and member functions are omitted here; the moc ignores member functions.
signals:
void overflow();
LcdNumber emits a signal when it is asked to show an impossible value.
If you don't care about overflow, or you know that overflow cannot occur, you can ignore the overflow() signal, i.e. don't connect it to any slot.
If on the other hand you want to call two different error functions when the number overflows, simply connect the signal to two different slots. Qt will call both (in the order they were connected).
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
A slot is a receiving function used to get information about state changes in other widgets. LcdNumber uses it, as the code above indicates, to set the displayed number. Since display() is part of the class's interface with the rest of the program, the slot is public.
Several of the example programs connect the valueChanged() signal of a QScrollBar to the display() slot, so the LCD number continuously shows the value of the scroll bar.
Note that display() is overloaded; Qt will select the appropriate version when you connect a signal to the slot. With callbacks, you'd have to find five different names and keep track of the types yourself.
Some irrelevant member functions have been omitted from this example.
信号和信号槽使用默认参数
信号和信号槽的签名可能包含整数,这些参数可以是默认值。考虑QObject::destroyed():
void destroyed(QObject* = 0);
当QObject被删除时,它会发射它的QObject::destroyred()信号。我们想捕获到这个信号,无论在哪儿只要我们有一个指向已经删除的QObject引用,我们就可以清除它。一个大致的信号槽签名可能是这样:
void objectDestroyed(QObject* obj = 0);
要连接一个信号到信号槽,我们使用QObject::connect和SIGNAL()以及SLOT()宏。The rule about whether to include arguments or not in the SIGNAL() and SLOT() macros, if the arguments have default values, is that the signature passed to the SIGNAL() macro must not have fewer arguments than the signature passed to the SLOT() macro.
All of these would work:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
But this one won't work:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
...because the slot will be expecting a QObject that the signal will not send. This connection will report a runtime error.
高级信号和信号槽使用方法
For cases where you may require information on the sender of the signal, Qt provides the QObject::sender() function, which returns a pointer to the object that sent the signal.
The QSignalMapper class is provided for situations where many signals are connected to the same slot and the slot needs to handle each signal differently.
Suppose you have three push buttons that determine which file you will open: "Tax File", "Accounts File", or "Report File".
In order to open the correct file, you use QSignalMapper::setMapping() to map all the clicked() signals to a QSignalMapper object. Then you connect the file's QPushButton::clicked() signal to the QSignalMapper::map() slot.
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()));
Then, you connect the mapped() signal to readFile() where a different file will be opened, depending on which push button is pressed.
connect(signalMapper, SIGNAL(mapped(QString)),
this, SLOT(readFile(QString)));
Note: The following code will compile and run, but due to signature normalization, the code will be slower.
//slower due to signature normalization at runtime
connect(signalMapper, SIGNAL(mapped(const QString &)),
this, SLOT(readFile(const QString &)));
在Qt中使用第三方的信号和信号槽
在用Qt的同时使用一个第三方的信号/信号槽机制是可能的。你还可以在同一个项目里同时使用两个机制。只需要把下面那行代码加入你的qmake工程(.pro)文件里。
CONFIG += no_keywords
表示Qt不会定义signals, slots和emit 这几个moc关键字( moc keywords),因那些名称会被第三方库使用,比例Boost。想不带关键字(no_keywords flag)继续使用Qt的信号和信号槽,很简单,将代码中所有的Qt moc 关键字(Qt moc keywords)用相对应的Q_SIGNALS (或 Q_SIGNAL), Q_SLOTS (或 Q_SLOT), 和 Q_EMIT 替换掉。