信号(Signals)
只有定义了信号的类及其子类可以发出信号。
一个信号发出后,连接的槽通常会立即执行,就像一个普通的函数调用。信号与槽机制完全独立于GUI的事件循环(event loop)。emit语句后的代码在所有的槽函数返回后执行。当使用queued connections时情形稍有不同。在这种情况下emit关键字后的代码会立即执行,而槽函数会稍后执行。
如果几个槽连接一个信号,当信号发出后这几个槽会按照连接的顺序一个接一个执行。
信号是由moc自动生成的,不能在.cpp文件中实现。信号没有返回类型(比如,void)。
关于参数有一个要注意的地方:我们的经验表明如果信号与槽不是使用特殊的类型是可以很好的重用的。如果QScrollBar::valueChanged()使用了特殊的类型比如QScrollBar::Range,它就只能连接为QScrollBar特定设计的槽。连接两个不同的输入Widget就不可能了。
槽(Slots)
当信号发出后,连接的槽就会被调用。槽是普通的c++函数,可以正常的调用;槽唯一的特性就是信号可以和它连接。
槽就是普通的c++函数,它遵守c++的规则。然而,作为槽,它们可以通过建立信号与槽的连接被任何组件调用而不考虑访问级别(private,public)。这意味着一个任意类实例发出的信号会导致另一个不相关类实例的私有槽被调用。
你也可以把槽定义为虚函数,实践中这个很有用。
和回调相比,信号与槽由于增加了灵活性,稍微慢一点,但是在现实的应用中这种差别是很细微的。总的来说,信号与槽比直接调用非虚函数慢大约10倍。开销用在了定位连接对象,安全的遍历所有连接(比如检查信号的发出的过程中,接受者没有被销毁),封送任何泛型风格的参数。10倍的非虚函数调用开销也许听着很多,但其实比任何new或delete操作的开销要小。
元对象信息
源对象编译器(moc)解析c++文件中的类定义,生成初始化元对象的c++代码。源对象包含了所有信号和槽成员的名称,以及指向这些函数的指针。
源对象包含了额外的信息,比如,对象的类名称。你可以检查一个对象是否继承自某个特定的类。
if (widget->inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget);
button->toggle();
}
qobject_cast<T>()同样可以使用源对象信息,这有点类似QObject::inherits(),但是容易出错。
+ expand sourceif (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))button->toggle();
一个真实的例子
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber继承自QObject,包含了大多数的信号与槽。有点类似内建的QLCDNumber Widget.
为了声明moc生成的成员函数,预处理器扩展Q_OBJECT宏;如果你看到"undefined reference to vtable forLcdNumber“编译器错误,可能是忘了运行moc或是未把moc的输出包含在链接命令中。
public:
LcdNumber(QWidget *parent = 0);
尽管和moc的联系不明显,但是如果继承自QWidget,几乎肯定类构造器需要有父类的参数,并把它传给父类的构造器。
析构器和成员函数在此省略了;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
有一个用于接收其他Widget状态改变信息的槽。LcdNumber使用它设置显示的数字。因为display()是类接口的一部分,所以槽式公开的。
几个实例程序把QScrollBal的valueChanged()信号和display()槽做连接,所以LCD数字可以持续的显示滚动条的值。
注意display()是重载的;Qt会自动选择合适的版本。而使用回调,你不得不找出5个不同的名称并且自己跟踪类型。
这个例子省略了不相关的成员函数。
信号与槽的默认参数
信号与槽的签名可以包含参数,参数还可以有默认值。思考一下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,但信号却没有发送QObject。这个连接将报一个运行时错误。
信号与槽的高级用法
当需要信号发送者信息的时候,Qt提供了QObject::sender()功能,返回一个指向信号发送对象的指针。
当多个信号连接同一个槽时,QSignalAMapper类可以帮助槽处理每一个不同的信号。
假设你有三个按钮,分别对应税务文件,账户文件,报表文件。
为了打开正确的文件,使用QSignalMapper::setMapping()映射所有的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可以使用第三方的信号与槽机制。你甚至可以在同一个项目里使用两种信号与槽机制。只需要把下面一行加到你的项目文件(.pro file)
CONFIG += no_keywords
这行命令式告诉Qt在使用第三方类库如boost时不定义signals,slots和emit关键字。在no_keywords标志下继续使用Qt的信号与槽,只要简单替换源代码中Qt的moc关键字为相对应的Qt宏Q_SIGNALS (Q_SIGNAL), Q_SLOTS (Q_SLOT) 和Q_EMIT。