分类: LINUX
2013-03-05 14:42:56
在 图形界面编程中,很多时候我们希望一个可视对象发生某种变化时通知另一个或几个对象,再一个地说,我们希望任何一类的对象能和其他对象进行通讯。例如,某 个数值显示窗口负责显示某个滚动条对象的当前数值,当滚动条对象的值发生变化时,我们希望数值显示窗口能收到来自滚动条对象发送的“数值改变”的信号,从 而改变自己的显示数值。
对于类似以上的问题,较早的工具包使用“回调”的方式来实现。回调是指一个函数的指针,如果你希望一个处理函数同志你一些事件,你可以把另一个函数的指针传递给处理函数。处理函数在适当的时候会调用回调函数。
采用回调方式实现对象间的通讯有两个主要缺点,首先回调函数不是类型安全的,我们不能确定处理函数使用了正确的参数来调用回调函数,第二,回调函数和处理函数间的联系非常紧密,因为处理函数必须知道要调用哪个回调函数。
在QT开发环境中,实现对象间的通讯我们有一种称为“信号和槽”的机制可以代替回调函数。信号和槽机制用于实现对象间的通讯,是QT的一个中心特性,恐怕也是QT与其它工具包最不同的地方了。
信号和槽机制就是:当一个特定的事件发生时,一个或几个被指定的信号就被发射,槽就是一个返回值为void的函数,如果存在一个或几个槽和该信号相连接,那在该信号被发射后,这个(些)槽(函数)就会立刻被执行。
信号和槽机制是类型安全的,一个信号的签名必须与它的接收槽的签名相匹配,这样编译器就可以帮助我们检查类型是否匹配。信号和槽是很宽松的联系在一 起的,一个发射信号的对象不用考虑哪个槽会接收这个信号,接收信号的槽的所在对象也不知道要连接的信号是哪个对象发射的。QT的信号和槽机制可以保证如果 你把一个信号和一个槽连接起来后,槽会在正确的时间使用信号的参数而被调用,信号和槽可以使用任何数量、类型的参数。
QT的窗口部件已经有很多预定义的信号,也有很多预定义的槽,但我们总是通过继承来加入我们自己的信号和自己的槽,这样我们就可以处理感兴趣的信号 了。凡是从QObject类或者它的某个子类继承的所有类都可以包含信号和槽。当某个事件发生后,被指定的信号就会被发射,它不知道也没有必要知道是否有 槽连接了该信号,这就是信息封装。
槽是可以用来接收信号的正常的对象的成员函数,一个槽不知道它是否被其它信号连接。可以把一个信号和一个槽进行单独连接,这时槽会因为该信号被发射 而被执行;也可以把几个信号连接在同一个槽上,这样任何一个信号被发射都会使得该槽被执行;也可以把一个信号和多个槽连接在一起,这样该信号一旦被发射, 与之相连接的槽都会被马上执行,但执行的顺序不确定,也不可以指定;也可以把一个信号和另一个信号进行连接,这样,只要第一个信号被发射,第二个信号立刻 就被发射。
下面看看不使用信号和槽和使用信号和槽的两个对象有什么不同。
不使用信号和槽的C++对象:
class NoSignalClass {
public:
NoSignalClass (void) {}
int value (void) const {return _value;}
int setValue (int value) {_value = value;}
private:
int _value;
}
使用信号和槽的QT对象:
class UseSignalClass {
Q_OBJECT
public:
UseSignalClass (void) {}
int value (void) const {return _value;}
public slots:
int setValue (int value) {_value = value;}
signals:
void valueChanged (int);
private:
int _value;
}
这两个类有相同的内部状态,相同的公有方法,但后一个类却支持使用信号和槽的组件编程:这个类可以通过发射一个信号 (valueChanged())来告诉外界它的状态发生了变化,并且它有一个槽,可以接收信号。所有包含信号和槽的类都必须在它们的声明中提到 Q_OBJECT。槽要由自己来实现,这里最好把int setValue (int value)槽这样实现:
void UseSignalClass::setValue( int value )
{
if ( value != _value ) {
_val = value;
emit valueChanged(value);
}
}
emit valueChanged(value);这行代码是发射一个信号valueChanged。
要想使一个槽在一个信号被发射后被执行,要显示地进行连接:
UseSignalClass a,b;
connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
这样做后,就把对象a的信号valueChanged和对象b的槽setValue连接了起来,当a的信号valueChanged被发射后,对象b的槽setValue马上就被执行。
执行以下这句代码:
b.setValue( 10 );
这句被执行后,对象b的valueChanged信号会被发射,但没有槽和该信号相连,所以什么也没做,信号被丢弃。
a.setValue( 80);
这句被执行后,对象a的valueChanged信号会被发射,该信号有槽(对象b的setValue)相连,所以b.setValue(int)马上被执行,且参数为80,所以b.value()的值为80。
如果要解除已经建立好连接的信号和槽,可以使用disconnect()函数。bool QObject::disconnect ( const QObject * sender, const char * signal,
const Object * receiver, const char * member ) [static]
这个函数断开发射者中的信号和接收者中的槽函数之间的关联。有以下三种情况:
1、断开某个对象与其它对象的任何连接:
disconnect(object, 0, 0, 0);或object->disconnect();
2、断开某个信号与其它任何槽的连接:
disconnect(object, SIGNAL(signal()), 0, 0);或object->disconnect(SIGNAL(signal()));
3、断开两个对象之间的任何关联:
disconnect(object, 0, receiver, 0);或object->disconnect(receiver);
在disconnect函数中0可以用作一个通配符,可分别表示任何信号、任何接收对象、接收对象中的任何槽函数。但是发射者不能为0,其它三个参数都可以为0。
关键字signals指出随后开始信号的声明,这里signals用的是复数形式而非单数,siganls没有public、private、 protected等属性,这点不同于slots。另外,signals、slots关键字是QT自己定义的,不是C++中的关键字。
这个例子说明,采用QT的信号和槽机制后,对象之间可以在相互不知道的情况下一起工作,只要在最初的时候在他们中间建立了连接。
槽也是普通的C++函数,可以一样被调用,他唯一的特点就是 他们可以被信号连接。因为槽就是普通的成员函数,它们也和普通的成员函数一样有访问权限,一个槽的访问权限决定了哪些信号可以和它相连接,而信号就没有访问权限的概念。
一个public slots:区包含了任何信号都可以相连的槽。你生成了许多对象,它们互相并不知道,把它们的信号和槽连接起来,这样信息就可以正确地传递,并且就像一个铁路模型,把它打开然后让它跑起来。
一个protected slots:区包含了之后这个类和它的子类的信号才能连接的槽。这就是说这些槽只是类的实现的一部分,而不是它和外界的接口。
一个private slots:区包含了之后这个类本身的信号可以连接的槽。这就是说它和这个类是非常紧密的,甚至它的子类都没有获得连接权利这样的信任。
也可以把槽定义为虚函数,这也很有用。
使用信号和槽机制,要注意以下问题:
1、信号和槽的机制是非常有效的,但是它不像“真正的”回调那样快。信号和槽稍微有些慢,这是因为它们所提供的灵活性。但这种损失相对来说是比较小的。但要追求高效率的话,比如在实时系统中就要尽量少用这种机制。
2、信号和槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时有可能形成死循环,所以,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射所接收到的同样的信号。
3、如果一个信号和多个槽相关联的话,那当这个信号被发射时,与之相关联的槽的执行顺序将是髓机的,且顺序不能指定。
4、宏定义不能用在signal和slot的参数中。
5、构造函数不能用在signals和slots声明区域内。
6、函数指针不能作为信号或槽的参数。
7、信号和槽不能有缺省参数值。
8、信号和槽不能携带模板类参数。
9、嵌套的类不能位于信号和槽区域内,也不能有信号或者槽。
10、友元声明不能位于信号和槽的声明区域内。