前言 Qt是Nokia今后最重要的开发平台,Symbian、Maemo、MeeGo都将使用Qt。 鉴于以下原因: n 市场上面几乎不存在相关的中文资料 n 许多开发者对E文很是感冒 n 电子版的文档方便技术传播和交流 DevDiv.com移动开发论坛的翻译组重操宝刀,利用业余时间和相关方面的经验,把一些好的Qt英文资料翻译成中文,为广大嵌入式移动开发者尽一点绵薄之力。 关于DevDiv翻译组 请参考DevDiv详细内容。 技术支持 首先DevDiv翻译组对您能够阅读本系列文档以及关注DevDiv表示由衷的感谢。Qt for Symbian开发是一个比较新的技术,在您学习和开发过程中,一定会遇到一些问题。DevDiv论坛集结了国内一流嵌入式移动开发专家,我们很乐意与您一起探讨Qt及相关问题。如果您有什么问题和技术需要支持的话,请登陆网站www.devdiv.com或者发送邮件到[email protected],我们将尽快回复您。
关于本文的翻译 感谢crystalblu,sand.fj.wen对本文的翻译,同时非常感谢Cxt_programmer在百忙中抽出时间对翻译初稿的认真校验。才使本文与读者尽快见面。由于书稿内容多,我们的知识有限,尽管我们进行了细心的检查,但是还是会存在错误,这里恳请广大读者批评指正,并发送邮件至[email protected],在此我们表示衷心的感谢。 注:本文原文地址:信号&槽。 第一章 信号&槽(Signals and Slots) 信号和槽通常用于对象间的通信。信号和槽机制是 Qt 的主要特性并且也很有可能是它与其他框架特性区别最大的部分。 第二章 简介 在GUI编程中,当我们想要改变某个widget时,通常想要其它widget获悉这一改变通知。更常用的情景是,我们想要各种类型的对象间能够互相通信。比如,当用户点击了“关闭”按钮,我们可能希望它调用“关闭”功能把窗口关掉。 Older toolkits使用回调(callbacks)来达到这样的目的。回调是一个指向函数的指针,所以如果你希望一个处理函数通知你某些事件发生了,你可以传递一个指向其他函数的指针(回调)给它。这个处理函数将在适当的时候调用回调函数。回调函数有两个明显的缺点,第一,它们并不是类型安全,我们永远都不能确定调用者是否将通过正确的参数来调用“回调函数”;第二,回调函数与处理函数是紧耦合(strongly coupled)的,因为调用者必须知道应该在什么时候调用哪个回调函数。
第三章 信号&槽 Qt使用了信号和槽来代替回调函数。当一个特定的事件发生时,信号会被发送出去。Qt的窗体部件(widget)拥有众多预先定义好的信号,当然,我们也可以创建窗体部件(widget)的子类来为它们添加我们需要的自定义信号。槽,则是对一个特定的信号进行的反馈。Qt的窗体部件(widget)同样拥有众多预先定义好的槽,但是通常的做法是,创建窗体部件(widget)的子类并添加自定义槽,以便对感兴趣的信号进行处理。 信号和槽机制是类型安全的(type-safe):一个信号的参数必须和接收槽的参数匹配。(槽的参数可以比它接收的信号的参数短,第十章的范例很好的解释了这一点。)由于这种参数匹配机制,编译器以帮助我们检查类型不匹配的签名。信号与槽是松耦合(loosely coupled)的:一个发出信号的类既不知道也不关心哪一个槽接收到这个信号。Qt的信号和槽机制保证了如果你将一个信号连接到一个槽上,槽会在正确的时间以号的参数被调用。信号与槽可以携带任意个、任意类型的参数。他们是完全的类型安全。 所有从QObject或者它的一个子类(比如:QWidget)继承的类都可以使用号与槽。对象中以这种方式通信:一个对象的状态发生了改变并发送信号,关心这个改变的另一对像接收到这个信号。发送信号的对象并不知道也不感兴趣什么对象接收它所发出的信号,这是真正的信息封装,保证了对象能被当作软件组件来使用。 槽能被用来接收信号,除此之外它们也是普通的成员函数。槽不知道是否有信号与它连接起来,正如对象不知道它发出信号是否会被接收一样。这样的机制确保了可以使用Qt创建一个个完全独立的组件。 你可以把你感兴趣的多个信号与一个个槽进行连接,也可以把一个信号与多个槽进行连接。甚至可以直接把一个信号连接到另一个信号(当第一个信号发送出去的时候,第二个信号紧接着被发送)。 就这样,信号与插槽建立了强大的组件编程机制。
第四章 一个小例子 一个最小的C++类声明可能表示为:
Counter() { m_value = 0; }
int value() const { return m_value; }
void setValue(int value);
一个小的QObject-based类可能表示为:
class Counter : public QObject
Counter() { m_value = 0; }
int value() const { return m_value; }
void setValue(int value);
void valueChanged(int newValue);
QObject-based版本与C++版本一样,有相同的内部状态,并能提供公共方法来访问这个状态,除此之外,QObject-based版本还可以利用信号和槽来支持组件编程。这个类通过发送一个valueChanged()信号类可以告知外界它的状态已经改变,同时它还有一个槽可以用来接收其他对象发来的信号。 所有包含信号或槽的类在他们声明的顶端都必须写上Q_OBJECT,同样也必须直接或间接继承自Qobject。 应用程序员需要实现槽。下面是Counter::setValue()槽的一种可能实现: void Counter::setValue(int value) { if (value != m_value) { m_value = value; emit valueChanged(value); } } emit行从对象中以新的value值作为参数,发送信号valueChanged()。 在下面的代码段中,我们创建两个Counter对象,然后利用QObject::connect():将第一个对象的valueChanged()信号与第二个对象的setValue()槽连接起来。
QObject::connect(&a, SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));
a.setValue(12); // a.value() == 12, b.value() == 12
b.setValue(48); // a.value() == 12, b.value() == 48 调用 a.setValue(12) 使a发送一个valueChanged(12) 信号,b将利用setValue() 槽来接收这个信号。比如,b.setValue(12)被调用,然后b发送相同的valueChanged()信号,但是因为没有槽与b的 valueChanged() 信号连接,因此这个信号被忽略。 注意,只有当value != m_value时,setValue()函数才可以设定值并且发送信号。这样就避免了循环连接时出现死循环(例如,如果b.valueChanged()与a.setValue()连接)。 一般情况下,发送一个信号的连接和发送两个信号的重复连接,都可以通过调用QObject::connect连接信号和槽时使用的参数。如果你在连接信号和槽时传递了Qt::UniqueConnection类型,只有它不是一个重复连接,连接才会成功。如果之前已经有了一个链接(相同的信号连接到同一对象的同一个槽上),那么连接将会失败并将返回false。 这个例子说明对象之间可以在不需要了解彼此信息的情况下相互协作。要实现这样的功能,对象之间仅需要互相连接就可以了,这种连接可以通过简单的调用QObject::connect() 函数或利用uic(User Interface Compiler,UI Designer工具之一,从.ui文件(XML样式)读取描述,然后生成C++代码。)的自动连接特性来完成。
第五章 示例编译 Qt工程编译时,C++预处理器会删改 signals, slots与emit关键字,以便让标准的C++编译器能够识别。因为这些关键字是Qt内部自定义的,而不是C++标准的关键字。 通过对包含信号与槽的类定义运行 moc 程序,会产生一个相应的C++源文件,这个源文件将会被编译,并参与应用程序的目标文件(Object file)链接过程。如果你直接使用 qmake 命令, 它会把自动调用 moc 程序的makefile规则添加到工程的makefile文件中。
第六章 信号(Signals) 信号在对象的内部状态改变的时候以某种方式发送出来,以通知该对象感兴趣的客户端或拥有者。只有在类中定义了信号才能在该类及其子类中发送信号。 当信号被发送时,一般情况下,与之相连的槽会立即被调用,这个过程与一般的函数调用是一样的。这一切的发生,信号与槽机制完全与任何的GUI事件循环无关。emit语句后面代码在所有的槽代码执行完毕之后将会继续执行。这个情况在使用“队列连接”(Queued Connections)时,会有些不同,这种情况下,emit关键字后面的代码会立即执行,而槽会在之后被执行。 如果一个信号同时与好几个槽连接,那么在信号产生时,这些槽将按与信号连接的先后顺序,逐个调用。 信号由moc程序自动产生,并且不能在“.cpp”文件中实现。信号不能有返回值(只能使用void)。 关于信号函数的注释:经验显示,如果信号与槽的参数不使用一些特殊的类型,那么它们的重用性将更好。如果QScrollBar::valueChanged()信号使用了一个特殊的参数,如:QScrollBar::Range,那么它仅可能被连接到为QScrollBar设计的特殊槽上。这是软件复用思想中最深恶痛绝的。这么做的话,该信号就不能与其他不同的窗口输入部件(Input Widget)中的槽连接了。
第七章 槽(Slots) 槽与信号连接之后,当信号发送时就会被调用。槽其实就是普通的C++函数,并且可以像普通函数一样调用.它们唯一的特点就是可以与信号相连。 因为槽是普通的成员函数,所以当它们被直接调用时,它们也遵循C++函数调用规则。然而,它们也可以通过信号-槽之间的连接,由其它组件调用而不管槽的访问级别(如:private级别的槽)。换句话说,从任意的类实例中发送出来的信号都能调用一个不相关类中的private级别的槽。 槽也可以用virutal定义,实际上,后面我们会发现这样子做很有用。 与回调函数(callback)相比,信号与槽会稍微慢一些(译者注:大概慢一个数量级左右,但以现在的硬件来说,我们根本感觉不出来。),这是因为信号-槽机制更灵活。当然,实际应用程序上,它们的不同点是忽略不计的。一般来讲,引发一个连接了多个槽的信号,会比直接调用信号的接收者,慢10倍左右(这里不与虚函数调用比较)。这是因为,信号-槽机制需要定位连接对象,以确保安全地遍历所有的连接(例如,检查在信号发送过程中,所有要接收信号的对象未被销毁),以及参数的正反序列化,这些过程都必须占用花费。举个例子说明下,同时调用十个非虚函数听起来好像很多,但它的花费将比任意的new或delete操作少得多。试想下,你执行创建或销毁字符串,向量或列表的操作,它们将引发new或delete操作,与这些操作相比,信号-槽所需要的花费仅仅占整个函数调用花费的一小部分。 在槽中进行系统调用的花费与上面讲述的类似。在一台i586-500机器上,你每秒可以进行2,000,000次的1对1信号-槽调用,或者1,200,000次的1对2信号-槽调用。信号-槽机制的简单性与灵活性,与它们所占花费比起来,那真是物超所值。当然客户在使用你的程序时,完全不会察觉到这种效率上的微小变化。 注意,有些第三方库可能定义了一些变量,像signals或slots,这些库在Qt程序中使用时,这可能会引发编译器的警告或错误。要解决这个问题,使用#undef预处理器关闭这些预处理符号。
第八章 元对象(Meta-Object)信息 元对象编译器(meta-object compiler, moc)会解析C++文件中的类声明,并生成相应的C++代码以初始化元对象。元对象包含所有信号,槽成员的名称,以及这些函数的指针。 元对象也包含了一些额外的信息,如:对象的类名。这样子你就可以检查一个对象是否派生自一个特殊的类,如下代码所示: if (widget->inherits("QAbstractButton")) { QAbstractButton *button = static_cast(widget); button->toggle(); } 元对象信息也被qobject_cast()操作符所使用,它与QObject::inherits()相似,但它更少出错 if (QAbstractButton *button = qobject_cast(widget)) button->toggle(); 更多元对象系统信息参见: 元对象系统。
第九章 范例分析 现在我们看一个简单的窗口部件示例代码,摘自LcdNumber类,代码有些与信号-槽不相关的成员函数已被省略。 #ifndef LCDNUMBER_H #define LCDNUMBER_H #include <QFrame> class LcdNumber : public QFrame { Q_OBJECT QFrame继承自QWidget,QWidget继承自QObject,从而使LcdNumber具有我们前面所讲的信号-槽特性。 Q_OBJECT宏由预处理器展开,该宏声明了几个由moc实现的成员函数。如果编译时,你发现“undefined reference to vtable for LcdNumber”错误消息,那么你就有可能是忘了运行moc程序,或者是在链接程序中,没有包含moc的输出文件。 public: LcdNumber(QWidget *parent = 0); LcdNumber的构造函数与moc的联系看上去没那么明显,但如果你从QWidget继承,那你可能想在你的构造函数中传递parent参数,并传递给基类的的构造函数。 一些构造函数在此被省略了。moc程序会忽略成员函数。 signals: void overflow(); LcdNumber要显示的值超过它的值范围时,会发送一个信号。 如果你不担心值溢出,或者你知道值溢出是不可能发生的,那么可以忽略overflow()信号,例如,不要让它与任何槽连接。 |