17.3 信号和槽

正如你在第16章中所看到的,信号和信号处理是GUI应用程序用来响应用户输入的主要机制,也是所有GUI库的核心特征。Qt的信号处理机制由信号(signal)和槽(slot)构成,它们相当于GTK+中的信号和回调函数,或者Java中的事件和事件句柄。

注意,Qt信号与第11章中讨论的UNIX信号是完全不同的两个概念。

这里再回顾一下事件驱动编程的原理:一个GUI是由菜单、工具栏、按钮、输入框和许多其他GUI元素组成的,这些元素被统称为构件。当用户与一个构件交互时,例如激活菜单项或者在输入框中输入一些文本,构件将发出一个命名信号,如clicked、text_changed或key_pressed。你通常要对用户的动作做出响应,例如保存一个文档或退出应用程序。你通过把一个信号连接到一个回调函数(在Qt的说法中,是一个槽)来做到这一点。

在Qt中使用信号和槽的方法比较特别,Qt定义了两个新的很贴切的伪关键字(signals和slots),它用这两个伪关键字来标识代码中类的信号和槽。这非常有利于增强代码的可读性和可维护性,但它也意味着,代码必须经过一个单独的预—预处理阶段(pre-pre processing),来搜索这些伪关键字并用额外的C++代码对它们进行替换。

因此,Qt代码并不是真正的C++代码。这有时候对某些开发人员来说是一个问题。http://doc.trolltech.com/上的Qt文档中包含了使用这些新的伪C++关键字的原因。此外,信号和槽的使用与Windows中的微软基础类或MFC并没有什么不同,后者也使用了一个C++语言的修改定义。

Qt中信号和槽的使用有一些限制,但这些限制不是太严重。

❑ 信号和槽必须是QObject的派生类的成员函数。

❑ 如果使用多重继承,QObject必须在类列表中第一个出现。

❑ 在类声明中必须出现Q_OBJECT语句。

❑ 信号不能在模板中使用。

❑ 函数指针不能用作信号和槽的参数。

❑ 信号和槽不能被覆写和提升为公共(public)方法。

因为你需要在QObject的派生类中编写信号和槽,而且Qt基础构件QWidget派生自QObject,所以通过扩展和定制构件来创建界面是合情合理的。在Qt中,你几乎都是通过扩展如QMainWindow这样的构件来创建界面。

一个典型的针对GUI的类定义MyWindow.h如下所示:

17.3 信号和槽 - 图1

该类继承自QMainWindow,它为应用程序中的主窗口提供功能。类似地,当需要一个对话框时,你将创建一个QDialog的子类。类声明的第一条语句是Q_OBJECT,它充当一个预处理器标记,接下来就是通常的构造函数和析构函数原型,然后是信号和槽的定义。

你有一个信号和一个槽,二者均无参数。要发出一个信号,你只需在代码的某处调用emit:

17.3 信号和槽 - 图2

也就是说,其他的所有事情都是由Qt来处理,你甚至不需要提供一个aSignal( )的实现。

要使用槽,你必须将它们连接到一个信号。这是通过QObject类中的静态方法connect来完成的:

17.3 信号和槽 - 图3

你只需传递4个参数:拥有信号的对象(发送者)、信号函数、拥有槽的对象(接收者)、槽的名字。

在上面的MyWindow例子中,如果想把QPushButton构件的clicked信号连接到doSomething槽,你可以这样写:

17.3 信号和槽 - 图4

注意,你必须用SIGNAL和SLOT宏来包围信号和槽函数。与GTK+类似,一个给定信号可以连接任意数目的槽,一个槽也可以连接任意数目的信号,只要多次调用connect方法即可。如果连接失败,它将返回FALSE。

剩下的事就是实现槽函数,它采用的是一个普通成员函数的形式:

17.3 信号和槽 - 图5

实 验 信号和槽

你已经了解了信号和槽的工作原理,现在通过一个例子来使用它们。扩展QMainWindow并增加一个按钮,然后把按钮的clicked信号连接到一个槽。

(1)输入下面的类声明,把它命名为ButtonWindow.h:

17.3 信号和槽 - 图6

(2)接下来是类的实现ButtonWindow.cpp:

17.3 信号和槽 - 图7

(3)在构造函数中,你设置窗口标题,创建按钮,并且把按钮的clicked信号连接到槽。setCaption是QMainWindow中设置窗口标题的方法:

17.3 信号和槽 - 图8

(4)Qt自动管理构件的析构函数,所以你的析构函数是空的:

17.3 信号和槽 - 图9

(5)接下来是槽的实现代码:

17.3 信号和槽 - 图10

(6)最后,在main函数中,你只创建了ButtonWindow的一个实例,把它设置成应用程序的主窗口,然后在屏幕上显示它:

17.3 信号和槽 - 图11

17.3 信号和槽 - 图12

(7)在编译这个例子之前,你需要对头文件运行预处理程序。这个预处理程序被称为元对象编译器(moc),它位于Qt软件包中。在ButtonWindow.h上运行moc,将输出结果保存为ButtonWindow.moc:

