前面两篇文章《QML 语言基础》和《Qt Quick 简单教程》中我们介绍了 QML 语言的基本语法和 Qt Quick 的常见元素,亲们,通过这两篇文章,您应该已经可以完成简单的 Qt Quick 应用了。接下来呢,哈,我们要介绍 Qt Quick 中一个灰常灰常重要的主题:事件处理。这将是比较长长长长的一篇,哦,不还有后续好几篇……废话少说,还是谈正事儿吧兄弟姐妹们。
本文是作者 Qt Quick 系列文章中的一篇,其它文章在这里:
GUI 应用都是基于事件的(同学,这么说对吗?),不管是用 C++ 还是用 QML ,你的应用都要处理事件,否则的话,哼哼,要你好看!当然当然,你也可以什么事儿不干,就搭个静态界面放那里欣赏……
Qt Quick 最大的一个特点,是与 Qt 元对象系统的结合;而这里边,我们熟稔的,鼎鼎大名大名鼎鼎的,要不断打交道的,就是信号与槽了。历史是割不断的,继承与发展才是正确的价值观……,了解过 Qt 的开发者一定对信号与槽印象深刻。在 QML 中,在 Qt Quick 中,要想妥善的处理各种事件,同样离不开信号与槽。所以呢,在介绍具体的事件之前,我们先要介绍 QML 中如何使用信号与槽。
为了演示本文的示例,我们先介绍一个工具: qmlscene 。
这是 Qt 框架提供的便利工具,使用它,你可以不用建立项目不用撰写 C++ 代码,就可以看到你编写的 qml 文件的效果。我们以 Windows 7 系统为例, qmlscene 需要在命令行窗口中使用。你可以这样打开 Qt 自带的命令行开发环境,参考图 1 找到命令行环境的快捷方式:
图 1 Qt 命令行环境的快捷方式
一旦你打开了命令行工具,它会帮你设置好 Qt 需要的环境变量,敲一个 qmake 命令试试吧。如和 图 2 所示:
图 2 Qt 命令行开发环境
如果你想知道 qmlscene 工具的详细用法,请输入 qmlscene --help 命令。如图 3 :
图 3 qmlscene 的帮助
现在,使用 cd 命令切换到你存放 qml 文档的目录下,就可以验证 qml 的效果了。只需要执行这样的命令: qmlscene yourapp.qml 。当然,yourapp.qml 要替换为你实际的 qml 文档的名字。我这里只是示例。如果你直接输入 qmlscene 命令来执行,那么它会打开一个文件选择对话框,让你选择一个 qml 文档,当你选择了之后呢,该文档对应的界面就会显示出来。我建议你使用 qmlscene yourapp.qml 这种方式,无它,唯快尔。
来看一个简单的 Hello World ,qml 文档是 hello_world.qml ,内容如下:
import QtQuick 2.0 import QtQuick.Controls 1.1 Rectangle { width: 320; height: 240; color: "gray"; Text { anchors.centerIn: parent; text: "Hello World!"; color: "blue"; font.pixelSize: 32; } }
图 4 使用 qmlscene 执行 Hello World 示例
好啦, qmlscene 的介绍到此为止,下面我们来看信号与槽了。
我们先看一个简单的示例, qml 中只有一个退出按钮,点击退出应用。 qml 文档为 button_quit.qml ,内容如下:
import QtQuick 2.0 import QtQuick.Controls 1.1 Rectangle { width: 320; height: 240; color: "gray"; Button { text: "Quit"; anchors.centerIn: parent; onClicked: { Qt.quit(); } } }
图 5 button_quit 示例效果
现在看看代码有何特别之处。其实在《Qt on Android: Qt Quick 简单教程》中我们已经见过类似的代码了:
onClicked:{}
信号处理器,其实等价于 Qt 中的槽。但是我们没有看到类似 C++ 中的明确定义的函数……没错,就是这样,你的的确确只看到了一对花括号!对啦,这是 JavaScript 中的代码块。其实呢,你可以理解为它是一个匿名函数。而 JavaScript 中的函数,其实具名的代码块。函数的好处是你可以在其它地方根据名字调用它,而代码块的好处是,除了定义它的地方,没人能调用它,一句话,它是私有的。代码块就是一系列语句的组合,它的作用就是使语句序列一起执行。
让我们回头再看信号处理器,它的名字还有点儿特别,一般是 on{Signal} 这种形式。在上节的示例中, Button 元素有一个名为 clicked() 的信号,我们提供的信号处理器是酱紫的:
onClicked: { Qt.quit(); }
Qt 对象是 Qt Quick 导出到 QML 环境中的对象,它的 quit() 方法退出应用。还有很多其它的方法,比如 rgba() 用于构造一个颜色(color类型), md5() 用来计算一段数据的 MD5 值……
你看到了,当信号是 clicked() 时,信号处理器就命名为 onClicked 。就这么简单,以 on 起始后跟信号名字(第一个字母大写)。如果你点击我们的 quit 按钮,应用就真的退出了。
上面的示例,信号处理器放在拥有信号的元素内部,当元素信号发射时处理器被调用。还有一种情况,要处理的信号不是当前元素发出来的,而是来自其它类型(对象)比如处理按键的 Keys ,这就是附加信号处理器。在 QML 语言的语法中,有一个附加属性(attached properties)和附加信号处理器(attached signal handlers)的概念,这是附加到一个对象上的额外的属性。从本质上讲,这些属性是由附加类型(attaching type)来实现和提供的,它们可能被附加到另一种类型的对象上。附加属性与普通属性的区别在于,对象的普通属性是由对象本身或其基类(或沿继承层级向上追溯的祖先们)提供的。
举个例子,下面的 Item 对象使用了附加属性和附加信号处理器:
import QtQuick 2.0 Item { width: 100; height: 100; focus: true; Keys.enabled: false; Keys.onReturnPressed: console.log("Return key was pressed"); }
enabled 是 Keys 对象的一个属性。
onReturnPressed 其实是 Keys 对象的一个信号。
对于附加信号处理器,和前面讲到的普通信号处理器又有所不同。普通信号处理器,你先要知道信号名字,然后按照 on{Signal} 的语法来定义信号处理器的名字;而附加信号处理器,信号名字本身已经是 onXXX 的形式,你只要通过附加类型名字引用它,把代码块赋值给它即可。下面是另外的代码片段:
Rectangle { width: 320; height: 480; color: "gray"; focus: true; Keys.enabled: true; Keys.onEscapePressed: { Qt.quit(); } }
Component 对象也有一些附加信号,如 Component.onCompleted() 、 Component.onDestruction() 。可以用来在 Component 创建完成或销毁时执行一些 JavaScript 代码来做与初始化或反初始化相关的工作。比如下面的代码:
Rectangle { Component.onCompleted: console.log("Completed Running!"); Component.onDestruction: console.log("Destruction Beginning!"); }
信号处理器与附加信号处理器有一个共性:响应信号的代码都放在元素内部,通过 JavaScript 代码块就地实现。而其实呢, Qt Quick 中还有另外一种方式来处理信号与槽,那就是:专业的 Connections 。
一个 Connections 对象创建一个到 QML 信号的连接。
前面两节在处理 QML 信号时,都是用 on{Signal} 这种就地代码块的方式。而在有些情况下,这样的处理并不方便。比如:
Connections 有一个属性名为 target ,它呢,指向发出信号的对象。
下面就看看 Connections 怎么使用。一般的用法:
Connections { target: area; on{Signal}: function or code block; }
import QtQuick 2.0 import QtQuick.Controls 1.1 Rectangle { width: 320; height: 240; color: "gray"; Text { id: text1; anchors.horizontalCenter: parent.horizontalCenter; anchors.top: parent.top; anchors.topMargin: 20; text: "Text One"; color: "blue"; font.pixelSize: 28; } Text { id: text2; anchors.horizontalCenter: parent.horizontalCenter; anchors.top: text1.bottom; anchors.topMargin: 8; text: "Text Two"; color: "blue"; font.pixelSize: 28; } Button { id: changeButton; anchors.top: text2.bottom; anchors.topMargin: 8; anchors.horizontalCenter: parent.horizontalCenter; text: "Change"; } Connections { target: changeButton; onClicked: { text1.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1); text2.color = Qt.rgba(Math.random(), Math.random(), Math.random(), 1); } } }
Math 是 JavaScript 语言内置的对象,有 random() / sin() / max() / min() / abs() 等等方法,参见 w3c 的文档。
图 6 是运行后的效果图:
图 6 Connections 的使用
到现在为止,我们说的都是如何使用 QML 中已有类型定义的信号。这些信号呢,其实又分类两类。一类是由用户输入产生的,比如按键、鼠标、触摸屏、传感器等,另一类呢是由对象状态或属性变化产生的,比如 Image 对象的 status 属性(在《Qt Quick 简单教程》有用到)。那么有一个问题,就是,怎样知道一个对象有哪些信号?
怎样找到你感兴趣的信号呢?
首先是查阅 Qt 帮助,你可以使用 Qt 帮助的索引模式,以你关心的对象名字为关键字检索,比如 Button ,检索结果如图 7 所示:
图7 使用 Qt 帮助索引模式检索 Button 对象
有时你会在查找结果中看到多个连接,点进去看看是否是 QML 类型即可。
还有另外一种方式,使用 Qt 帮助的目录模式。如图 8 所示:
图 8 使用 Qt 帮助目录模式
一旦你找到一个对象的文档,你可以找到它的部分信号说明。还是以 Button 为例,看图 9:
图 9 Button 的信号 clicked()
Qt Quick 相关类型的文档中,你可以看到对象的属性和信号。列为属性的,可以在 QML 中访问;列为信号的,可以连接它,通过信号处理器来响应用于操作。
至于具体某个属性或信号是何含义,点击它们,跟过去看看吧。
话说,Qt 的文档是否列出了 Qt Quick 类型的所有信号了呢?想必我这么说你用脚趾头也可以想到答案:没有!个人认为这是 Qt 5.2 文档关于 Qt Quick 和 QML 类型手册的一个缺失。前面提到 QML 中的信号,一类是输入事件触发的,一类是属性变化触发的。文档中缺失的,正是属性变化触发的那些信号。不过呢,不过我们有办法找到它。
要说呢, Qt Quick 中你看到的很多对象,都是 Qt C++ 中实现,然后导入到 QML 环境中的。所以呢,如果你关心那些被文档隐藏了的信号,可以这么做:
怎么找 QML 类型对应的 C++ 类型呢?很简单,只需要使用 Component.onCompleted 附加信号,在附加信号处理器中输出类型信息即可。示例代码:
import QtQuick 2.0 import QtQuick.Controls 1.1 Rectangle { width: 320; height: 240; color: "gray"; Text { id: text1; anchors.centerIn: parent; text: "Hello World!"; color: "blue"; font.pixelSize: 32; } Button { id: button1; text: "A Button"; anchors.top: text1.bottom; anchors.topMargin: 4; } Image { id: image1; } Component.onCompleted: { console.log("QML Text\'s C++ type - ", text1); console.log("QML Button\'s C++ type - ", button1); console.log("QML Image\'s C++ type - ", image1); } }
图 10 打印 QML 类型对应的 C++ 类型
别看界面效果哦,注意看命令行窗口的输出。对,QML Text 对应的 C++ 类型是 QQuickText , QML Image 对应的 C++ 类型是 QQuickImage ,而 Button ,其实是 QML 中定义的对象(含有 QMLTYPE 字样)。我这里使用的 Qt 5.2.0 ,如果是其他的 Qt 版本,比如 Qt 4.7 / Qt 4.8 ,能不能看到我就不知道了。
下面我们就以 QQuickText 为例,找到它的头文件,路径是C:\Qt\Qt5.2.0\5.2.0\mingw48_32\include\QtQuick\5.2.0\QtQuick\private\qquicktext_p.h 。你的环境中根据 Qt SDK 安装目录,路径可能有所不同。
看看 QQuickText 类的声明吧(我截取了属性部分的几行代码):
// qquicktext_p.h // class Q_QUICK_PRIVATE_EXPORT QQuickText : public QQuickImplicitSizeItem { Q_OBJECT Q_ENUMS(HAlignment) Q_ENUMS(VAlignment) Q_ENUMS(TextStyle) Q_ENUMS(TextFormat) Q_ENUMS(TextElideMode) Q_ENUMS(WrapMode) Q_ENUMS(LineHeightMode) Q_ENUMS(FontSizeMode) Q_ENUMS(RenderType) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(QColor linkColor READ linkColor WRITE setLinkColor NOTIFY linkColorChanged) Q_PROPERTY(TextStyle style READ style WRITE setStyle NOTIFY styleChanged) ... }
再来看看 text 和 color 对应的信号原型:
class Q_QUICK_PRIVATE_EXPORT QQuickText : public QQuickImplicitSizeItem { Q_OBJECT ... Q_SIGNALS: void textChanged(const QString &text); void colorChanged(); }
import QtQuick 2.0 import QtQuick.Controls 1.1 Rectangle { width: 320; height: 240; color: "gray"; Text { id: hello; anchors.centerIn: parent; text: "Hello World!"; color: "blue"; font.pixelSize: 32; onTextChanged: { console.log(text); } } Button { anchors.top: hello.bottom; anchors.topMargin: 8; anchors.horizontalCenter: parent.horizontalCenter; text: "Change"; onClicked: { hello.text = "Hello Qt Quick"; } } }
注意啦,QML 信号的参数名字,可以直接在信号处理器中访问。之前只用没说,这里特意说一下这点。如果你通过头文件找属性绑定的信号,就可以观察信号的参数,在 QML 中使用。
图 11 是运行效果:
图 11 属性绑定的信号
你看,图片上的文字变了哈,命令行也输出了新的文本:"Hello Qt Quick" 。喏,没骗你。
行文至此,如何在 QML 中使用已知类型的信号,已经介绍差不多了。
当自定义类型不可避免,当你需要通知别的对象你的状态发生了变化,当你对象的粉丝想了解你对象的近况……此时此刻,彼时彼刻,最好的方法, Qt 给我们的,QML 给我们的,还是信号。
现在我们就来看怎么定义自己的信号。
如果你自己定义新的 QML 类型,可以使用 signal 关键字给你的类型添加信号。其语法如下:
signal <name>[([<type> <parameter name>[, ...]])]
信号其实是个方法(函数),所以呢,它的发射,实际是通过调用以信号名为名的方法达成的。
举个实例的例子:我们在界面上放一个字符串,两个代表颜色的小方块,点小方块,字符串的颜色就变成小方块的颜色。先看代码(my_signal.qml):
import QtQuick 2.0 import QtQuick.Controls 1.1 Rectangle { width: 320; height: 240; color: "#C0C0C0"; Text { id: coloredText; anchors.horizontalCenter: parent.horizontalCenter; anchors.top: parent.top; anchors.topMargin: 4; text: "Hello World!"; font.pixelSize: 32; } Component { id: colorComponent; Rectangle { id: colorPicker; width: 50; height: 30; signal colorPicked(color clr); MouseArea { anchors.fill: parent onPressed: colorPicker.colorPicked(colorPicker.color); } } } Loader{ id: redLoader; anchors.left: parent.left; anchors.leftMargin: 4; anchors.bottom: parent.bottom; anchors.bottomMargin: 4; sourceComponent: colorComponent; onLoaded:{ item.color = "red"; } } Loader{ id: blueLoader; anchors.left: redLoader.right; anchors.leftMargin: 4; anchors.bottom: parent.bottom; anchors.bottomMargin: 4; sourceComponent: colorComponent; onLoaded:{ item.color = "blue"; } } Connections { target: redLoader.item; onColorPicked:{ coloredText.color = clr; } } Connections { target: blueLoader.item; onColorPicked:{ coloredText.color = clr; } } }
首先定义了一个 Text 对象,id 为 coloredText ,后面会根据这个 id 来改变它的颜色。
然后我定义了一个组件,组件内有一个 Rectangle 对象,这里是我们定义信号的地方。我设置 Rectangle 的尺寸,然后定义了信号 colorPicked ,语句如下:
signal colorPicked(color clr);为了触发信号,我给 Rectangle 引入了 MouseArea 。MouseArea 是专门处理鼠标操作的 item ,这里我们先知道它有一个 onClicked() 信号就行了。我们给这个信号指定信号处理器,在信号处理器中调用我们刚定义的信号:colorPicker.colorPicked(colorPicker.color); ……如你所见,信号的触发就是一个函数调用。
Loader 是专门用来动态创建组件的,它可以从 qml 文件中创建组件,也可以指定 sourceComponent 来创建,这里的示例,因为组件是嵌入在主 qml 文件中定义的,所以使用了 sourceComponent 方式。我给每个 Loader 一个 id ,以便后面连接时使用。我还使用 anchors 为 Loader 布局。最后呢,在 Loader 的 onLoaded 信号处理器内给 Rectangle 对象配置颜色。
创建完 Loader ,就是建立连接了。这里使用 Connections 来建立信号的连接,target 指向刚才说的 Loader 对象的 item 属性, item 属性实际指向 Loader 创建的对象。在 Connections 对象中,通过 onColorPicked 信号处理器响应用户点击操作。你看到了,我们定义 colorPicked 信号时命名的参数是 clr ,所以 Connections 的信号处理器中可以直接使用它给 coloredText 对象复制。
好了,看看初始运行的效果,图 12 :
图 12 自定义信号之变色文本初始效果图
图 13 是我点击了红色的颜色选择组件时的效果:
图 13 选择红色后的效果
嗯,不知道你是否已经明白如何使用自定义信号?多练练吧。这个示例用到的 Component 和 Loader ,还请您先自行查阅 Qt 帮助来理解,后续我们有专门的文章来讲述它们。
前面我们使用信号时,要么通过信号处理器,要么使用 Connections 对象。其实在 QML 中还有一种更一般的方式。先回想下 Qt C++ 中我们如何使用信号与槽…… QObject::connection() ,诺,木错,就是它了。对应的,在 QML 中,其实 signal 是个对象,它也有一个 connect() 方法,你可以使用它连接到任意的方法上哦。有 connect() 就有 disconnect() ,正确, signal 对象的的确确有这两个方法,允许我们使用它维护连接。
signal 对象的 connect() 方法允许你连接一个信号到另外一个信号或者方法。其实没差别,信号本身也是个方法(函数)。当信号发射时,连接到信号上的其它信号或方法就会被调用。
signal 对象的这种连接方式,使用起来比信号处理器更加灵活。前面介绍信号处理器时,已经提到,信号处理器和信号是一对一的关系。而 signal 对象的这种连接方式,使得一个信号能够连接多个方法。
举个简单的例子来看看如何使用吧。下面的代码(来自 Qt 帮助), messageReceived 信号通过 connect() 方法连接到了三个方法上。
Rectangle { id: relay; signal messageReceived(string person, string notice); Component.onCompleted: { relay.messageReceived.connect(sendToPost); relay.messageReceived.connect(sendToTelegraph); relay.messageReceived.connect(sendToEmail); relay.messageReceived("Tom", "Happy Birthday"); } function sendToPost(person, notice) { console.log("Sending to post: " + person + ", " + notice); } function sendToTelegraph(person, notice) { console.log("Sending to telegraph: " + person + ", " + notice); } function sendToEmail(person, notice) { console.log("Sending to email: " + person + ", " + notice); } }
Rectangle { id: forwarder; width: 100; height: 100; signal send(); onSend: console.log("Send clicked"); MouseArea { id: mousearea; anchors.fill: parent; onClicked: console.log("MouseArea clicked"); } Component.onCompleted: { mousearea.clicked.connect(send); } }
K.O. !终于把 QML 中的信号与槽介绍完了。
温故知新哦,回顾一下本系列的文章:Qt Quick 简介