13.2 信号与槽

信号和槽机制是 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()会把它们的参数转换成相应的字符串。

到目前为止,在已经看到的实例中,我们已经把不同的信号和不同的槽连接在了一 起。但这里还需要考虑一些其他的可能性。

(1) 一个信号可以连接多个槽

connect(slider,SIGNAL(valueChanged(int)),spinBox,SLOT(setValue(int)));
connect(slider,SIGNAL(valueChanged(int)),this,SLOT(updateStatusBarIndicator(int)));

在发射这个信号的时候,会以不确定的顺序一个接一个的调用这些槽。

(2) 多个信号可以连接同一个槽

connect()

无论发射的是哪一个信号,都会调用这个槽。

(3) 一个信号可以与另外一个信号相连接

connect(lineEdit,SIGNAL(textChanged(const Qstring &)),this,SIGNAL(updateRecord(const Qstring &)));

当发射第一个信号时,也会发射第二个信号。除此之外,信号与信号之间的连接和信 号与槽之间的连接是难以区分的。

(4) 连接可以被移除

disconnect(lcd,SIGNAL(overflow()),this,SLOT(handleMathError()));

这种情况较少用到,因为当删除对象时, Qt 会自动移除和这个对象相关的所有连接。

(5) 要把信号成功连接到槽(或者连接到另外一个信号),它们的参数必须具有相同的顺序 和相同的类型

connect(ftp,SIGNAL(rawCommandReply(int,const QString&)),this,SLOT(processReply(int,const QString &)));

(6) 如果信号的参数比它所连接的槽的参数多,那么多余的参数将会被简单的忽略掉

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.需要注意的问题

信号与槽机制是比较灵活的,但有些局限性我们必须了解,这样在实际的使用过程中才能够做到有的放矢,避免产生一些错误。下面就介绍一下这方面的情况。

(1) 信号与槽的效率是非常高的,但是同真正的回调函数比较起来,由于增加了灵活 性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的,通过在一台 i586- 133 的机器上测试是 10 微秒(运行 Linux),可见这种机制所提供的简洁性、灵活性还是 值得的。但如果我们要追求高效率的话,比如在实时系统中就要尽可能的少用这种机制。

(2) 信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能 产生死循环。因此,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射 所接收到的同样信号。

(3) 如果一个信号与多个槽相关联的话,那么,当这个信号被发射时,与之相关的槽被 激活的顺序将是随机的,并且我们不能指定该顺序。

(4) 宏定义不能用在 signal 和 slot 的参数中。

(5) 构造函数不能用在 signals 或者 slots 声明区域内。

(6) 函数指针不能作为信号或槽的参数。

(7) 信号与槽不能有缺省参数。

(8) 信号与槽也不能携带模板类参数。

6.小结

从 QObject 或其子类(例如 Qwidget)派生的类都能够使用信号和槽机制。这种机制本身 是在 QObject 中实现的,并不只局限于图形用户界面编程中:当对象的状态得到改变时, 它可以某种方式将信号发射(emit)出去,但它并不了解是谁在接收这个信号。槽被用于接收 信号,事实上槽是普通的对象成员函数。槽也并不了解是否有任何信号与自己相连接。而 且,对象并不了解具体的通信机制。这实际上是 “封装”概念的生动体现,信号与槽机制 确保了 Qt 中的对象被当作软件的组件来使用,体现了“软件构件化”的思想。