信号和槽机制是Qt的核心机制之一,要掌握Qt编程就需要对信号和槽有所了解。信号和槽是一种高级接口,它们被应用于对象之间的通信,它们是Qt的核心特性,也是Qt不同于其它同类工具包的重要地方之一。
在我们所了解的其它GUI工具包中,窗口小部件(widget)都有一个回调函数用于响应它们触发的动作,这个回调函数通常是一个指向某个函数的指针。在Qt中用信号和槽取代了上述机制。
1.信号(signal)
当对象的状态发生改变时,信号被某一个对象发射(emit)。只有定义过这个信号的类或者其派生类能够发射这个信号。当一个信号被发射时,与其相关联的槽将被执行,就象一个正常的函数调用一样。信号-槽机制独立于任何GUI事件循环。只有当所有的槽正确返回以后,发射函数(emit)才返回。
如果存在多个槽与某个信号相关联,那么,当这个信号被发射时,这些槽将会一个接一个地被执行,但是它们执行的顺序将会是不确定的,并且我们不能指定它们执行的顺序。
信号的声明是在头文件中进行的,并且moc工具会注意不要将信号定义在实现文件中。Qt用signals关键字标识信号声明区,随后即可声明自己的信号。例如,下面定义了几个信号:
signals:
void yourSignal();
void yourSignal(int x);
在上面的语句中,signals是Qt的关键字。接下来的一行void yourSignal(); 定义了信号yourSignal,这个信号没有携带参数;接下来的一行void yourSignal(int x);定义 了信号yourSignal(int x),但是它携带一个整形参数,这种情形类似于重载。
注意,信号和槽函数的声明一般位于头文件中,同时在类声明的开始位置必须加上Q_OBJECT语句,这条语句是不可缺少的,它将告诉编译器在编译之前必须先应用moc工具进行扩展。关键字signals指出随后开始信号的声明,这里signals用的是复数形式而非单数,siganls没有public、private、protected等属性,这点不同于slots。另外,signals、slots关键字是QT自己定义的,不是C++中的关键字。
还有,信号的声明类似于函数的声明而非变量的声明,左边要有类型,右边要有括号,如果要向槽中传递参数的话,在括号中指定每个形式参数的类型,当然,形式参数的个数可以多于一个。
从形式上讲,信号的声明与普通的C++函数是一样的,但是信号没有定义函数实现。另外,信号的返回类型都是void,而C++函数的返回值可以有丰富的类型。
注意,signal 代码会由 moc自动生成,moc将其转化为标准的C++语句,C++预处理器会认为自己处理的是标准C++源文件。所以大家不要在自己的C++实现文件实现signal。
2.槽(slot)
槽是普通的C++成员函数,可以被正常调用,不同之处是它们可以与信号(signal)相关联。当与其关联的信号被发射时,这个槽就会被调用。槽可以有参数,但槽的参数不能有缺省值。
槽也和普通成员函数一样有访问权限。槽的访问权限决定了谁可以和它相连。通常,槽也分为三种类型,即public slots、private slots和protected slots。
public slots:在这个代码区段内声明的槽意味着任何对象都可将信号与之相连接。这对于组件编程来说非常有用:你生成了许多对象,它们互相并不知道,把它们的信号和槽连接起来,这样信息就可以正确地传递,并且就像一个小孩子喜欢玩耍的铁路轨道上的火车模型,把它打开然后让它跑起来。
protected slots:在这个代码区段内声明的槽意味着当前类及其子类可以将信号与之相关联。这些槽只是类的实现的一部分,而不是它和外界的接口。
private slots:在这个代码区段内声明的槽意味着只有类自己可以将信号与之相关联。这就是说这些槽和这个类是非常紧密的,甚至它的子类都没有获得连接权利这样的信任。
通常,我们使用public和private声明槽是比较常见的,建议尽量不要使用protected关键字来修饰槽的属性。此外,槽也能够声明为虚函数。
槽的声明也是在头文件中进行的。例如,下面声明了几个槽:
public slots:
void yourSlot();
void yourSlot(int x);
注意,关键字slots指出随后开始槽的声明,这里slots用的也是复数形式。
3.信号与槽的关联
槽和普通的C++成员函数几乎是一样的-可以是虚函数;可以被重载;可以是共有的、保护的或是私有的,并且也可以被其它C++成员函数直接调用;还有,它们的参数可以是任意类型。唯一不同的是:槽还可以和信号连接在一起,在这种情况下,每当发射这个信号的时候,就会自动调用这个槽。
connect()语句看起来会是如下的样子:
connect(sender,SIGNAL(signal),receiver,SLOT(slot));
这里的sender和receiver是指向QObject的指针,signal和slot是不带参数的函数名。实际上,SIGNAL()宏和SLOT()会把它们的参数转换成相应的字符串。
到目前为止,在已经看到的实例中,我们已经把不同的信号和不同的槽连接在了一起。但这里还需要考虑一些其他的可能性。
⑴一个信号可以连接多个槽
connect(slider,SIGNAL(valueChanged(int)),spinBox,SLOT(setValue(int)));
connect(slider,SIGNAL(valueChanged(int)),this,SLOT(updateStatusBarIndicator(int)));
在发射这个信号的时候,会以不确定的顺序一个接一个的调用这些槽。
⑵多个信号可以连接同一个槽
connect()
无论发射的是哪一个信号,都会调用这个槽。
⑶一个信号可以与另外一个信号相连接
connect(lineEdit,SIGNAL(textChanged(const Qstring &)),this,SIGNAL(updateRecord(const Qstring &)));
当发射第一个信号时,也会发射第二个信号。除此之外,信号与信号之间的连接和信号与槽之间的连接是难以区分的。
⑷连接可以被移除
disconnect(lcd,SIGNAL(overflow()),this,SLOT(handleMathError()));
这种情况较少用到,因为当删除对象时,Qt会自动移除和这个对象相关的所有连接。
⑸要把信号成功连接到槽(或者连接到另外一个信号),它们的参数必须具有相同的顺序和相同的类型
connect(ftp,SIGNAL(rawCommandReply(int,const QString &)),this,SLOT(processReply(int,const QString &)));
⑹如果信号的参数比它所连接的槽的参数多,那么多余的参数将会被简单的忽略掉
connect(ftp,SIGNAL(rawCommandReply(int,const Qstring &)),this,SLOT(checkErrorCode(int)));
还有,如果参数类型不匹配,或者如果信号或槽不存在,则当应用程序使用调试模式构建后,Qt会在运行时发出警告。与之相类似的是,如果在信号和槽的名字中包含了参数名,Qt也会发出警告。
信号和槽机制本身是在QObject中实现的,并不只局限于图形用户界面编程中。这种机制可以用于任何QObject的子类中。
当指定信号signal时必须使用Qt的宏SIGNAL(),当指定槽函数时必须使用宏SLOT()。如果发射者与接收者属于同一个对象的话,那么在connect调用中接收者参数可以省略。
例如,下面定义了两个对象:标签对象label和滚动条对象scroll,并将valueChanged()信号与标签对象的setNum()相关联,另外信号还携带了一个整形参数,这样标签总是显示滚动条所处位置的值。
QLabel *label = new QLabel;
QScrollBar *scroll = new QScrollBar;
QObject::connect( scroll, SIGNAL(valueChanged(int)),
label, SLOT(setNum(int)) );
4.信号和槽连接示例
以下是 QObject子类的示例:
class BankAccount : public QObject
{
Q_OBJECT
public:
BankAccount() { curBalance = 0; }
int balance() const { return curBalance; }
public slots:
void setBalance(int newBalance);
signals:
void balanceChanged(int newBalance);
private:
int currentBalance;
};
与多数 C++ 类的风格类似,BankAccount 类拥有构造函数、balance() “读取”函数和 setBalance() “设置”函数。它还拥有 balanceChanged() 信号,帐户余额更改时将发出此信号。发出信号时,与它相连的槽将被执行。
Set函数是在公共槽区中声明的,因此它是一个槽。槽既可以作为成员函数,与其他任何函数一样调用,也可以与信号相连。以下是 setBalance()槽的实现过程:
void BankAccount::setBalance(int newBalance)
{
if (newBalance != currentBalance)
{
currentBalance = newBalance;
emit balanceChanged(currentBalance);
}
}
语句emit balanceChanged(currentBalance);将发出 balanceChanged() 信号,并使用当前新余额作为其参数。
关键字 emit 类似于“signals”和“slots”,由 Qt 提供,并由 C++ 预处理器转换成标准 C++ 语句。
以下示例说明如何连接两个 BankAccount对象:
BankAccount x, y;
connect(&x, SIGNAL(balanceChanged(int)), &y, SLOT(setBalance(int)));
x.setBalance(2450);
当 x 中的余额设置为 2450 时,系统将发出 balanceChanged() 信号。y 中的setBalance() 槽收到此信号后,将 y 中的余额设置为 2450。一个对象的信号可以与多个不同槽相连,多个信号也可以与特定对象中的某一个槽相连。参数类型相同的信号和槽可以互相连接。槽的参数个数可以少于信号的参数个数,这时多余的参数将被忽略。
5.需要注意的问题
信号与槽机制是比较灵活的,但有些局限性我们必须了解,这样在实际的使用过程中才能够做到有的放矢,避免产生一些错误。下面就介绍一下这方面的情况。
⑴信号与槽的效率是非常高的,但是同真正的回调函数比较起来,由于增加了灵活性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的,通过在一台i586-133的机器上测试是10微秒(运行Linux),可见这种机制所提供的简洁性、灵活性还是值得的。但如果我们要追求高效率的话,比如在实时系统中就要尽可能的少用这种机制。
⑵信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能产生死循环。因此,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射所接收到的同样信号。
⑶如果一个信号与多个槽相关联的话,那么,当这个信号被发射时,与之相关的槽被激活的顺序将是随机的,并且我们不能指定该顺序。
⑷宏定义不能用在signal和slot的参数中。
⑸构造函数不能用在signals或者slots声明区域内。
⑹函数指针不能作为信号或槽的参数。
⑺信号与槽不能有缺省参数。
⑻信号与槽也不能携带模板类参数。