概述
信号和槽用于对象间的通信。信号和槽是Qt最核心的一个特征也可能是与其它框架最不相同的部分。
介绍
在GUI编程中,当我们改变一个部件的同时,我们通常希望另一个部件也能获悉此部件的改变。更普遍地说,我们希望任何一个对象都能与其它部件建立通信。例如,用户点击Close按钮之后,我们可能想窗口的close()函数被调用。
老工具箱使用回调函数(callbacks)实现此类通信。回调函数是一个函数指向函数的指针,如果你想要一个处理函数通知你一些事件,你应该传递一个回调函数到这个处理函数。处理函数会在适当的时机调用此回调函数。回调函数有两个最基本的缺点:第一,回调函数是不类型安全的(type-safe)。我们永远不能确定处理函数会传递给回调函数正确的参数。第二,回调函数与处理函数存在很强的耦合,因为处理函数必须知道调用哪个回调函数。
信号和槽
在Qt中,我们有一个替代回调函数的技术:使用信号和槽。在一个特定的事件发生的时候,信号被发出。Qt的部件预定义有许多信号,但是我们可能通过子类化部件为它添加更多的信号。槽是一个特定信号被激发而被调用的函数。Qt的部件有许多预定义的槽,在实际应用中,子类化部件,并添加你感兴趣的槽是非常适用的。
信号和槽机制是类型安全的(type safe): 信号的签名必须与接收的槽相匹配。(事实上,槽可能的签名可能会比它收到信号的签名更短,因为它可以忽略多余的变量。)因为签名的兼容,编译器可以帮助我们检测类型是否匹配。信号和槽耦合松散:发送信号的类并不知道也不关心谁会接收收信号。Qt的信号和槽机制保证了连接到信号的槽,会被在适当的时机被调用,并被赋予正确的参数。你可以传递给任何个数任何类型的参数给信号和槽。它们是完全类型安全的。
所有自继承QObject或QObject的子类的类都可以获得信号和槽的使用权。当对象状态被改变时,其它对象感兴趣的信号被发出。这就是与其它对象通信需要做的。部件不知道也不关心是否有谁收到了此信号。此为真正的信息封装,并且保证了此对象可以被用途软件组件。
槽可以用来接收信号,同时它们也是正常的成员函数。正如部件不知道是否有其它部件接收它发出的信号,部件也不知道它有没有连接到其它部件发出信号。此机制保证了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为父类的版本有相同的内部状态,也提供了访问状态的公共方法,除此之外,它还支持信号和槽的组件编程。此类可以通过信号valueChanged()告诉外部世界它的状态发生了改变,它也有一个槽来接收来自其它对象的信号。
所有包含信号和槽的类必须有在类的最开始有Q_OBJECT声明。当然它们必须继承自QObject。
槽是由程序员实现的。Counter::setValue()槽的可能实现:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
emit行触发valueChanged()信号,新值作为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)的调用触发valueChanged(12)信号,b将收到信号,并调用b.setValue(12). b同样会触发相同的valueChaned()信号,但是并没有槽与之连接,所以此信号将被忽略。
注意只有当value!=m_value时,setValue()函数才会更新m_value并触发信号。这样避免了死循环(如当b.valueChanged()被连接到a.setValue()).
默认地,你所创建的每个连接,都有一个信号发出,复制连接会有两个信号发出。使用一个disconnect(),你可以断开所有连接。如果你传递Qt::UniqueConnection类型,只有当此连接不是重复连接时,此连接才会被创建。如果已经存在一个复本(同一个对象的同一信号连接到同一槽),此连接会失败,并返回false.
此例说明了对象可以一直工作地很好而不需要知道对方的任何信息。这样你必须将这些对象连接在一起,仅仅使用QObject::connect()或uic的automatic connection特性就可以实现。
编译
c++编译器改变或移除了signals, slots和emit关键字以便呈现标准的c++.
在使用了信号和槽的c++类中运行moc, 会产生一个与应用目标文件一起编译连接的c++源文件。如果你用qmake,自动调用moc的makefile规则会添加到工程makefile.
信号
当一个对象的内部状态改变,信号会以此对象的客户感兴趣的方式触发。只有定义此信号的类或其子类可以触发此信号。
当此信号被触发,与其连接的槽被立即调用,就像其它普通函数一样。此行为发生时,信号和槽机制与完全与GUI事件循环完全独立开来。emit语句后的程序只有当槽返回后才会执行。只情形与qunued connection略有不同。此情况(queued connection)下,slot将在emit关键字后的程序执行后执行。
当一个几个槽被连接到一个信号,这几个槽将按被连接的顺序依次执行。
信号是由moc自动产生的,没有返回值,并且不能在cpp文件里面实现。
关于参数的注意事项: 我们的经验表明,当使用普通类型的参数时,信号和槽的重用性更强。如果 QScollBar::valueChanged()使用特殊类型的参数如假想的QScrollBar::Range,它只能与QScrollBar的特殊设计的槽连接。将不可能与不同的输入部件连接。
槽
当与之连接的信号触发时,相应的槽将被调用。槽是可以被正常调用的c++函数。它唯一特殊的地方是可以与信号连接。
因为槽是普通的成员函数,所以当被直接调用时它遵循正常的c++规则。然而作为槽,通过signal-slot连接,它可以被任何部分调用,无视它的访问等级。这意味着来自一个实例任何一个类的信号都可能会引起另一个实例中一个不相关为类的私有槽被调用。
你也可以定义一个虚槽,这在实际应用相当有用。
相比回调函数,鉴于它们所提供的灵活性,信号和槽效率会低一点,尽管此差异在实际程序中无关紧要。一般来说,触发一个有槽与之连接的信号的效率,在没有虚函数调用的情况下,是直接调用接收者时间的十倍。这是定位连接对象,枚举所有连接,排列参数的开销。十次非虚函数调用,听起来好像非常消耗时间,但是和new, delete所消耗的时间比起来,它所花费的时间少多了。例如,在你进行string, vector, 或者list操作时,在它们背景进行的delete, new动作, 信号和槽的开销只是一个完整函数调用的开销的很小部分。
请注意,当与基于Qt应用程序编译时,那些定义了名为signals或slots变量的库可能会导致编译器警告或错误。#undef冲突的预定义可以解决这个问题。
Meta-Object
meta-object编译器解析c++文件内的类声明并产生初始化meta-object的c++代码。meta-object包含了所有信号和槽成员的名字,包括指向它们的指针。
meta-object包含了其它额外信息如对象的类名。你可以检查一个对象是否继承自一个指定类,如:
if (widget-inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget)
button->toggle();
}
meta-object信息也用于qobject_cast(), 它与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继承自QObject, 通过QFrame和QWidget,LcdNumber已经具有大部分signal-slot的资质。在某方面,它与内置的QLCDNumber有些类似。
Q_OBJECT被展开,它定义了几个成员函数,并由moc实现。如果出现”undefined reference to vtable for LcdNumber”,你可以忘了运行moc或忘了将moc输入含入链接命令。
public:
LcdNumber(QWidget *parent = 0);
它与moc的关联性并不明显,但是你继承自QWidget,几乎很明显地,在你的建构函数中你想要一个parent,并将它传入父类的构造函数中。
在这里,一些函数和成员函数被忽略了;
signal:
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
槽用来接收其它部件状态改变信号的函数。LcdNumber用它来显示数字,如上所示。因为display和程序其它部分属于类的接口部分,所以槽是公开的。
注意display()是重载的。当你将之与一个信号连接时,Qt会选择合适的版本。使用回调函数,你还得自己找到一个不同的名字,并追踪参数数据类型。
在此例中,一些不相关成员函数就没有显示。
信号和槽的默认参数
信号和槽可能函数参数,这些参数可能是默认值,考虑QObject::destroyed():
void destroyed(QObject* = 0);
当一个QObject被删除,它触发QObject::destroyed()信号. 在任何可能会引用被删除的QOjbect的地方,我们想捕捉它,并清理。一个合理的签名可能会是这样:
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(), this, SLOT(objectDestroyed()));
但是这一个却不能正常工作:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
因为槽期待一个没有信号并没有随之发送的QObject。这个连接将报告一个运行时错误。
Signal和Slots的高级用法
在某些情况下,你可能会想知道发送者的一些信息,Qt提供了QObject::sender()函数,它返回发送者的指针。
QSignalMapper适用于在多个信号连接到一个槽,而槽又想根据不同信号作出不同处理的情况。
假想你有三个按钮分别决定了你打开哪个文件: “Tax File”, “Accounts File”或者”Report File”.
为了打开正确的文件,你使用QSignalMapper::setMapping()将所有clicked()信号映射到一个QSignalMapper对象。然后你连接文件的QPushButton::clicked()信号到QSingnalMapper()槽。
signalMapper = new QSignalMapper(this);
signalMapper->setMapping(taxFileButton, QString("taxFile.txt"));
signalMapper->setMapping(accountFileButton, QString("accountFile.txt"));
...
connect(taxFileButton, SIGNAL(clicked()), signalMapper, SLOT(map()));
connect(accountFileButton, SIGNAL(clicked()), signalMapper, SLOT(map()));
...
然后,你将mapped()信号连接到readFile(),在这里打开不同的文件,它取决于哪个按钮被按下。
connect(signalMapper, SIGNAL(mapped(QString)), this, SLOT(readFile(QString)));
注意:以下代码编译,运行正常,但是由于参数的标准化,它会慢一点。
使用第三方信号和槽
在Qt中使用第三方信号/槽是可能的。你甚至可以将它们在一个工程内使用。添加以下代码到你的qmake工程文件中(.pro)。
CONFIG += no_keywords
它告诉QT不要定义moc关键字signals, slots和emit。因为第三方库要使用它们。比如,Boost。在no_keywords标志下继续使用Qt信号,槽,用相应的Qt宏Q_SIGNALS(或 Q_SIGNAL), Q_SLOTS(或 Q_SLOT)和Q_EMIT代替signals, slots, emit。
最后
以上就是飘逸发夹为你收集整理的信号和槽的全部内容,希望文章能够帮你解决信号和槽所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复