Qt Quick 事件处理之信号与槽
前面两篇文章《QML 语言基础》和《Qt Quick 简单教程》中我们介绍了 QML 语言的基本的语法和 Qt Quick 的常见元素,亲们,通过这两篇文章,您应该已经能够完毕简单的 Qt Quick 应用了。接下来呢,哈。我们要介绍 Qt Quick 中一个灰常灰常重要的主题:事件处理。
这将是比較长长长长的一篇。哦,不还有兴许好几篇……废话少说,还是谈正事儿吧兄弟姐妹们。
本文是作者 Qt Quick 系列文章中的一篇。其他文章在这里:
- Qt Quick 简单介绍
- QML 语言基础
- Qt Quick 之 Hello World 图文具体解释
- Qt Quick 简单教程
- Qt Quick 事件处理之信号与槽
- Qt Quick事件处理之鼠标、键盘、定时器
- Qt Quick 事件处理之捏拉缩放与旋转
- Qt Quick 组件与对象动态创建具体解释
- Qt Quick 布局介绍
- Qt Quick 之 QML 与 C++ 混合编程具体解释
GUI 应用都是基于事件的(同学,这么说对吗?)。无论是用 C++ 还是用 QML ,你的应用都要处理事件。否则的话,哼哼,要你好看!
当然当然,你也能够什么事儿不干,就搭个静态界面放那里赞赏……
Qt Quick 最大的一个特点。是与 Qt 元对象系统的结合。而这里边,我们熟稔的。鼎鼎大名大名鼎鼎的。要不断打交道的。就是信号与槽了。历史是割不断的,继承与发展才是正确的价值观……。了解过 Qt 的开发人员一定对信号与槽印象深刻。
在 QML 中,在 Qt Quick 中,要想妥善的处理各种事件。相同离不开信号与槽。所以呢,在介绍具体的事件之前,我们先要介绍 QML 中怎样使用信号与槽。
为了演示本文的演示样例,我们先介绍一个工具: qmlscene 。
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; } }
在命令行环境运行 qmlscene hello_world.qml ,效果如图 4 所看到的:
图 4 使用 qmlscene 运行 Hello World 演示样例
好啦, qmlscene 的介绍到此为止,以下我们来看信号与槽了。
连接 QML 类型的已知信号
我们先看一个简单的演示样例, qml 中仅仅有一个退出button。点击退出应用。 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(); } } }
使用 qmlscene 运行 button_quit.qml 效果如图 5 所看到的:
图 5 button_quit 演示样例效果
如今看看代码有何特别之处。事实上在《Qt on Android: Qt Quick 简单教程》中我们已经见过相似的代码了:
onClicked:{}
对,就是这个 onClicked ,事实上就包括了 QML 中使用信号与槽的一般形式:信号处理器。
信号处理器
信号处理器,事实上等价于 Qt 中的槽。
可是我们没有看到相似 C++ 中的明白定义的函数……没错,就是这样。你的的确确仅仅看到了一对花括号!
对啦,这是 JavaScript 中的代码块。事实上呢,你能够理解为它是一个匿名函数。
而 JavaScript 中的函数,事实上具名的代码块。函数的优点是你能够在其他地方依据名字调用它,而代码块的优点是。除了定义它的地方,没人能调用它,一句话。它是私有的。代码块就是一系列语句的组合,它的作用就是使语句序列一起运行。
让我们回头再看信号处理器,它的名字还有点儿特别。通常是 on{Signal} 这样的形式。
在上节的演示样例中, Button 元素有一个名为 clicked() 的信号,我们提供的信号处理器是酱紫的:
onClicked: { Qt.quit(); }
非常easy吧。仅仅是调用 Qt.quit() 来退出应用而已。
Qt 对象是 Qt Quick 导出到 QML 环境中的对象,它的 quit() 方法退出应用。还有非常多其他的方法,比方 rgba() 用于构造一个颜色(color类型)。 md5() 用来计算一段数据的 MD5 值……
你看到了,当信号是 clicked() 时,信号处理器就命名为 onClicked 。
就这么简单。以 on 起始后跟信号名字(第一个字母大写)。假设你点击我们的 quit button,应用就真的退出了。
上面的演示样例。信号处理器放在拥有信号的元素内部,当元素信号发射时处理器被调用。另一种情况,要处理的信号不是当前元素发出来的,而是来自其他类型(对象)比方处理按键的 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"); }
你看, Item 对象能够訪问和设置 Keys.enabled 和 Keys.onReturnPressed 的值。
enabled 是 Keys 对象的一个属性。
onReturnPressed 事实上是 Keys 对象的一个信号。
对于附加信号处理器,和前面讲到的普通信号处理器又有所不同。
普通信号处理器,你先要知道信号名字,然后依照 on{Signal} 的语法来定义信号处理器的名字。而附加信号处理器,信号名字本身已经是 onXXX 的形式,你仅仅要通过附加类型名字引用它,把代码块赋值给它就可以。以下是另外的代码片段:
Rectangle { width: 320; height: 480; color: "gray"; focus: true; Keys.enabled: true; Keys.onEscapePressed: { Qt.quit(); } }
这个代码片段实现的功能非常easy:用户按下 Esc 键时退出应用。
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
一个 Connections 对象创建一个到 QML 信号的连接。
前面两节在处理 QML 信号时。都是用 on{Signal} 这样的就地代码块的方式。而在有些情况下。这样的处理并不方便。比方:
- 你须要将多个对象连接到同一个 QML 信号上
- 你须要在发出信号的对象的作用域之外来建立连接
- 发射信号的对象没有在 QML 中定义(可能是通过 C++ 导出的,这非经常见)
Connections 有一个属性名为 target 。它呢,指向发出信号的对象。
以下就看看 Connections 怎么使用。
一般的使用方法:
Connections { target: area; on{Signal}: function or code block; }
来看一个实际的演示样例。是酱紫的:界面上放置两个文本,一个button。每点button一次,两个文本对象都变颜色,而它们的颜色随机的。以下是演示样例代码:
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); } } }
代码比較简单,除了 Connections 和 Math ,没有其他的新内容。都是 《Qt Quick 简单教程》和《QML 语言基础》中讲过的。
我在 Connections 对象中指定 target 为 changeButton (Changebutton的 id),然后定义了 onClicked 信号处理器,在信号处理器中使用 Math 对象的 random() 方法构造颜色值来改变两个文本的颜色。
Math 是 JavaScript 语言内置的对象。有 random() / sin() / max() / min() / abs() 等等方法,參见 w3c 的文档。
图 6 是运行后的效果图:
图 6 Connections 的使用
到如今为止。我们说的都是怎样使用 QML 中已有类型定义的信号。这些信号呢,事实上又分类两类。一类是由用户输入产生的,比方按键、鼠标、触摸屏、传感器等,另一类呢是由对象状态或属性变化产生的。比方 Image 对象的 status 属性(在《Qt Quick 简单教程》实用到)。那么有一个问题,就是,怎样知道一个对象有哪些信号?
怎样寻找感兴趣的信号
怎样找到你感兴趣的信号呢?
Qt 帮助
首先是查阅 Qt 帮助,你能够使用 Qt 帮助的索引模式。以你关心的对象名字为keyword检索,比方 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 Quick 中你看到的非常多对象,都是 Qt C++ 中实现,然后导入到 QML 环境中的。
所以呢。假设你关心那些被文档隐藏了的信号,能够这么做:
- 找到 QML 类型相应的 C++ 类型
- 找到 C++ 类型的头文件。查看属性声明来确认是否有信号与属性关联
怎么找 QML 类型相应的 C++ 类型呢?非常easy,仅仅须要使用 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); } }
如代码所看到的。我们使用 console 对象来输出 QML 对象,它会打印出 QML 对象的实际类型。图 10 是使用 qmlscene 运行 cplusplus_types.qml 的效果图:
图 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) ... }
亲爱的,看到了吗,那么多的 Q_PROPERTY 宏啊。
Q_PROPERTY 宏就是用来定义 QML 中可訪问属性的,当你看到 NOTIFY 字样,它后面的字段就是与属性绑定的信号的名字。
Qt 实现了动态属性绑定,当你为 QML Text 的属性 color 赋值时,实际上会调用到 QQuickText 的 setColor() 函数,也会触发 colorChanged() 信号。
再来看看 text 和 color 相应的信号原型:
class Q_QUICK_PRIVATE_EXPORT QQuickText : public QQuickImplicitSizeItem { Q_OBJECT ... Q_SIGNALS: void textChanged(const QString &text); void colorChanged(); }
看到了吧, textChanged 信号有个 text 參数。我们来看看 textChanged 信号怎样在 QML 中使用。演示样例代码(property_signal.qml):
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"; } } }
当用户点击button时,改变 Text 对象的文本为 "Hello Qt Quick" 。而我在 Text 对象中实现了 onTextChanged 信号处理器。使用 console.log 输出新的文本。
注意啦,QML 信号的參数名字,能够直接在信号处理器中訪问。之前仅仅用没说,这里特意说一下这点。假设你通过头文件找属性绑定的信号。就能够观察信号的參数。在 QML 中使用。
图 11 是运行效果:
图 11 属性绑定的信号
你看,图片上的文字变了哈,命令行也输出了新的文本:"Hello Qt Quick" 。喏,没骗你。
行文至此,怎样在 QML 中使用已知类型的信号,已经介绍差点儿相同了。
定义自己的信号
当自己定义类型不可避免,当你须要通知别的对象你的状态发生了变化。当你对象的粉丝想了解你对象的近况……此时此刻,彼时彼刻。最好的方法, Qt 给我们的,QML 给我们的,还是信号。
如今我们就来看怎么定义自己的信号。
自己定义信号与使用
假设你自己定义新的 QML 类型。能够使用 signal keyword给你的类型加入信号。其语法例如以下:
signal <name>[([<type> <parameter name>[, ...]])]
这是你的类型通告自己状态的最好的方式。符合 Qt 的风格,作为使用 Qt 的开发人员,还是崇德向善吧。
信号事实上是个方法(函数)。所以呢,它的发射,实际是通过调用以信号名为名的方法达成的。
举个实例的样例:我们在界面上放一个字符串,两个代表颜色的小方块,点小方块。字符串的颜色就变成小方块的颜色。先看代码(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); ……如你所见。信号的触发就是一个函数调用。
组件是能够反复利用的,一个组件能够在一个单独的 qml 文件里定义,也能够嵌入到其他 qml 文档中来定义以方便简单组件的使用。这里我选择在主 qml 文档内嵌入组件的定义。
定义好了组件,就能够使用 Loader 来载入组件了。
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); } }
这里我们给 Rectangle 定义了一个信号 send() ,在 Component.onCompleted 附加信号处理器中,把 MouseArea 对象的 clicked 信号连接到 Rectangle 的 send() 信号上。
K.O. !最终把 QML 中的信号与槽介绍完了。
温故知新哦。回想一下本系列的文章:Qt Quick 简单介绍
- QML 语言基础
- Qt Quick 之 Hello World 图文具体解释
- Qt Quick 简单教程
- Qt Quick 事件处理之信号与槽
- Qt Quick事件处理之鼠标、键盘、定时器
- Qt Quick 事件处理之捏拉缩放与旋转
- Qt Quick 组件与对象动态创建具体解释
- Qt Quick 布局介绍
- Qt Quick 之 QML 与 C++ 混合编程具体解释