信号与槽用于对象之间的通信。信号与槽机制是Qt的核心特性和区别于其他框架的特性。
在GUI程序中,当我们改变一个widget,经常需要其他的widget得到通知。更普遍的是,我们需要任意的对象可以与另外的对象进行通信。例如,一个用户点击关闭按钮,我们可以需要windows的close()函数被调用。
旧的工具包通过回调函数实现这种通信。一个回调函数是一个函数指针,所以你需要一个处理函数来通知你关于一些事件,你传递一个指向另一个函数的处理函数。这个处理函数在适当的时候调用回调函数。回调函数有两个基本的缺陷:一是,它们不是类型安全的。我们不能保证处理函数能用正确的参数进行调用回调函数。二是,回调函数和处理函数是强耦合的,因为处理函数必须知道要调用那个回调函数。
在Qt中,我们有回调函数的好的替代方案:信号与槽。当发生特定的事件时激发一个信号。Qt的widget有很多预先定义的信号,但是我们可以自定义widget来添加我们自己的信号。槽函数是一个特定的信号的响应函数。Qt的widget有很多预先定义的槽函数,但是我们也可以自定义widget来添加自己的槽函数以处理我们感兴趣的信号。
信号与槽机制是类型安全的。信号的签名必须与接收信号的槽的签名匹配。(实际上,槽函数的签名可能比它接收的信号的签名要短,因为它可以忽略额外的参数)因为签名是兼容的,编译器可以帮我们捕获类型不匹配的情况。信号与槽是轻耦合的:一个类激发一个信号,它既不用知道也不用关心哪个槽接收该信号。Qt的信号与槽机制确保当你连接一个信号到一个槽,这个槽将在适当的时候用信号的参数进行调用。信号和槽可以用任何类型任意数量的参数。它们完全的类型安全的。
所有继承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-based类:
#include <QObject>
class Counter : publicQObject
{
Q_OBJECT
public:
Counter() { m_value =0; }
int value() const { return m_value; }
publicslots:
void setValue(int value);
signals:
void valueChanged(int newValue);
private:
int m_value;
};
QObject-based版本有相同的内部状态,而且提供了public的方法来获取状态。另外,它使用信号和槽以支持组件编程。这个类可以通过激发信号valueChanged(),告诉外部它的状态的改变,而且它有一个其他对象可以给它发送信号的槽。
所有包含信号或槽的类必须在声明的开头包含 Q_OBJECT宏声明,而且必须直接或间接的继承自QObject。
槽可以由程序员来实现。这里是Counter::setValue()槽的一个可能的实现:
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
对象激发带有新的value作为参数的信号valueChanged()
在下面的代码片段,我们创建了两个Counter 对象和用 QObject::connect():把第一个对象的valueChanged() 信号连接到第二个对象的setValue() 槽函数。
Counter a, b;
QObject::connect(&a,&Counter::valueChanged,
&b,&Counter::setValue);
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) ,吧同样激发一个valueChanged() 信号,但是没有哪个槽连接到b的valueChanged() 信号,所以信号被忽略了。
注意,setValue() 只有在value != m_value的时候才设置value和激发信号。这避免了在环形连接情况下出现无限循环。
默认情况,对于你建立的每一个连接发送一个信号。对于重复的连接发送两个信号。你可以调用disconnect() 中断连接。如果你传递了 Qt::UniqueConnection type,当没有副本时连接才会建立。如果已经存在一个副本,连接将失败并返回false。
这个例子阐述了对象进行协作但是不需要知道彼此的任何信息。为了达到这样,对象仅仅需要简单的通过QObject::connect() 或 uic's automatic connections 特性连接在一起。
C++预处理器改变或去掉了signals, slots, 和 emit关键字,使得给编译器呈现的是标致的C++。
用moc来运行一个包含信号和槽的类声明,将生成一个被程序编译和连接的C++源文件。如果你用qmake,makefile的规则自动调用moc。
当对象的内部状态变化时会发出在某种程度上其他对象或者用户感兴趣的信号。信号是公共访问的函数,可以从任何地方发送,但是我们建议仅仅从定义信号的类或其子类中发送信号。
当信号被激发,连接它的槽函数通常像函数调用一样被立即执行。在这种情况下,信号和槽机制完全独立于任何GUI事件循环。一旦所有的槽函数返回之后,跟在emit后面代码才会执行。当使用queued connections时情况会稍有不同。在这种情况下,跟着emit后面的代码会立即被执行,槽函数会晚一些执行。
如果有多个槽函数连接到一个信号,当信号被激发时,槽函数会以它们连接的顺序一个接一个的执行。
信号由moc自动生成而且不能在.cpp中实现。它们没有返回类型。
关于参数需要注意的是:我们的经验显示,如果信号和槽不要专门的类型,它们会更具可重用性。如果QScrollBar::valueChanged()使用特定的类型如QScrollBar::Range,它就只能连接到为 QScrollBar专门设计的槽函数了。不可能将不同的输入widget连接在一起。
当连接到槽函数信号被激发,槽函数将被调用。槽函数是正常的C++函数,可以被正常的调用,它们唯一的特性就是可以连接信号。
由于槽函数是正常的成员函数,所以被直接调用时遵循C++规则。然而,槽函数可以被其他组件通过信号连接调用而不管其访问属性。这意味着,任意一个实例激发的信号可以引起一个不相关的类的private槽函数被调用。
你也可以定义槽函数为虚函数。
与回调函数相比较,由于其提供的灵活性,信号与槽稍微慢了一点。尽管对于现实中的程序来说这点不同无关紧要。通常,激发一个连接到多个槽函数的信号,不用虚函数,大概比直接调用接收者慢10倍。这开销用来定位连接对象,安全的迭代所有的连接和以一致的方式安排参数。虽然这听起来更像是调用10个非虚函数,但是它比任何new或delete操作的开销更小。例如,当你执行一个字符串,vector或list等幕后需要new或delete的操作,信号和槽的开销仅仅占整个函数调用开销的很小部分。当你的系统调用用槽函数或间接调用超过10个函数也同样如此。信号和槽机制的简单性和灵活性是值得的开销,这点用户甚至不知道。
注意:如果其他库的变量我signals 或 slots,当编译基于Qt 的程序时可能引起警告或错误。要解决这个问题,用 #undef 取消预处理的标记。
元对象编译器moc解析C++文件的类声明并生成初始化元对象的C++代码。元对象包含所有的信号和槽的名称和指向它们的指针。
元对象包含一些额外的信息,比如对象的类名。你也可以检查一个对象是否继承指定的类。例如:
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();
一个带注释的简单的例子:
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT
LcdNumber 继承QObject,,有点类似内置的QLCDNumber widget。
Q_OBJECT宏被与处理器扩展以声明一些函数是被moc实现的。如果出现编译错误"undefined reference to vtable for LcdNumber,你可能忘记运行moc或者在连接命令行忘记包含moc生成的文件。
public:
LcdNumber(QWidget *parent =0);
这和moc不是明显相关的,但是如果你继承了QWidget,你几乎确定构造函数要带parent参数并把它传递给基类的构造函数。
在这里省略了一些析构函数和成员函数,moc忽略了成员函数。
signals:
void overflow();
当LcdNumber 要显示一个不可能的值的时发出一个信号。
如果你不关心溢出,或者知道不可能出现溢出。你可以忽略overflow() 信号,不连接到任何槽函数。
如果你想在发生溢出时调用两个不同的错误函数,只需要简单的把信号连接到两个不同的槽函数就可以了。
publicslots:
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 用槽来显示number,由于display()是类的接口的一部分,槽是公共的。
一些程序例子连接QScrollBar的信号 valueChanged() 到display() 槽,所以LcdNumber 不断的显示 scroll bar的值。
注意:display()被重载了,Qt在对它连接信号时要选好适当的版本。如果用回调函数,你必须找五个不同的名称和自己跟踪它们的类型。
信号和槽的签名可以包含参数,而且参数可以由默认值。考虑QObject::destroyed():
void destroyed(QObject*=0);
当一个QObject 被删除,它发出QObject::destroyed() 信号。我们想捕获这个信号,有时我们可能有一个对被删除QObject的悬空引用,所以我们可以清楚它。一个合适的槽签名如下:
void objectDestroyed(QObject* obj =0);
为了为该槽连接信号,我们用QObject::connect(). 。有很多方法可以连接信号和槽,第一种方法是用函数指针:
connect(sender,&QObject::destroyed,this,&MyObject::objectDestroyed);
在 connect() 中用函数指针有不少好处:一是它允许编译器检查信号的参数是否与槽的参数兼容。如果需要则对参数进行隐式转换。
你也可以连接到函数或者C++11的lamdas:
connect(sender,&QObject::destroyed,[=](){ this->m_objects.remove(sender); });
如果你的编译器不支持C++11的可变参数模板,只有在信号和槽有少于等于六个参数这种语法才有效。
另外一种连接信号和槽的方法是,用 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::connect() 进行重载时,编译器不好检查信号和槽的参数。
有时你可能需要信号发送方的信息,Qt提供了函数 QObject::sender() ,它返回一个指向信号发送方的指针。
QSignalMapper 类被用在,当很多信号被连接到同一个槽,而且槽需要处理每一个不同的信号。
假设你有三个按钮来决定你将打开哪个文件:"Tax File", "Accounts File", 或 "Report File".
为了打开正确的文件,你用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,&QPushButton::clicked,
signalMapper,&QSignalMapper::map);
connect(accountFileButton,&QPushButton::clicked,
signalMapper,&QSignalMapper::map);
connect(reportFileButton,&QPushButton::clicked,
signalMapper,&QSignalMapper::map);
然后,你连接 mapped() 信号到依据被按下的按钮打开不同文件的 readFile()。
connect(signalMapper, SIGNAL(mapped(QString)),
this, SLOT(readFile(QString)));
Qt可以和第三方信号/槽机制一起使用。甚至可以在同一个工程中使用两种机制。只需要在qmake project 文件添加
CONFIG += no_keywords
它告知Qt不要定义moc关键字signals, slots, 和 emit,因为这些名词可能被第三方库使用。Qt信号和槽可以继续和no_keywords标记一起使用。只需要将源代码中用到的moc关键字相应的替换成 Q_SIGNALS (or Q_SIGNAL), Q_SLOTS (or Q_SLOT), 和Q_EMIT.就行了。
http://blog.csdn.net/hai200501019/article/details/9166547