3.12 信号
3.12.1 基本概念
Qt为了方便一些事件的处理,引入了信号(Signal)的概念,封装了一些事件操作的标准预处理,使得用户不必去处理底层事件,只需要处理信号即可。Qt还定义了一些预定义信号。在某些事件处理函数中会发送预定义信号,如果用户添加了与该信号相连的信号处理函数(也叫槽函数),则调用该槽函数。当然,并不是所有事件处理函数都会有信号发送。除了预定义信号外,用户也可以自己发送自定义信号。
信号与槽(Slot)其实都是函数。当特定事件被触发时(如在编辑框输入了字符)将发送一个信号,与之连接的槽则可以接收到并做出响应。
信号类似Windows编程中的消息,槽类似消息处理函数。比如,鼠标的按钮被单击,就会发出名为clicked的信号,如果该信号连接了槽(函数),就会调用这个函数来进行处理。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是将想要处理的信号和自己的一个函数(槽)绑定来进行处理。也就是说,当信号发出时,被连接的槽函数会自动被回调。
信号和槽是Qt特有的信息传输机制,是Qt程序设计的重要基础,可以让互不干扰的对象建立一种联系。
槽的本质是类的成员函数,它的参数可以是任意类型,和普通C++成员函数几乎没有区别,可以是虚函数,可以被重载,可以是公有的、保护的、私有的,也可以被其他C++成员函数调用。唯一的区别是:槽可以与信号连接在一起,每当和槽连接的信号被发出时,就会调用这个槽。
信号和槽是多对多的关系。一个信号可以连接多个槽,一个槽也可以监听多个信号。
信号可以有附加信息。例如,窗口关闭的时候可能发出windowClosing信号,这个信号可以包含窗口的句柄,用来表明究竟是哪个窗口发出的;一个滑块在滑动时可能发出一个信号,包含滑块的具体位置或者新的值等。我们可以把信号和槽理解成函数签名。信号只能同具有相同签名的槽连接起来。也可以把信号看成是底层事件一个形象的名字,比如windowClosing信号就是窗口关闭事件发生时会发出的信号。
信号和槽的机制实际是与语言无关的,有很多方法都可以实现信号和槽的机制,不同的实现机制会导致信号和槽的差别很大。信号和槽这一术语最初来自Trolltech(奇趣)公司的Qt库(后来被Nokia收购)。1994年,Qt的第一个版本发布后,为我们带来了信号和槽的概念。这一概念立刻引起计算机科学界的注意,提出了多种不同的实现。如今,信号和槽依然是Qt库的核心之一。其他许多库也提供了类似的实现,甚至出现了一些专门提供这一机制的工具库。
3.12.2 信号和槽的连接
这里的连接是关联的意思。信号和槽是通过系统函数connect()关联起来的。该函数是信号和槽里最重要的函数,它将信号发送者sender对象中的信号signal与接收者receiver中的member槽函数联系起来。
需要注意的是,connect()函数只能在QObject类和QObject派生类中使用,在自己新建的类(基类不是QObject类和QObject派生类)中使用connect()函数是无效的,编译时会一直报错。我们新建的项目(比如widget、mainwindow、dialog)都是QObject类的派生类,所以可以直接调用connect()函数,实现信号与槽的机制。该函数的原型声明如下:
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
其中,sender是一个指针,指向信号的发送对象;signal表示要发送的信号,具体使用时必须要用宏SIGNAL()将信号转为const char*类型;receiver是一个指针,指向信号的接收对象;method表示槽函数(信号处理函数),必须使用SLOT宏将其转换为const char*类型;type表示连接类型,可以取以下5个值:
· Qt::AutoConnection:默认值,使用这个值时连接类型会在信号发送时决定。如果接收者和发送者在同一个线程中,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在同一个线程中,则自动使用Qt::QueuedConnection类型。
· Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在的线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成系统崩溃。
· Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在的线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。在多线程环境下一般用这种连接类型。
· Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完毕。接收者和发送者绝对不能在同一个线程中,否则会死锁。在多线程间进行同步的场合可能需要这种类型。
· Qt::UniqueConnection:可以通过按位或(|)运算符来把以上4个结合在一起使用。使用这种类型,当某个信号和槽已经连接时,再进行重复的连接就会失败,也就是说避免了重复连接。
该函数会返回连接句柄,可用于稍后断开连接的操作。
值得注意的是,在指定信号和方法时,必须使用SIGNAL()和SLOT()宏。下面的代码演示了connect()函数的使用:
QLabel *label = new QLabel; QScrollBar *scrollBar = new QScrollBar; QObject::connect(scrollBar, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)));
这段代码确保标签始终显示当前滚动条的值。注意,信号和槽函数的参数不能包含任何变量名,只能包含类型,比如信号valueChanged的参数是int类型,槽函数的参数是int类型。例如,以下用法将不起作用并返回false:
3.12.3 信号和事件的区别
Qt的事件很容易和信号与槽相混淆。信号由具体对象发出,然后会马上交给由connect()函数连接的槽进行处理。对于事件,Qt使用一个事件队列对所有发出的事件进行维护;当新的事件产生时,会被追加到事件队列的尾部;前一个事件完成后,取出后面的事件接着进行处理。但是,必要的时候,Qt事件也是可以不进入事件队列而直接进行处理的。事件还可以使用“事件过滤器”进行过滤。比如对于一个按钮对象,我们只关心它被按下的信号,至于与这个按钮相关的其他信号,我们是不用关心的。如果我们要重载一个按钮事件处理函数,就要面对事件触发的时机。比如我们可以改变它的行为,让它在按下鼠标按钮的时候(mouse press event)就触发clicked()信号,而不是通常在释放鼠标按钮的时候(mouse release event)才触发信号。
总而言之,Qt的事件和Qt中的信号是不一样的。后者通常用来使用widget,而前者是用来实现widget的。如果是使用系统预定义的控件,那么我们关心的是信号;如果使用的是自定义控件,那么我们关心的是事件。