企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 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 中的对象被当作软件的组件来使用,体现了“软件构件化”的思想。