17.3 信号和槽 - 图13

现在你可以像往常那样编译程序了,将moc的输出链接进来:

17.3 信号和槽 - 图14

运行该程序,你将看到如图17-3所示的内容。

17.3 信号和槽 - 图15

图 17-3

实验解析

我们在这里引入了一个新的构件和一些新函数,下面就对它们分别进行介绍。QPushButton是一个简单的按钮构件,它有一个标签和位图,用户可以通过使用鼠标点击或按键盘来激活它。

QPushButton的构造函数很简单:

17.3 信号和槽 - 图16

第一个参数是按钮标签的文本,第二个是父构件,最后一个是由Qt在其内部使用的按钮名字。

parent参数是所有QWidget都有的参数,父构件控制该构件什么时候显示和销毁,以及其他各种特性。传递NULL给parent参数表示该构件是顶层构件,Qt将创建一个空白窗口来包含它。在本例中,你使用this为parent参数传递了当前的ButtonWindow对象,这将把这个按钮添加到ButtonWindow主区域中。

name参数设置构件在Qt内部使用的名字。如果Qt遇到错误,则该名字会显示在输出的错误信息中,因此你应该选择一个合适的构件名字,这样可以在调试时节省大量时间。

你可能已注意到,程序只是很随便地通过设置QPushButton构造函数的parent参数,将QPushButton添加到ButtonWindow中。它没有指定构件的位置、大小、边界或其他类似的属性。如果想精确控制构件的布局(这对创建一个有吸引力的用户界面很关键),你就必须使用Qt的布局对象。下面就让我们来看看它。

Qt中有很多种方法可以用来排列构件的位置和布局。你已经看到可以通过调用setGeometry来设置绝对坐标,但这很少使用。因为当调整窗口大小时,构件不会做相应地调整来适应窗口。

排列构件的首选方法是使用QLayout类或Box构件,在你给出构件的边距值和构件间的间距值后,它们会根据情况自动调整大小。

QLayout类和Box构件之间的主要不同是:布局对象不是构件。

布局类派生自QObject而不是QWidget,因此你在使用它时受到一些限制。比如,你不能将QVBoxLayout作为QMainWindow的中心构件。

与布局类相反,Box构件(如QHBox和QVBox)派生自QWidget,因此你可以把它们看做为普通的构件。你可能会奇怪为什么Qt同时有QLayout和QBox且两者具有重复的功能。其实QBox构件只是为了方便,本质上它是在一个QWidget中包含了一个QLayout。QLayout具备自动调整大小的优势,而构件则必须通过调用Qwidget::resizeEvent( )来手工调整大小。

QLayout的子类QVBoxLayout和QHBoxLayout是创建界面最常用到的方法,也是你在Qt代码中最常见的类。

QVBoxLayout和QHBoxLayout都是不可见的容器对象,它们分别以垂直和水平的方向包含其他构件和布局。你可以创建一个任意复杂的构件排列,因为你可以对布局进行嵌套。例如,将一个横向布局作为一个元素放置到一个纵向布局中。

下面是我们感兴趣的3个QVBoxLayout构造函数(QHBoxLayout有相似的API):

17.3 信号和槽 - 图17

QLayout的parent参数可以是一个构件或是另一个QLayout。如果没有指定parent,那么你以后只能通过addLayout方法把这个布局加到另外一个QLayout中去。

margin和spacing设置围绕在QLayout四周的边距和构件间的间隔的像素值。

一旦创建了QLayout对象,你就可以用下面两个方法分别添加子构件和布局:

17.3 信号和槽 - 图18

实 验 使用QBoxLayout类

在本例中,你通过在QMainWindow中安排一个QLable构件来了解QBoxLayout类的实际使用情况。

(1)首先,编写头文件LayoutWindow.h:

17.3 信号和槽 - 图19

17.3 信号和槽 - 图20

(2)然后,编写类的实现文件LayoutWindow.cpp:

17.3 信号和槽 - 图21

(3)你需要创建一个哑QWidget来容纳QHBoxLayout,这是因为你不能在QMainWindow中直接增加QLayout:

17.3 信号和槽 - 图22

像前面一样,你需要在编译之前在头文件上运行moc:

17.3 信号和槽 - 图23

运行这个程序,你将看到几个QLabel的位置被安排好了(见图17-4)。试着改变窗口的大小,看看标签怎样根据窗口的大小放大和缩小。

17.3 信号和槽 - 图24

图 17-4

实验解析

LayoutWindow.cpp的代码创建了两个盒布局构件用于放置构件,分别是横向盒布局构件和纵向盒布局构件。纵向盒布局放置了两个标签,分别为Top和Bottom。横向盒布局也放置了两个构件,一个是显示为Right的标签,另一个是纵向盒布局构件。你可以像本例中那样,随意地在一个布局构件中放置另一个布局构件。

你可以尝试修改LayoutWindow.cpp中的代码,以便更好地了解盒布局的工作原理。

我们已经介绍了Qt的基本概念——信号和槽、moc和布局。现在是时候进一步讨论各个构件了。