Qt5 Cadaques中文完整版


1. Introduction 2. 初识Qt5(Meeet Qt5) i. 序(Preface) ii. Qt5介绍(Qt5 Introduction) iii. Qt构建模块(Qt Building Blocks) iv. Qt项⺫(Qt Project) 3. 开始学习(Get Start) i. 安装Qt5软件⼯具包(Installing Qt 5 SDK) ii. 你好世界(Hello World) iii. 应⽤程序类型(Application Types) iv. 总结( Summary) 4. Qt Creator集成开发环境(Qt Creator IDE) i. ⽤户界⾯(The User Interface) ii. 注册你的Qt⼯具箱(Registering your Qt Kit) iii. 项⺫管理(Managing Projects) iv. 使⽤编辑器(Using the Editor) v. 定位器(Locator) vi. 调试(Debugging) vii. 快捷键(Shortcuts) 5. QML快速⼊⻔(Quick Starter) i. QML语法(QML Syntax) ii. 基本元素(Basic Elements) iii. 组件(Compontents) iv. 简单的转换(Simple Transformations) v. 定位元素(Positioning Element) vi. 布局元素(Layout Items) vii. 输⼊元素(Input Element) viii. ⾼级⽤法(Advanced Techniques) 6. 动态元素(Fluid Elements) Table of Contents 《QmlBook》In Chinese 1 i. 动画(Animations) ii. 状态与过渡(States and Transitions) iii. ⾼级⽤法(Advanced Techniques) 7. 模型-视图-代理(Model-View-Delegate) i. 概念(Concept) ii. 基础模型(Basic Model) iii. 动态视图(Dynamic Views) iv. 代理(Delegate) v. ⾼级⽤法(Advanced Techniques) vi. 总结(Summary) 8. 画布元素(Canvas Element) i. 便捷的接⼝(Convenient API) ii. 渐变(Gradients) iii. 阴影(Shadows) iv. 图⽚(Images) v. 转换(Transformation) vi. 组合模式(Composition Mode) vii. 像素缓冲(Pixels Buffer) viii. 画布绘制(Canvas Paint) ix. HTML5画布移植(Porting from HTML5 Canvas) 9. 粒⼦模拟(Particle Simulations) i. 概念(Concept) ii. 简单的模拟(Simple Simulation) iii. 粒⼦参数(Particle Parameters) iv. 粒⼦⽅向(Directed Particle) v. 粒⼦画笔(Particle Painter) vi. 粒⼦控制(Affecting Particles) vii. 粒⼦组(Particle Group) viii. 总结(Summary) 10. 着⾊器效果(Shader Effect) i. OpenGL着⾊器(OpenGL Shader) ii. 着⾊器元素(Shader Elements) iii. ⽚段着⾊器(Fragement Shader) 《QmlBook》In Chinese 2 iv. 波浪效果(Wave Effect) v. 顶点着⾊器(Vertex Shader) vi. 剧幕效果(Curtain Effect) vii. Qt图像效果库(Qt GraphicsEffect Library) 11. 多媒体(Multimedia) i. 媒体播放(Playing Media) ii. 声⾳效果(Sounds Effects) iii. 视频流(Video Streams) iv. 捕捉图像(Capturing Images) v. ⾼级⽤法(Advanced Techniques) vi. 总结(Summary) 12. ⺴络(Networking) i. 通过HTTP服务UI(Serving UI via HTTP) ii. 模板(Templating) iii. HTTP请求(HTTP Requests) iv. 本地⽂件(Local files) v. REST接⼝(REST API) vi. 使⽤开放授权登陆验证(Authentication using OAuth) vii. 云服务(Engine IO) viii. Web Sockets ix. 总结(Summary) 13. 存储(Storage) i. 配置(Settings) ii. 本地存储 - SQL(Local Storage - SQL) iii. 其它存储接⼝(Other Storage APIs) 14. 动态QML(Dynamic QML) i. 动态加载组件(Loading Components Dynamically) i. 间接连接(Connecting Indirectly) ii. 间接绑定(Binding Indirectly) ii. 创建与销毁对象(Creating and Destroying Objects) i. 动态加载和实例化项(Dynamically Loading and Instantiating Items) ii. 从⽂本中动态实例化项(Dynamically Instantiating Items from 《QmlBook》In Chinese 3 Text) iii. 管理动态创建的元素(Managing Dynamically Created Elements) iii. 跟踪动态对象(Tracking Dynamic Objects) iv. 总结(Summary) 15. JavaScript i. 浏览器/HTML与QtQuick/QML对⽐(Browser/HTML vs QtQuick/QML) ii. JavaScript语法(The Language) iii. JS对象(JS Objects) iv. 创建JS控制台(Creating a JS Console) 16. Qt and C++ i. 演⽰程序(A Boilerplate Application) ii. Qt对象(The QObject) iii. 编译系统(Build Systems) i. QMake ii. CMake iv. Qt通⽤类(Common Qt Classes) i. QString ii. 顺序容器(Sequential Containers) iii. 组合容器(Associative Containers) iv. ⽂件IO(File IO) v. C++数据模型(Models in C++) i. ⼀个简单的模型(A simple model) ii. 更复杂的数据(More Complex Data) iii. 动态数据(Dynamic Data) iv. 进阶技巧(Advanced Techniques) 17. C++扩展QML(Extending QML with C++) i. 理解QML运⾏环境(Understanding the QML Run-time) ii. 插件内容(Plugin Content) iii. 创建插件(Creating the plugin) iv. FileIO实现(FileIO Implementation) v. 使⽤FileIO(Using FileIO) 《QmlBook》In Chinese 4 i. 应⽤程序窗⼝(The Application Window) ii. 使⽤动作(Using Actions) iii. 格式化表格(Formatting the Table) iv. 读取数据(Reading Data) v. 写⼊数据(Writing Data) vi. 收尾⼯作(Finishing Touch) vi. 总结(Summary) 18. 其它(Other) i. ⽰例源码 ii. 术语英汉对照表 iii. 格式定义 iv. 协作校正 《QmlBook》In Chinese 5 中⽂版《QmlBook》,原作地址QmlBook。 QML的中⽂资料⼀直⽐较少,希望⼤家能喜欢。 使⽤Gitbook制作,可以直接在线阅读。 点我下载 QmlBook上发布的课程已完成所有章节的翻译,进⼊第⼀次校正阶段,还有 很多不通顺或者翻译很⽣硬的地⽅。 很多术语可能不准确,如果有什么错误希望⼲⼤Qt爱好者谅解,并及时指 出。 排名不分先后 DreamerCorey Jakes Lee itviewer 《QmlBook》In Chinese 在线阅读 PDF下载 当前阶段 校对贡献 《QmlBook》In Chinese 6Introduction 初识Qt5(Meet Qt5) 序(Preface) Qt5介绍(Qt5 Introduction) Qt构建模块(Qt Building Blocks) Qt项⺫(Qt Project) 开始学习(Get Start) 安装Qt5软件⼯具包(Installing Qt5 SDK) 你好世界(Hello World) 应⽤程序类型(Application Types) 总结(Summary) Qt Creator集成开发环境(Qt Creator IDE) ⽤户界⾯(The User Interface) 注册你的Qt⼯具箱(Registering your Qt Kit) 使⽤编辑器(Managing Projects) 定位器(Locator) 调试(Debugging) 快捷键(Shortcuts) QML快速⼊⻔(Quick Starter) QML语法(QML Syntax) 基本元素(Basic Elements) 组件(Compontents) 简单的转换(Simple Transformations) 定位元素(Positioning Element) 布局元素(Layout items) 输⼊元素(Input Element) ⾼级⽤法(Advanced Techniques) 动态元素(Fluid Elements) 动画(Animations) 状态与过渡(States and Transitions) ⾼级⽤法(Advanced Techniques) 课程⺫录 《QmlBook》In Chinese 7Introduction 模型-视图-代理(Model-View-Delegate) 概念(Concept) 基础模型(Basic Model) 动态视图(Dynamic Views) 代理(Delegate) ⾼级⽤法(Advanced Techniques) 总结(Summary) 画布元素(Canvas Element) 便捷的接⼝(Convenient API) 渐变(Gradients) 阴影(Shadows) 图⽚(Images) 转换(Transformation) 组合模式(Composition Mode) 像素缓冲(Pixels Buffer) 画布绘制(Canvas Paint) HTML5画布移植(Porting from HTML5 Canvas) 粒⼦模拟(Particle Simulations) 概念(Concept) 简单的模拟(Simple Simulation) 粒⼦参数(Particle Parameters) 粒⼦⽅向(Directed Particle) 粒⼦画笔(Particle Painter) 粒⼦控制(Affecting Particles) 粒⼦组(Particle Group) 总结(Summary) 着⾊器效果(Shader Effect) OpenGL着⾊器(OpenGL Shader) 着⾊器元素(Shader Elements) ⽚段着⾊器(Fragment Shader) 波浪效果(Wave Effect) 顶点着⾊器(Vertex Shader) 剧幕效果(Curtain Effect) 《QmlBook》In Chinese 8Introduction Qt图像效果库(Qt GraphicsEffect Library) 多媒体(Multimedia) 媒体播放(Playing Media) 声⾳效果(Sounds Effects) 视频流(Video Streams) 捕捉图像(Capturing Images) ⾼级⽤法(Advanced Techniques) 总结(Summary) ⺴络(Networking) 通过HTTP服务UI(Serving UI via HTTP) 模板(Templating) HTTP请求(HTTP Requests) 本地⽂件(Local files) REST接⼝(REST API) 云服务(Engine IO) Web Sockets 总结(Summary) 存储(Stgorage) 配置(Settings) 本地存储-SQL(Local Storage - SQL) 其它存储接⼝(Other Storage APIs) 动态QML(Dynamic QML) 动态加载组件(Loading Components Dynamically) 创建与销毁对象(Creating and Destorying Objects) 跟踪动态对象(Tracking Dynamic Objects) 总结(Summary) JavaScript 浏览器/HTML与QtQuick/QML对⽐(Browser/HTML vs QtQuick/QML) JavaScript语法(The Language) JS对象(JS Objects) 创建JS控制台(Creating a JS Console) Qt and C++ 《QmlBook》In Chinese 9Introduction 演⽰程序(A Boilerplate Application) Qt对象(The QObject) 编译系统(Build Systems) Qt通⽤类(Common Qt Classes) C++数据模型(Models in C++) C++扩展QML(Extending QML with C++) 理解QML运⾏环境(Understanding the QML Run-time) 插件内容(Plugin Content) 创建插件(Creating the plugin) FileIO实现(FileIO Implementation) 使⽤FileIO(Using FileIO) 总结(Summary) 其它(Other) ⽰例源码 术语英汉对照表 格式定义 协作校正 感谢原作者Juergen Bocklage-Ryannel和Johan Thelin的分享。 Creative Commons Attribution Non Commercial Share Alike 4.0 有任何建议可以在项⺫issue中提出,或者email我:cwc1987@163.com 原作者 开源协议 问题与建议 《QmlBook》In Chinese 10Introduction 注意 这章的源代码能够在assetts folder找到。 这本书展⽰了通过Qt 5.x 版本开发应⽤的各⽅⾯内容。它主要关注新的Qt Quick的技术,但也提供了如何书写C++后端和Qt Quick扩展⽅法。 这⼀章是在⼀个较⾼的层次对Qt5的⼀个概述,它展⽰了开发者可以使⽤的不 同开发模式,并通过⼀个Qt5⽰例演⽰本书将要叙述的内容。另外本章内容也 提供了对Qt5所包含内容的⼀个⼲泛描述和如何与Qt的开发者联系的⽅法。 初识Qt5(Meet Qt5) 《QmlBook》In Chinese 11初识Qt5(Meeet Qt5) 历史 Qt4⾃2005年发布以来向成千上万的应⽤程序提供了开发框架,甚⾄是完整 的桌⾯与移动系统。在最近⼏年计算机的使⽤模式发⽣了改变。从PC机向便 携式设备和移动电脑发展。传统的桌⾯设备被越来越多的基于触摸屏的⼿机 设备取代。桌⾯⽤户的体验模式也在发⽣改变。在过去,Windows UI占据了 我们的世界,但现在我们会花更多的时间在其它的UI语⾔上。 Qt4设计⽤于满⾜在⼤多数主流平台的桌⾯上有⼀个可以使⽤的UI窗⼝部件。 如今对于Qt的开发者⾯临新的问题,它将提供更多的基于⽤户触摸驱动的⽤ 户界⾯并且适⽤于⼤多数主流桌⾯与移动系统。Qt4.7开始引进了QtQuick技 术,允许⽤户创建⼀个满⾜客户需求的,从简单的元素来实现⼀个完整的新 的⽤户界⾯。 Qt5是Qt4版本完整的更新,到Qt4.8版本,Qt4已经发布了7年。是时候让这 个令⼈惊奇的⼯具更加惊奇了。 Qt5主要关注以下⽅⾯: 杰出的图形绘制:Qt Quick2是基于OpenGL(ES)场景的实现。重组的图 形堆栈可以得到更加好的图形效果与更加简单的使⽤⽅法,在这⼀领域 是之前是从未实现的。 开发者⽣产率:QML和JavaScript语⾔是主要⽤于创建UI的⽅法。后端 将有C++来完成绘制。将JavaScript与C++分开能够快速的迭代开发,让 前端的开发⼈员专注于创建漂亮的⽤户界⾯,后端的C++开发⼈员专注 于稳定,性能和扩展。 跨平台移植性:基于Qt平台的统⼀抽象概念,现在可以更加容易和快速 序(Preface) 1.1.1 Qt5关注⽅⾯(Qt5 Focus) 《QmlBook》In Chinese 12序(Preface) 的将Qt移植到更多的平台上。Qt5是⼀个围绕Qt必要组件和附加组件的 概念,操作系统开发者只需要专注于必要模块的实现,可以使程序更加 效率的运⾏。 开放的开发:Qt是由Qt-Porject(qt-project.org)主持的开放管理的项⺫, 它的开发是开放的,由Qt社区驱动的。 《QmlBook》In Chinese 13序(Preface) Qt Quick是Qt5中⽤户界⾯技术的涵盖。Qt Quick⾃⾝包含了以下⼏种技术: QML-使⽤于⽤户界⾯的标识语⾔ JavaScript-动态脚本语⾔ Qt C++-具有⾼度可移植性的C++库. 类似HTML语⾔,QML是⼀个标识语⾔。它由QtQuick封装在Item {}的元素的 标识组成。它从头设计了⽤户界⾯的创建,并且可以让开发⼈员快速,简单 的理解。⽤户界⾯可以使⽤JavaScript代码来提供和加强更多的功能。Qt Quick可以使⽤你⾃⼰本地已有的Qt C++轻松快速的扩展它的能⼒。简单声 明的UI被称作前端,本地部分被称作后端。这样你可以将程序的计算密集部 分与来⾃应⽤程序⽤户界⾯操作部分分开。 在典型的项⺫中前端开发使⽤QML/JaveScript,后端代码开发使⽤Qt C++来 Qt5介绍(Qt5 Introduction) 1.2.1 Qt Quick 《QmlBook》In Chinese 14Qt5介绍(Qt5 Introduction) 完成系统接⼝和繁重的计算⼯作。这样就很⾃然的将设计界⾯的开发者和功 能开发者分开了。后端开发测试使⽤Qt⾃有的单元测试框架后,导出给前端 开发者使⽤。 让我们来使⽤QtQuick来创建⼀个简单的⽤户界⾯,展⽰QML语⾔某些⽅⾯ 的特性。最后我们将获得⼀个旋转的⻛⻋。 我们开始创建⼀个空的main.qml⽂档。所有的QML⽂件都已.qml作为后缀。 作为⼀个标识语⾔(类似HTML)⼀个QML⽂档需要并且只有⼀个根元素, 在我们的案例中是⼀个基于background的图像⾼度与宽度的⼏何图形元素: import QtQuick 2.0 Image { id: root source: "images/background.png" } QML不会对根元素设置任何限制,我们使⽤⼀个backgournd图像作为资源的 图像元素来作为我们的根元素。 1.2.2 ⼀个⽤户界⾯(Digesting an User Interface) 《QmlBook》In Chinese 15Qt5介绍(Qt5 Introduction) 注意 每⼀个元素都有属性,⽐如⼀个图像有宽度,⾼度但是也有⼀些其它的属性 例如资源。图像元素的⼤⼩能够⾃动的从图像⼤⼩上得出。否则我们应该设 置宽度和⾼度属性来显⽰有效的像素。 ⼤多数典型的元素都放置在QtQuick2.0模块中,我们⾸先应该在第⼀⾏作这 个重要的声明。 id是这个特殊的属性是可选的,包含了⼀个标识符,在⽂档后⾯的地⽅可以 直接引⽤。 重要提⽰:⼀个id属性⽆法在它被设置后改变,并且在程序执⾏期间⽆法被 设置。使⽤root作为根元素id仅仅是作者的习惯,可以在⽐较⼤的QML⽂档 中⽅便的引⽤最顶层元素。 ⻛⻋作为前景元素使⽤图像的⽅式放置在我们的⽤户界⾯上。 《QmlBook》In Chinese 16Qt5介绍(Qt5 Introduction) 正常情况下你的⽤户界⾯应该有不同类型的元素构成,⽽不是像我们的例⼦ ⼀样只有图像元素。 Image { id: root ... Image { id: wheel anchors.centerIn: parent source: "images/pinwheel.png" } ... } 为了把⻛⻋放在中间的位置,我们使⽤了⼀个复杂的属性,称之为锚。锚定 允许你指定⼏何对象与⽗对象或者同级对象之间的位置关系。⽐如放置我在 另⼀个元素中间(anchors.centerIn:parent).有左边(left),右边 (right),顶部(top),底部(bottom),中央(centerIn),填充 (fill),垂直中央(verticalCenter)和⽔平中央(horizontalCenter)来表⽰ 元素之间的关系。确保他们能够匹配,锚定⼀个对象的左侧顶部的⼀个元素 这样的做法是没有意义的。所以我们设置⻛⻋在⽗对象background的中央。 注意 有时你需要进⾏⼀些微⼩的调整。使⽤anchors.horizontalCenterOffset或 者anchors.verticalCenterOffset可以帮你实现这个功能。类似的调整属性 《QmlBook》In Chinese 17Qt5介绍(Qt5 Introduction) 也可以⽤于其他所有的锚。查阅Qt的帮助⽂档可以知道完整的锚属性列表。 注意 将⼀个图像作为根矩形元素的⼦元素放置展⽰了⼀种声明式语⾔的重要概 念。你描述了⽤户界⾯的层和分组的顺序,最顶部的⼀层(根矩形框)先绘 制,然后⼦层按照包含它的元素局部坐标绘制在包含它的元素上。 为了让我们的展⽰更加有趣⼀点,我们应该让程序有⼀些交互功能。当⽤户 点击场景上某个位置时,让我们的⻛⻋转动起来。 我们使⽤mouseArea元素,并且让它与我们的根元素⼤⼩⼀样。 Image { id: root ... MouseArea { anchors.fill: parent onClicked: wheel.rotation += 90 } ... } 当⽤户点击覆盖区域时,⿏标区域会发出⼀个信号。你可以重写onClicked函 数来链接这个信号。在这个案例中引⽤了⻛⻋的图像并且让他旋转增加90 度。 注意 对于每个⼯作的信号,命名⽅式都是on + SignalName的标题。当属性的值 发⽣改变时也会发出⼀个信号。它们的命名⽅式是:on + PropertyName + Chagned。 如果⼀个宽度(width)属性改变了,你可以使⽤ onWidthChanged: print(width)来得到这个监控这个新的宽度值。 现在⻛⻋将会旋转,但是还不够流畅。⻛⻋的旋转⾓度属性被直接改变了。 我们应该怎样让90度的旋转可以持续⼀段时间呢。现在是动画效果发挥作⽤ 《QmlBook》In Chinese 18Qt5介绍(Qt5 Introduction) 的时候了。⼀个动画定义了⼀个属性的在⼀段时间内的变化过程。为了实现 这个效果,我们使⽤⼀个动画类型叫做属性⾏为。这个⾏为指定了⼀个动画 来定义属性的每⼀次改变并赋值给属性。每次属性改变,动画都会运⾏。这 是QML中声明动画的⼏种⽅式中的⼀种⽅式。 Image { id: root Image { id: wheel Behavior on rotation { NumberAnimation { duration: 250 } } } } 现在每当⻛⻋旋转⾓度发⽣改变时都会使⽤NumberAnimation来实现250毫 秒的旋转动画效果。每⼀次90度的转变都需要花费250ms。 现在⻛⻋看起来好多了,我希望以上这些能够让你能够对Qt Quick编程有⼀ 些了解。 《QmlBook》In Chinese 19Qt5介绍(Qt5 Introduction) Qt5是由⼤量的模块组成的。⼀个模块通常情况下是⼀个库,提供给开发者使 ⽤。⼀些模块是强制性⽤来⽀持Qt平台的,它们分成⼀组叫做Qt基础模块。 许多模块是可选的,它们分成⼀组叫做Qt附加模块,预计⼤多数得到开发⼈ 员将不会使⽤它们,但是最好知道它们可以对⼀些通⽤的问题提供⾮常有价 值的解决⽅案。 Qt基础模块是对Qt⼀台的必要⽀持。它们使⽤Qt Quick 2开发Qt 5应⽤程序 的基础。 核⼼基础模块 以下这些是启动QML程序最⼩的模块集合。 模块名 描述 Qt Core 核⼼的⾮图形类,供其它模块使⽤。 Qt GUI 图形⽤户界⾯(GUI)组件的基类,包括OpenGL。 Qt Multimedia ⾳频,视频,电台,摄像头的功能类。 Qt Network 简化⽅便的⺴络编程的类。 Qt QML QML类与JavaScript语⾔的⽀持。 Qt Quick 可⾼度动态构建的⾃定义应⽤程序⽤户界⾯框架。 Qt SQL 集成SQL数据库类。 Qt Test Qt应⽤程序与库的单元测试类。 Qt WebKit 集成WebKit2的基础实现并且提供了新的QML应⽤程序接⼝。在附 件模块中查看Qt WebKit Widgets可以获取更多的信息。 Qt WebKit Widgets Widgets 来⾃Qt4中集成WebKit1的窗⼝基础类。 Qt构建模块(Qt Building Blocks) 1.3.1 Qt模块(Qt Modules) 《QmlBook》In Chinese 20Qt构建模块(Qt Building Blocks) Qt Widgets 扩展Qt GUI模块的C++窗⼝类。 Qt附加模块 除了必不可少的基础模块,Qt提供了附加模块供软件开发者使⽤,这部分不 ⼀定包含在发布的版本中。以下简短的列出了⼀些可⽤的附加模块列表。 Qt 3D - ⼀组使3D编程更加⽅便的应⽤程序接⼝和声明。 Qt Bluetooth - 在多平台上使⽤⽆线蓝⽛技术的C++和QML应⽤程序接 ⼝。 Qt Contacts - 提供访问联系⼈与联系⼈数据库的C++和QML应⽤程序接 ⼝。 Qt Location - 提供了定位,地图,导航和位置搜索的C++与QML接⼝。 使⽤NMEA在后端进⾏定位。(NMEA缩写,同时也是数据传输标准⼯ 业协会,在这⾥,实际上应为NMEA 0183。它是⼀套定义接收机输出的 标准信息,有⼏种不同的格式,每种都是独⽴相关的ASCII格式,逗点隔 开数据流,数据流⻓度从30-100字符不等,通常以每秒间隔选择输出, 最常⽤的格式为"GGA",它包含了定位时间,纬度,经度,⾼度,定位 所⽤的卫星数,DOP值,差分状态和校正时段等,其他的有速度,跟踪, 《QmlBook》In Chinese 21Qt构建模块(Qt Building Blocks) ⽇期等。NMEA实际上已成为所有的GPS接收机和最通⽤的数据输出格 式,同时它也被⽤于与GPS接收机接⼝的⼤多数的软件包⾥。) Qt Organizer - 提供了组织事件(任务清单,事件等等)的C++和QML应 ⽤程序接⼝。 Qt Publish and SubScribe - Qt发布与订阅 Qt Sensors - 访问传感器的QML与C++接⼝。 Qt Service Framework - 允许应⽤程序读取,操纵和订阅来改变通知信 息。 Qt System Info - 发布系统相关的信息和功能。 Qt Versit - ⽀持电⼦名⽚与⽇历数据格式(iCalendar)。(iCalendar 是“⽇历数据交换”的标准(RFC 2445)。 此标准有时指的是“iCal”,即 苹果公司的出品的⼀款同名⽇历软件,这个软件也是此标准的⼀种实现 ⽅式。) Qt Wayland - 只⽤于Linux系统。包含了Qt合成器应⽤程序接⼝ (server),和Wayland平台插件(clients)。 Qt Feedback - 反馈⽤户的触摸和声⾳操作。 Qt JSON DB - 对于Qt的⼀个不使⽤SQL对象存储。 注意 这些模块⼀部分还没有发布,这依赖于有多少贡献者,并且它们能够获得更 好的测试。 Qt⽀持各种不同的平台。⼤多数主流的桌⾯与嵌⼊式平台都能够⽀持。通过 1.3.2 ⽀持的平台(Supported Platforms) 《QmlBook》In Chinese 22Qt构建模块(Qt Building Blocks) Qt应⽤程序抽象,现在可以更容易的将Qt移植到你⾃⼰的平台上。在⼀个平 台上测试Qt5是⾮常花费时间的。选择测试的平台⼦集可以参考qt-project构 件的平台设置。这些平台需要完全通过系统的测试才能确保最好的质量。友 情提醒:任何代码都可能会有Bug的。 《QmlBook》In Chinese 23Qt构建模块(Qt Building Blocks) 来⾃qt-project百科:Qt-Project是由Qt社区上对Qt感兴趣的⼈达成共识的地 ⽅。任何⼈都可以在社区上分享它感兴趣的东⻄,参与它的开发,并且向Qt 的开发做出贡献。 Qt-Project是⼀个为Qt未来开发开源部分的组织。它基于使⽤者的贡献。最⼤ 的贡献者是DIGIA,它可以提供Qt的商业授权。 Qt对于公司分为开源⽅向和 商业⽅向。商业⽅向的公司不需要遵守开源协议。没有商业⽅向的许可的公 司不能使⽤Qt,并且它也不允许DIGIA向Qt项⺫贡献太多的代码。 在全球有很多公司,他们在不同的平台上使⽤Qt开发产品,提供咨询。同样 也有很多开源项⺫和开源开发者,它们使⽤Qt作为它们的开发库。成为这样 开发活泼的社区的⼀部分,并且使⽤这个很棒的⼯具盒库让⼈感觉很好。它 能让你成为⼀个更好的⼈吗?也许:-)。 Qt项⺫(Qt Project) 《QmlBook》In Chinese 24Qt项⽬(Qt Project) 这⼀章介绍了如何使⽤Qt5进⾏开发。我们将告诉你如何安装Qt软件开发⼯ 具包(Qt SDK)和如何使⽤Qt Creator集成开发环境(Qt Creator IDE)创 建并运⾏⼀个简单的hello word应⽤程序。 注意 这章的源代码能够在assetts folder找到。 开始学习(Get Start) 《QmlBook》In Chinese 25开始学习(Get Start) Qt软件⼯具包包含了编译桌⾯或者嵌⼊式应⽤程序的⼯具。最新的版本可以 从Qt-Project下载。我们将使⽤这种⽅法开始。 软件⼯具包⾃⾝包含了⼀个维护⼯具允许你更新到最新版本的软件⼯具包。 Qt软件⼯具包⾮常容易安装,并且附带了⼀个它⾃⾝的快速集成开发环境叫 做Qt Creator。这个集成开发环境可以让你⾼效的使⽤Qt进⾏开发,我们推 荐给所有的读者使⽤。在任何情况下Qt都可以通过命令的⽅式来编译,你可 以⾃由的选择你的代码编辑器。 当你安装软件⼯具包时,你最好选择默认的选项确保Qt 5.x可以被使⽤。然 后⼀切准备就绪。 安装Qt5软件⼯具包(Installing Qt 5 SDK) 《QmlBook》In Chinese 26安装Qt5软件⼯具包(Installing Qt 5 SDK) 为了测试你的安装,我们创建⼀个简单的应⽤程序hello world.打开Qt Creator并且创建⼀个Qt Quick UI Project(File->New File 或者 Project-> Qt Quick Project -> Qt Quick UI)并且给项⺫取名 HelloWorld。 注意 Qt Creator集成开发环境允许你创建不同类型的应⽤程序。如果没有另外说 明,我们都创建Qt Quick UI Project。 提⽰ ⼀个典型的Qt Quick应⽤程序在运⾏时解释,与本地插件或者本地代码在运 ⾏时解释代码⼀样。对于才开始的我们不需要关注本地端的解释开发,只需 要把注意⼒集中在Qt5运⾏时的⽅⾯。 Qt Creator将会为我们创建⼏个⽂件。HellWorld.qmlproject⽂件是项⺫⽂ 件,保存了项⺫的配置信息。这个⽂件由Qt Creator管理,我们不需要编辑 它。 另⼀个⽂件HelloWorld.qml保存我们应⽤程序的代码。打开它,并且尝试想 想这个应⽤程序要做什么,然后再继续读下去。 // HelloWorld.qml import QtQuick 2.0 Rectangle { width: 360 height: 360 Text { anchors.centerIn: parent text: "Hello World" } 你好世界(Hello World) 《QmlBook》In Chinese 27你好世界(Hello World) MouseArea { anchors.fill: parent onClicked: { Qt.quit(); } } } HelloWorld.qml使⽤QML语⾔来编写。我们将在下⼀章更深⼊的讨论QML语 ⾔,现在只需要知道它描述了⼀系列有层次的⽤户界⾯。这个代码指定了显 ⽰⼀个360乘以360像素的⼀个矩形,矩形中间有⼀个“Hello World"的⽂本。 ⿏标区域覆盖了整个矩形,当⽤户点击它时,程序就会退出。 你⾃⼰可以运⾏这个应⽤程序,点击左边的运⾏或者从菜单选择select Bulid- >Run。 如果⼀切顺利,你将看到下⾯这个窗⼝: 《QmlBook》In Chinese 28你好世界(Hello World) Qt 5似乎已经可以⼯作了,我们接着继续。 建议 如果你是⼀个名系统集成⼈员,你会想要安装最新稳定的Qt版本,将这个Qt 版本的源代码编译到你特定的⺫标机器上。 从头开始构建 如果你想使⽤命令⾏的⽅式构建Qt5,你⾸先需要拷⻉⼀个代码库并构建他。 git clone git://gitorious.org/qt/qt5.git 《QmlBook》In Chinese 29你好世界(Hello World) cd qt5 ./init-repository ./configure -prefix $PWD/qtbase -opensource make -j4 等待两杯咖啡的时间编译完成后,qtbase⽂件夹中将会出现可以使⽤的Qt5。 任何饮料都好,不过我喜欢喝着咖啡等待最好的结果。 如果你想测试你的编译,只需简单的启动qtbase/bin/qmlscene并且选择⼀个 QtQuick的例⼦运⾏,或者跟着我们进⼊下⼀章。 为了测试你的安装,我们创建了⼀个简单的hello world应⽤程序。创建⼀个 空的qml⽂件example1.qml,使⽤你最喜爱的⽂本编辑器并且粘贴⼀下内 容: // HelloWorld.qml import QtQuick 2.0 Rectangle { width: 360 height: 360 Text { anchors.centerIn: parent text: "Greetings from Qt5" } MouseArea { anchors.fill: parent onClicked: { Qt.quit(); } } } 你现在使⽤来⾃Qt5的默认运⾏环境来可以运⾏这个例⼦: $ qtbase/bin/qmlscene 《QmlBook》In Chinese 30你好世界(Hello World) 《QmlBook》In Chinese 31你好世界(Hello World) 这⼀节贯穿了可能使⽤Qt5编写的不同类型的应⽤程序。没有任何建议的选 择,只是想告诉读者Qt5通常情况下能做些什么。 ⼀个控制台应⽤程序不需要提供任何⼈机交互图形界⾯通常被称作系统服 务,或者通过命令⾏来运⾏。Qt5附带了⼀系列现成的组件来帮助你⾮常有效 的创建跨平台的控制台应⽤程序。例如⺴络应⽤程序编程接⼝或者⽂件应⽤ 程序编程接⼝,字符串的处理,⾃Qt5.1发布的⾼效的命令解析器。由于Qt是 基于C++的⾼级应⽤程序接⼝,你能够快速的编程并且程序拥有快速的执⾏ 速度。不要认为Qt仅仅只是⽤户界⾯⼯具,它也提供了许多其它的功能。 字符串处理 在第⼀个例⼦中我们展⽰了怎样简单的增加两个字符串常量。这不是⼀个有 ⽤的应⽤程序,但能让你了解本地端C++应⽤程序没有事件循环时是什么样 的。 // module or class includes #include // text stream is text-codec aware QTextStream cout(stdout, QIODevice::WriteOnly); int main(int argc, char** argv) { // avoid compiler warnings Q_UNUSED(argc) Q_UNUSED(argv) QString s1("Paris"); QString s2("London"); // string concatenation 应⽤程序类型(Application Types) 2.3.1 控制台应⽤程序 《QmlBook》In Chinese 32应⽤程序类型(Application Types) QString s = s1 + " " + s2 + "!"; cout << s << endl; } 容器类 这个例⼦在应⽤程序中增加了⼀个链表和⼀个链表迭代器。Qt⾃带⼤量⽅便 使⽤的容器类,并且其中的元素使⽤相同的应⽤程序接⼝模式。 QString s1("Hello"); QString s2("Qt"); QList list; // stream into containers list << s1 << s2; // Java and STL like iterators QListIterator iter(list); while(iter.hasNext()) { cout << iter.next(); if(iter.hasNext()) { cout << " "; } } cout << "!" << endl; 这⾥我们展⽰了⼀些⾼级的链表函数,允许你在⼀个字符串中加⼊⼀个链表 的字符串。当你需要持续的⽂本输⼊时⾮常的⽅便。使⽤QString::split()函数 可以将这个操作逆向(将字符串转换为字符串链表)。 QString s1("Hello"); QString s2("Qt"); // convenient container classes QStringList list; list << s1 << s2; // join strings QString s = list.join(" ") + "!"; cout << s << endl; 《QmlBook》In Chinese 33应⽤程序类型(Application Types) ⽂件IO 下⼀个代码⽚段我们从本地读取了⼀个CSV⽂件并且遍历提取每⼀⾏的每⼀ 个单元的数据。我们从CSV⽂件中获取⼤约20⾏的编码。⽂件读取仅仅给了 我们⼀个⽐特流,为了有效的将它转换为可以使⽤的Unicode⽂本,我们需 要使⽤这个⽂件作为⽂本流的底层流数据。编写CSV⽂件,你只需要以写⼊ 的⽅式打开⼀个⽂件并且⼀⾏⼀⾏的输⼊到⽂件流中。 QList data; // file operations QFile file("sample.csv"); if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); // loop forever macro forever { QString line = stream.readLine(); // test for empty string 'QString("")' if(line.isEmpty()) { continue; } // test for null string 'String()' if(line.isNull()) { break; } QStringList row; // for each loop to iterate over containers foreach(const QString& cell, line.split(",")) { row.append(cell.trimmed()); } data.append(row); } } // No cleanup necessary. 现在我们结束Qt关于基于控制台应⽤程序⼩节。 2.3.2 窗⼝应⽤程序 《QmlBook》In Chinese 34应⽤程序类型(Application Types) 基于控制台的应⽤程序⾮常⽅便,但是有时候你需要有⼀些⽤户界⾯。但是 基于⽤户界⾯的应⽤程序需要后端来写⼊/读取⽂件,使⽤⺴络进⾏通讯或者 保存数据到⼀个容器中。 在第⼀个基于窗⼝的应⽤程序代码⽚段,我们仅仅只创建了⼀个窗⼝并显⽰ 它。没有⽗对象的窗⼝部件是Qt世界中的⼀个窗⼝。我们使⽤智能指针来确 保当智能指针指向范围外时窗⼝会被删除掉。 这个应⽤程序对象封装了Qt的运⾏,调⽤exec开始我们的事件循环。从这⾥ 开始我们的应⽤程序只响应由⿏标或者键盘或者其它的例如⺴络或者⽂件IO 的事件触发。应⽤程序也只有在事件循环退出时才退出,在应⽤程序中调 ⽤"quit()"或者关掉窗⼝来退出。 当你运⾏这段代码的时候你可以看到⼀个 240乘以120像素的窗⼝。 #include int main(int argc, char** argv) { QApplication app(argc, argv); QScopedPointer widget(new CustomWidget()); widget->resize(240, 120); widget->show(); return app.exec(); } ⾃定义窗⼝部件 当你使⽤⽤户界⾯时你需要创建⼀个⾃定义的窗⼝部件。典型的窗⼝是⼀个 窗⼝部件区域的绘制调⽤。附加⼀些窗⼝部件内部如何处理外部触发的键盘 或者⿏标输⼊。为此我们需要继承QWidget并且重写⼏个函数来绘制和处理 事件。 #ifndef CUSTOMWIDGET_H #define CUSTOMWIDGET_H 《QmlBook》In Chinese 35应⽤程序类型(Application Types) #include class CustomWidget : public QWidget { Q_OBJECT public: explicit CustomWidget(QWidget *parent = 0); void paintEvent(QPaintEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); private: QPoint m_lastPos; }; #endif // CUSTOMWIDGET_H 在实现中我们绘制了窗⼝的边界并在⿏标最后的位置上绘制了⼀个⼩的矩形 框。这是⼀个⾮常典型的低层次的⾃定义窗⼝部件。⿏标或者键盘事件会改 变窗⼝的内部状态并触发重新绘制。我们不需要更加详细的分析这个代码, 你应该有能⼒分析它。Qt⾃带了⼤量现成的桌⾯窗⼝部件,你有很⼤的⼏率 不需要再做这些⼯作。 #include "customwidget.h" CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { } void CustomWidget::paintEvent(QPaintEvent *) { QPainter painter(this); QRect r1 = rect().adjusted(10,10,-10,-10); painter.setPen(QColor("#33B5E5")); painter.drawRect(r1); QRect r2(QPoint(0,0),QSize(40,40)); if(m_lastPos.isNull()) { r2.moveCenter(r1.center()); 《QmlBook》In Chinese 36应⽤程序类型(Application Types) } else { r2.moveCenter(m_lastPos); } painter.fillRect(r2, QColor("#FFBB33")); } void CustomWidget::mousePressEvent(QMouseEvent *event) { m_lastPos = event->pos(); update(); } void CustomWidget::mouseMoveEvent(QMouseEvent *event) { m_lastPos = event->pos(); update(); } 桌⾯窗⼝ Qt的开发者们已经为你做好⼤量现成的桌⾯窗⼝部件,在不同的操作系统中 他们看起来都像是本地的窗⼝部件。你的⼯作只需要在⼀个打的窗⼝容器中 安排不同的的窗⼝部件。在Qt中⼀个窗⼝部件能够包含其它的窗⼝部件。这 个操作由分配⽗⼦关系来完成。这意味着我们需要准备类似按钮 (button),复选框(check box),单选按钮(radio button)的窗⼝部件并 且对它们进⾏布局。下⾯展⽰了⼀种完成的⽅法。 这⾥有⼀个头⽂件就是所谓的窗⼝部件容器。 class CustomWidget : public QWidget { Q_OBJECT public: explicit CustomWidgetQWidget *parent = 0); private slots: void itemClicked(QListWidgetItem* item); void updateItem(); private: 《QmlBook》In Chinese 37应⽤程序类型(Application Types) QListWidget *m_widget; QLineEdit *m_edit; QPushButton *m_button; }; 在实现中我们使⽤布局来更好的安排我们的窗⼝部件。当容器窗⼝部件⼤⼩ 被改变后它会按照窗⼝部件的⼤⼩策略进⾏重新布局。在这个例⼦中我们有 ⼀个链表窗⼝部件,⾏编辑器与按钮垂直排列来编辑⼀个城市的链表。我们 使⽤Qt的信号与槽来连接发送和接收对象。 CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *layout = new QVBoxLayout(this); m_widget = new QListWidget(this); layout->addWidget(m_widget); m_edit = new QLineEdit(this); layout->addWidget(m_edit); m_button = new QPushButton("Quit", this); layout->addWidget(m_button); setLayout(layout); QStringList cities; cities << "Paris" << "London" << "Munich"; foreach(const QString& city, cities) { m_widget->addItem(city); } connect(m_widget, SIGNAL(itemClicked(QListWidgetItem*)), this, SLOT(itemClicked(QListWidgetItem*))); connect(m_edit, SIGNAL(editingFinished()), this, SLOT(updateItem())); connect(m_button, SIGNAL(clicked()), qApp, SLOT(quit())); } void CustomWidget::itemClicked(QListWidgetItem *item) { Q_ASSERT(item); m_edit->setText(item->text()); } 《QmlBook》In Chinese 38应⽤程序类型(Application Types) void CustomWidget::updateItem() { QListWidgetItem* item = m_widget->currentItem(); if(item) { item->setText(m_edit->text()); } } 绘制图形 有⼀些问题最好⽤可视化的⽅式表达。如果⼿边的问题看起来有点像⼏何对 象,qt graphics view是⼀个很好的选择。⼀个图形视窗(graphics view)能 够在⼀个场景(scene)排列简单的⼏何图形。⽤户可以与这些图形交互, 它们使⽤⼀定的算法放置在场景(scene)上。填充⼀个图形视图你需要⼀ 个图形窗⼝(graphics view)和⼀个图形场景(graphics scene)。⼀个图 形场景(scene)连接在⼀个图形窗⼝(view)上,图形对象(graphics item)是被放在图形场景(scene)上的。这⾥有⼀个简单的例⼦,⾸先头⽂ 件定义了图形窗⼝(view)与图形场景(scene)。 class CustomWidgetV2 : public QWidget { Q_OBJECT public: explicit CustomWidgetV2(QWidget *parent = 0); private: QGraphicsView *m_view; QGraphicsScene *m_scene; }; 在实现中⾸先将图形场景(scene)与图形窗⼝(view)连接。图形窗⼝ (view)是⼀个窗⼝部件,能够被我们的窗⼝部件容器包含。最后我们添加 ⼀个⼩的矩形框在图形场景(scene)中。然后它会被渲染到我们的图形窗 ⼝(view)上。 《QmlBook》In Chinese 39应⽤程序类型(Application Types) #include "customwidgetv2.h" CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { m_view = new QGraphicsView(this); m_scene = new QGraphicsScene(this); m_view->setScene(m_scene); QVBoxLayout *layout = new QVBoxLayout(this); layout->setMargin(0); layout->addWidget(m_view); setLayout(layout); QGraphicsItem* rect1 = m_scene->addRect(0,0, 40, 40, Qt::NoPen, QColor("#FFBB33")); rect1->setFlags(QGraphicsItem::ItemIsFocusable|QGraphicsItem::ItemIsMovable); } 到现在我们已经知道了⼤多数的基本数据类型,并且知道如何使⽤窗⼝部件 和图形视图(graphics views)。通常在应⽤程序中你需要处理⼤量的结构体 数据,也可能需要不停的储存它们,或者这些数据需要被⽤来显⽰。对于这 些Qt使⽤了模型的概念。下⾯⼀个简单的模型是字符串链表模型,它被⼀⼤ 堆字符串填满然后与⼀个链表视图(list view)连接。 m_view = new QListView(this); m_model = new QStringListModel(this); view->setModel(m_model); QList cities; cities << "Munich" << "Paris" << "London"; model->setStringList(cities); 另⼀个⽐较普遍的⽤法是使⽤SQL(结构化数据查询语⾔)来存储和读取数 2.3.3 数据适配 《QmlBook》In Chinese 40应⽤程序类型(Application Types) 据。Qt⾃⾝附带了嵌⼊式版的SQLLite并且也⽀持其它的数据引擎(⽐如 MySQL,PostgresSQL,等等)。⾸先你需要使⽤⼀个模式来创建你的数据 库,⽐如像这样: CREATE TABLE city (name TEXT, country TEXT); INSERT INTO city value ("Munich", "Germany"); INSERT INTO city value ("Paris", "France"); INSERT INTO city value ("London", "United Kingdom"); 为了能够在使⽤sql,我们需要在我们的项⺫⽂件(*.pro)中加⼊sql模块。 QT += sql 然后我们需要c++来打开我们的数据库。⾸先我们需要获取⼀个指定的数据 库引擎的数据对象。使⽤这个数据库对象我们可以打开数据库。对于SQLLite 这样的数据库我们可以指定⼀个数据库⽂件的路径。Qt提供了⼀些⾼级的数 据库模型,其中有⼀种表格模型(table model)使⽤表格标⽰符和⼀个选项 分⽀语句(where clause)来选择数据。这个模型的结果能够与⼀个链表视 图连接,就像之前连接其它数据模型⼀样。 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); db.setDatabaseName('cities.db'); if(!db.open()) { qFatal("unable to open database"); } m_model = QSqlTableModel(this); m_model->setTable("city"); m_model->setHeaderData(0, Qt::Horizontal, "City"); m_model->setHeaderData(1, Qt::Horizontal, "Country"); view->setModel(m_model); m_model->select(); 《QmlBook》In Chinese 41应⽤程序类型(Application Types) 对⾼级的模型操作,Qt提供了⼀种分类⽂件代理模型,允许你使⽤基础的分 类排序和数据过滤来操作其它的模型。 QSortFilterProxyModel* proxy = new QSortFilterProxyModel(this); proxy->setSourceModel(m_model); view->setModel(proxy); view->setSortingEnabled(true); 数据过滤基于列号与⼀个字符串参数完成。 proxy->setFilterKeyColumn(0); proxy->setFilterCaseSensitive(Qt::CaseInsensitive); proxy->setFilterFixedString(QString) 过滤代理模型⽐这⾥演⽰的要强⼤的多,现在我们只需要知道有它的存在就 够了。 注意 这⾥是综述了你可以在Qt5中开发的不同类型的经典应⽤程序。桌⾯应⽤程 序正在发⽣着改变,不久之后移动设备将会为占据我们的世界。移动设备的 ⽤户界⾯设计⾮常不同。它们相对于桌⾯应⽤程序更加简洁,只需要专注的 做⼀件事情。动画效果是⼀个⾮常重要的部分,⽤户界⾯需要⽣动活泼。传 统的Qt技术已经不适于这些市场了。 接下来:Qt Quick将会解决这个问题。 在现代的软件开发中有⼀个内在的冲突,⽤户界⾯的改变速度远远⾼于我们 的后端服务。在传统的技术中我们开发的前端需要与后端保持相同的步调。 当⼀个项⺫在开发时⽤户想要改变⽤户界⾯,或者在⼀个项⺫中开发⼀个⽤ 2.3.4 Qt Quick应⽤程序 《QmlBook》In Chinese 42应⽤程序类型(Application Types) 户界⾯的想法就会引发这个冲突。敏捷项⺫需要敏捷的⽅法。 Qt Quick 提供了⼀个类似HTML声明语⾔的环境应⽤程序作为你的⽤户界⾯ 前端(the front-end),在你的后端使⽤本地的c++代码。这样允许你在两端 都游刃有余。 下⾯是⼀个简单的Qt Quick UI的例⼦。 import QtQuick 2.0 Rectangle { width: 240; height: 1230 Rectangle { width: 40; height: 40 anchors.centerIn: parent color: '#FFBB33' } } 这种声明语⾔被称作QML,它需要在运⾏时启动。Qt提供了⼀个典型的运⾏ 环境叫做qmlscene,但是想要写⼀个⾃定义的允许环境也不是很困难,我们 需要⼀个快速视图(quick view)并且将QML⽂档作为它的资源。剩下的事 情就只是展⽰我们的⽤户界⾯了。 QQuickView* view = new QQuickView(); QUrl source = Qurl::fromLocalUrl("main.qml"); view->setSource(source); view.show(); 回到我们之前的例⼦,在⼀个例⼦中我们使⽤了⼀个c++的城市数据模型。 如果我们能够在QML代码中使⽤它将会更加的好。 为了实现它我们⾸先要编写前端代码怎样展⽰我们需要使⽤的城市数据模 型。在这⼀个例⼦中前端指定了⼀个对象叫做cityModel,我们可以在链表视 图(list view)中使⽤它。 《QmlBook》In Chinese 43应⽤程序类型(Application Types) import QtQuick 2.0 Rectangle { width: 240; height: 120 ListView { width: 180; height: 120 anchors.centerIn: parent model: cityModel delegate: Text { text: model.city } } } 为了使⽤cityModel,我们通常需要重复使⽤我们以前的数据模型,给我们的 根环境(root context)加上⼀个内容属性(context property)。(root context是在另⼀个⽂档的根元素中)。 m_model = QSqlTableModel(this); ... // some magic code QHash roles; roles[Qt::UserRole+1] = "city"; roles[Qt::UserRole+2] = "country"; m_model->setRoleNames(roles); view->rootContext()->setContextProperty("cityModel", m_model); 警告 这不是完全正确的⽤法,作为包含在SQL表格模型列中的数据,⼀个QML模 型的任务是来表达这些数据。所以需要做⼀个在列和任务之间的映射关系。 请查看来QML and QSqlTableModel获得更多的信息。 《QmlBook》In Chinese 44应⽤程序类型(Application Types) 我们已经知道了如何安装Qt软件开发⼯具包,并且知道如何创建我们的应 ⽤。我们向你展⽰和概述了使⽤Qt开发不同类型的应⽤程序。展⽰Qt可以给 你的应⽤程序开发提供的⼀些功能。我希望你对Qt留下⼀个好的印象,Qt是 ⼀个⾮常好的⽤户界⾯开发⼯具并且尽可能的提供了⼀个应⽤开发者期望的 东⻄。当前你也不必⼀直锁定使⽤Qt,你也可以使⽤其它的库或者⾃⼰来扩 展Qt。Qt对于不同类型的应⽤程序开发⽀持⾮常丰富:包括控制台程序,经 典的桌⾯⽤户界⾯程序和触摸式⽤户界⾯程序。 总结( Summary) 《QmlBook》In Chinese 45总结( Summary) Qt Creator是Qt默认的集成开发环境。它由Qt的开发者们编写提供的。这个 集成开发环境能够在⼤多数的桌⾯开发平台上使⽤,例如 Windows/Mac/Linux。我们也已经看到有些⽤户在嵌⼊式设备上使⽤Qt Creator。Qt Creator有着精简的⽤户界⾯,可以帮助开发者们⾼效的完成开 发⽣产。Qt Creator 能够启动你的QtQuick⽤户界⾯,也可以⽤来编译c++代 码到你的主机系统或者使⽤交叉编译到你的设备系统上。 注意 这章的源代码能够在assetts folder找到。 Qt Creator集成开发环境(Qt Creator IDE) 《QmlBook》In Chinese 46Qt Creator集成开发环境(Qt Creator IDE) 当你启动Qt Creator时,你可以看到⼀个欢迎画⾯。在这⾥你可以找到怎样 在Qt Creator中继续的重要提⽰,或者你最近使⽤的项⺫。你可以看到⼀个 会话列表,你可以看到是⼀个空的。⼀个会话是供你参考使⽤的⼀堆项⺫的 集合。当你在同时拥有⼏个客户的⼤项⺫时,这个功能⾮常有⽤。 你可以在左边看到模式选择。模式选择包含了你典型的⼯作步骤。 欢迎模式:你⺫前所在的位置。 编辑模式:专注于编码。 设计模式:专注于⽤户界⾯设计。 调试模式:获取当前运⾏程序的相关信息。 项⺫模式:修改你的项⺫编译运⾏配置。 分析模式:检查内存泄露并剖析。 帮助模式:阅读Qt的帮助⽂档。 在模式选择下⾯你可以找到项⺫配置选择与执⾏/调试。 ⽤户界⾯(The User Interface) 《QmlBook》In Chinese 47⽤户界⾯(The User Interface) 你应该⼤多数时间都处于编辑模式的中央⾯板中的代码编辑器编辑你的代 码。当你需要配置你的项⺫时,你将不时的访问项⺫模式。当你点击 Run(运⾏)。Qt Creator会先确保充分的构建你的项⺫后再运⾏它。 在最下⾯的输出窗是错误信息,应⽤程序信息,编译信息和其它的信息。 《QmlBook》In Chinese 48⽤户界⾯(The User Interface) 最开始使⽤Qt Creator时最困难的部分可能是Qt Kit。⼀个Qt Kit由Qt的版 本,编译系统和设备等等其它设置来配置它。它使⽤唯⼀标识的⼯具组合来 构建你的项⺫。⼀个典型的桌⾯kit(⼯具箱)可能包含⼀个GCC编译程序, ⼀个Qt版本库(⽐如Qt5.1.1)和⼀个设备(”桌⾯“)。在你创建好你的项⺫ 后你需要为项⺫指定⼀个kit(⼯具箱)来构建项⺫。在你创建⼀个kit(⼯具 箱)之前你需要先安装⼀个编译程序并注册⼀个Qt版本。Qt版本的注册由指 定qmake的执⾏路径完成。Qt Creator通过查询qmake的信息来获取Qt的版 本标识。 添加kit(⼯具箱)与注册Qt版本在Settings->Bulild & Run entry中完成,在这 ⾥你也可以查看有哪些编译程序已经被注册了的。 注意 请⾸先确保你的Qt Creator中已经注册了正确的Qt版本,并且确保⼀个 Kit(⼯具箱)指定了⼀个编译程序与Qt版本和设备的组合。你⽆法离开 Kit(⼯具箱)来构建⼀个项⺫。 注册你的Qt⼯具箱(Registering your Qt Kit) 《QmlBook》In Chinese 49注册你的Qt⼯具箱(Registering your Qt Kit) Qt Creator在项⺫中管理你的源代码。你可以使⽤File->New File或者Project 来创建⼀个新项⺫。当你创建⼀个项⺫时,你可以选择多种应⽤程序模板。 Qt Creator 能够创建桌⾯,⼿机应⽤程序。这些应⽤程序使⽤窗⼝部件 (Widgets)或者QtQuick或者控制台,甚⾄可以是更加简单的项⺫。当然也 ⽀持HTML5与python的项⺫。对于⼀个新⼿是很难选择的,所以我们为你选 择了三种类型的项⺫。 应⽤程序/QtQuick2.0⽤户界⾯:这将会为你创建⼀个QML/JS的项⺫, 不需要使⽤任何的C++代码。使⽤这个你可以迅速的创建⼀个新的⽤户 界⾯或者计划创建⼀个基于本地插件的现代的⽤户界⾯应⽤程序。 库/Qt Quick2.0扩展插件:使⽤这个安装引导能够创建⼀个你⾃⼰的Qt Quick⽤户界⾯插件。这个插件被⽤来扩展Qt Quick的本地元素。 其它项⺫/空的Qt项⺫:只是⼀个项⺫的⾻架。如果你想从头使⽤C++来 编写你的应⽤程序,你可以使⽤这种⽅式。你需要知道你在这⾥能做什 么。 注意 在这本书的前⾯部分我们主要使⽤QtQuick 2.0⽤户界⾯项⺫。在后⾯我们会 使⽤空的Qt项⺫或者类似的项⺫描述⼀些C++⽅⾯的使⽤。为了使⽤我们⾃ ⼰的本地插件来扩展QtQuick,我们将会使⽤Qt Quick2.0扩展插件安装引导 项⺫。 项⺫管理(Managing Projects) 《QmlBook》In Chinese 50项⽬管理(Managing Projects) 当你打开⼀个项⺫或者创建⼀个新的项⺫后,Qt Creator将会转换到编辑模 式下。你应该可以在左边看到你的项⺫⽂件,在中央区域看到代码编辑器。 左边选中的⽂件将会被编辑器打开。编辑器提供了语法⾼亮,代码补全和智 能纠错的功能。也提供⼏种代码重构的命令。当你使⽤这个编辑器⼯作时你 会觉得它的响应⾮常的迅速。这感谢与Qt Creaotor的开发者将这个⼯具做的 如此杰出。 使⽤编辑器(Using the Editor) 《QmlBook》In Chinese 51使⽤编辑器(Using the Editor) 定位器是Qt Creaotor中⼼的⼀个组件。它可以让开发者迅速的找到指定代码 的位置,或者获得帮助。使⽤Ctrl+K来打开定位器。 左边底部可以显⽰弹出⼀系列的选项。如果你只是想搜索你项⺫中的⼀个⽂ 件,你只需要给出⽂件第⼀个字⺟提⽰就可以了。定位器也接收通配符,⽐ 如*main.qml也可以查找。你也可以通过前缀搜索来搜索指定内容的类型。 试试它,例如寻找⼀个QML矩形框的帮助,输⼊?rectangle。定位器会不停 定位器(Locator) 《QmlBook》In Chinese 52定位器(Locator) 的更新它的建议直到你找到你想要的参考⽂档。 《QmlBook》In Chinese 53定位器(Locator) Qt Creator⽀持C++与QML代码调试。 注意 嗯,我才意识到我还没有使⽤过调试。这是⼀个好的现象。我需要有⼈对此 提出问题,查看Qt Creator documentation来获得更多的帮助吧。 调试(Debugging) 《QmlBook》In Chinese 54调试(Debugging) 在好使⽤的系统中和专业系统中,快捷键是不同的。作为专业的开发⼈员, 你也许会在你的应⽤程序上花很多时间,每⼀个快捷键都能使你的⼯作效率 得到提⾼。Qt Creator的开发者也这样想,并且在应⽤程序中加⼊了许许多 多的快捷键。 我们列出了⼀些基本的快捷键操作: Ctrl+B - 构建项⺫ Ctrl+R - 运⾏项⺫ Ctrl+Tab - 切换已打开的⽂档 Ctrl+k - 打开定位器 Esc - 返回 F2 - 查找对应的符号解释。 F4 - 在头⽂件与源⽂件之间切换(只对c++代码有效) 这些快捷键的定义来⾃Qt Creator shortcuts这个⽂档。 注意 你可以使⽤设置窗⼝来编辑你的快捷键。 快捷键(Shortcuts) 《QmlBook》In Chinese 55快捷键(Shortcuts) 《QmlBook》In Chinese 56快捷键(Shortcuts) 注意 最后⼀次构建:2014年1⽉20⽇下午18:00。 这章的源代码能够在assetts folder找到。 这章概述了QML语⾔,Qt5中⼤量使⽤了这种声明⽤户界⾯的语⾔。我们将 会讨论QML语⾔,⼀个树形结构的元素,跟着是⼀些最基本的元素概述。然 后我们会简短的介绍怎样创建我们⾃⼰的元素,这些元素被叫做组件,并如 何使⽤属性操作来转换元素。最后我们会介绍如何对元素进⾏布局,如何向 ⽤户提供输⼊。 QML快速⼊⻔(Quick Starter) 《QmlBook》In Chinese 57QML快速⼊门(Quick Starter) QML是⼀种描述⽤户界⾯的声明式语⾔。它将⽤户界⾯分解成⼀些更⼩的元 素,这些元素能够结合成⼀个组件。QML语⾔描述了⽤户界⾯元素的形状和 ⾏为。⽤户界⾯能够使⽤JavaScript来提供修饰,或者增加更加复杂的逻 辑。从这个⾓度来看它遵循HTML-JavaScript模式,但QML是被设计⽤来描 述⽤户界⾯的,⽽不是⽂本⽂档。 从QML元素的层次结构来理解是最简单的学习⽅式。⼦元素从⽗元素上继承 了坐标系统,它的x,y坐标总是相对应于它的⽗元素坐标系统。 让我们开始⽤⼀个简单的QML⽂件例⼦来解释这个语法。 // rectangle.qml import QtQuick 2.0 // The root element is the Rectangle QML语法(QML Syntax) 《QmlBook》In Chinese 58QML语法(QML Syntax) Rectangle { // name this element root id: root // properties: : width: 120; height: 240 // color property color: "#D8D8D8" // Declare a nested element (child of root) Image { id: rocket // reference the parent x: (parent.width - width)/2; y: 40 source: 'assets/rocket.png' } // Another child of root Text { // un-named element // reference element by id y: rocket.y + rocket.height + 20 // reference root element width: root.width horizontalAlignment: Text.AlignHCenter text: 'Rocket' } } import声明导⼊了⼀个指定的模块版本。⼀般来说会导⼊QtQuick2.0来 作为初始元素的引⽤。 使⽤//可以单⾏注释,使⽤/**/可以多⾏注释,就像C/C++和JavaScript⼀ 样。 《QmlBook》In Chinese 59QML语法(QML Syntax) 每⼀个QML⽂件都需要⼀个根元素,就像HTML⼀样。 ⼀个元素使⽤它的类型声明,然后使⽤{}进⾏包含。 元素拥有属性,他们按照name:value的格式来赋值。 任何在QML⽂档中的元素都可以使⽤它们的id进⾏访问(id是⼀个任意 的标识符)。 元素可以嵌套,这意味着⼀个⽗元素可以拥有多个⼦元素。⼦元素可以 通过访问parent关键字来访问它们的⽗元素。 建议 你会经常使⽤id或者关键字parent来访问你的⽗对象。有⼀个⽐较好的⽅法 是命名你的根元素对象id为root(id:root),这样就不⽤去思考你的QML⽂ 档中的根元素应该⽤什么⽅式命名了。 提⽰ 你可以在你的操作系统命令⾏模式下使⽤QtQuick运⾏环境来运⾏这个例 ⼦,⽐如像下⾯这样: $ $QTDIR/bin/qmlscene rectangle.qml 将$QTDIR替换为你的Qt的安装路径。qmlscene会执⾏Qt Quick运⾏环境初 始化,并且解释这个QML⽂件。 在Qt Creator中你可以打开对应的项⺫⽂件然后运⾏rectangle.qml⽂档。 元素使⽤他们的元素类型名进⾏声明,使⽤它们的属性或者创建⾃定义属性 来定义。⼀个属性对应⼀个值,例如 width:100,text: 'Greeting', color: 4.1.1 属性(Properties) 《QmlBook》In Chinese 60QML语法(QML Syntax) '#FF0000'。⼀个属性有⼀个类型定义并且需要⼀个初始值。 Text { // (1) identifier id: thisLabel // (2) set x- and y-position x: 24; y: 16 // (3) bind height to 2 * width height: 2 * width // (4) custom property property int times: 24 // (5) property alias property alias anotherTimes: thisLabel.times // (6) set text appended by value text: "Greetings " + times // (7) font is a grouped property font.family: "Ubuntu" font.pixelSize: 24 // (8) KeyNavigation is an attached property KeyNavigation.tab: otherLabel // (9) signal handler for property changes onHeightChanged: console.log('height:', height) // focus is neeed to receive key events focus: true // change color based on focus value color: focus?"red":"black" } 让我们来看看不同属性的特点: 《QmlBook》In Chinese 61QML语法(QML Syntax) 1. id是⼀个⾮常特殊的属性值,它在⼀个QML⽂件中被⽤来引⽤元素。id 不是⼀个字符串,⽽是⼀个标识符和QML语法的⼀部分。⼀个id在⼀个 QML⽂档中是唯⼀的,并且不能被设置为其它值,也⽆法被查询(它的 ⾏为更像C++世界⾥的指针)。 2. ⼀个属性能够设置⼀个值,这个值依赖于它的类型。如果没有对⼀个属 性赋值,那么它将会被初始化为⼀个默认值。你可以查看特定的元素的 ⽂档来获得这些初始值的信息。 3. ⼀个属性能够依赖⼀个或多个其它的属性,这种操作称作属性绑定。当 它依赖的属性改变时,它的值也会更新。这就像订了⼀个协议,在这个 例⼦中height始终是width的两倍。 4. 添加⾃⼰定义的属性需要使⽤property修饰符,然后跟上类型,名字和可 选择的初始化值(property : )。如果没有初始值将会给定⼀个系统初始 值作为初始值。注意如果属性名与已定义的默认属性名不重复,使⽤ default关键字你可以将⼀个属性定义为默认属性。这在你添加⼦元素时 ⽤得着,如果他们是可视化的元素,⼦元素会⾃动的添加默认属性的⼦ 类型链表(children property list)。 5. 另⼀个重要的声明属性的⽅法是使⽤alias关键字(property alias : )。 alias关键字允许我们转发⼀个属性或者转发⼀个属性对象⾃⾝到另⼀个 作⽤域。我们将在后⾯定义组件导出内部属性或者引⽤根级元素id会使 ⽤到这个技术。⼀个属性别名不需要类型,它使⽤引⽤的属性类型或者 对象类型。 6. text属性依赖于⾃定义的timers(int整型数据类型)属性。int整型数据会 ⾃动的转换为string字符串类型数据。这样的表达⽅式本⾝也是另⼀种属 性绑定的例⼦,⽂本结果会在times属性每次改变时刷新。 7. ⼀些属性是按组分配的属性。当⼀个属性需要结构化并且相关的属性需 要联系在⼀起时,我们可以这样使⽤它。另⼀个组属性的编码⽅式是 font{family: "UBuntu"; pixelSize: 24 }。 8. ⼀些属性是元素⾃⾝的附加属性。这样做是为了全局的相关元素在应⽤ 《QmlBook》In Chinese 62QML语法(QML Syntax) 程序中只出现⼀次(例如键盘输⼊)。编码⽅式.: 。 9. 对于每个元素你都可以提供⼀个信号操作。这个操作在属性值改变时被 调⽤。例如这⾥我们完成了当height(⾼度)改变时会使⽤控制台输出 ⼀个信息。 警告 ⼀个元素id应该只在当前⽂档中被引⽤。QML提供了动态作⽤域的机制,后 加载的⽂档会覆盖之前加载⽂档的元素id号,这样就可以引⽤已加载并且没 有被覆盖的元素id,这有点类似创建全局变量。但不幸的是这样的代码阅读 性很差。⺫前这个还没有办法解决这个问题,所以你使⽤这个机制的时候最 好仔细⼀些甚⾄不要使⽤这种机制。如果你想向⽂档外提供元素的调⽤,你 可以在根元素上使⽤属性导出的⽅式来提供。 QML与JavaScript是最好的配合。在JavaScrpit的章节中我们将会更加详细的 介绍这种关系,现在我们只需要了解这种关系就可以了。 Text { id: label x: 24; y: 24 // custom counter property for space presses property int spacePresses: 0 text: "Space pressed: " + spacePresses + " times" // (1) handler for text changes onTextChanged: console.log("text changed to:", text) // need focus to receive key events focus: true // (2) handler with some JS 4.1.2 脚本(Scripting) 《QmlBook》In Chinese 63QML语法(QML Syntax) Keys.onSpacePressed: { increment() } // clear the text on escape Keys.onEscapePressed: { label.text = '' } // (3) a JS function function increment() { spacePresses = spacePresses + 1 } } 1. ⽂本改变操作onTextChanged会将每次空格键按下导致的⽂本改变输出 到控制台。 2. 当⽂本元素接收到空格键操作(⽤户在键盘上点击空格键),会调⽤ JavaScript函数increment()。 3. 定义⼀个JavaScript函数使⽤这种格式function (){....},在这个例⼦中是 增加spacePressed的计数。每次spacePressed的增加都会导致它绑定的 属性更新。 注意 QML的:(属性绑定)与JavaScript的=(赋值)是不同的。绑定是⼀个协 议,并且存在于整个⽣命周期。然⽽JavaScript赋值(=)只会产⽣⼀次效 果。当⼀个新的绑定⽣效或者使⽤JavaScript赋值给属性时,绑定的⽣命周 期就会结束。例如⼀个按键的操作设置⽂本属性为⼀个空的字符串将会销毁 我们的增值显⽰: Keys.onEscapePressed: { label.text = '' } 《QmlBook》In Chinese 64QML语法(QML Syntax) 在点击取消(ESC)后,再次点击空格键(space-bar)将不会更新我们的 显⽰,之前的text属性绑定(text: "Space pressed:" + spacePresses + "times")被销毁。 当你对改变属性的策略有冲突时(⽂本的改变基于⼀个增值的绑定并且可以 被JavaScript赋值清零),类似于这个例⼦,你最好不要使⽤绑定属性。你 需要使⽤赋值的⽅式来改变属性,属性绑定会在赋值操作后被销毁(销毁协 议!)。 《QmlBook》In Chinese 65QML语法(QML Syntax) 元素可以被分为可视化元素与⾮可视化元素。⼀个可视化元素(例如矩形框 Rectangle)有着⼏何形状并且可以在屏幕上显⽰。⼀个⾮可视化元素(例如 计时器Timer)提供了常⽤的功能,通常⽤于操作可视化元素。 现在我们将专注于⼏个基础的可视化元素,例如Item(基础元素对象), Rectangle(矩形框),Text(⽂本),Image(图像)和MouseArea(⿏标 区域)。 Item(基础元素对象)是所有可视化元素的基础对象,所有其它的可视化元 素都继承⾃Item。它⾃⾝不会有任何绘制操作,但是定义了所有可视化元素 共有的属性: Group(分组) Properties(属性) Geometry(⼏何属 性) x,y(坐标)定义了元素左上⾓的位置,width,height(⻓ 和宽)定义元素的显⽰范围,z(堆叠次序)定义元素之间 的重叠顺序。 Layout handling(布局操 作) anchors(锚定),包括左(left),右(right),上 (top),下(bottom),⽔平与垂直居中(vertical center,horizontal center),与margins(间距)⼀起定义 了元素与其它元素之间的位置关系。 Key handlikng(按 键操作) 附加属性key(按键)和keyNavigation(按键定位)属性来 控制按键操作,处理输⼊焦点(focus)可⽤操作。 Transformation(转 换) 缩放(scale)和rotate(旋转)转换,通⽤的x,y,z属性列 表转换(transform),旋转基点设置 (transformOrigin)。 Visual(可视化) 不透明度(opacity)控制透明度,visible(是否可⻅)控 制元素是否显⽰,clip(裁剪)⽤来限制元素边界的绘制, smooth(平滑)⽤来提⾼渲染质量。 State definition(状 态定义) states(状态列表属性)提供了元素当前所⽀持的状态列 表,当前属性的改变也可以使⽤transitions(转变)属性列 表来定义状态转变动画。 基本元素(Basic Elements) 4.2.1 基础元素对象(Item Element) 《QmlBook》In Chinese 66基本元素(Basic Elements) 为了更好的理解不同的属性,我们将会在这章中尽量的介绍这些元素的显⽰ 效果。请记住这些基本的属性在所有可视化元素中都是可以使⽤的,并且在 这些元素中的⼯作⽅式都是相同的。 注意 Item(基本元素对象)通常被⽤来作为其它元素的容器使⽤,类似HTML语 ⾔中的div元素(div element)。 Rectangle(矩形框)是基本元素对象的⼀个扩展,增加了⼀个颜⾊来填充 它。它还⽀持边界的定义,使⽤border.color(边界颜⾊), border.width(边界宽度)来⾃定义边界。你可以使⽤radius(半径)属性来 创建⼀个圆⾓矩形。 Rectangle { id: rect1 x: 12; y: 12 width: 76; height: 96 color: "lightsteelblue" } Rectangle { id: rect2 x: 112; y: 12 width: 76; height: 96 border.color: "lightsteelblue" border.width: 4 radius: 8 } 4.2.2 矩形框元素(Rectangle Element) 《QmlBook》In Chinese 67基本元素(Basic Elements) 注意 颜⾊的命名是来⾃SVG颜⾊的名称(查看http://www.w3.org/TR/css3- color/#svg-color可以获取更多的颜⾊名称)。你也可以使⽤其它的⽅法来 指定颜⾊,⽐如RGB字符串('#FF4444'),或者⼀个颜⾊名字(例 如'white')。 此外,填充的颜⾊与矩形的边框也⽀持⾃定义的渐变⾊。 Rectangle { id: rect1 x: 12; y: 12 width: 176; height: 96 gradient: Gradient { GradientStop { position: 0.0; color: "lightsteelblue" } GradientStop { position: 1.0; color: "slategray" } } border.color: "slategray" } 《QmlBook》In Chinese 68基本元素(Basic Elements) ⼀个渐变⾊是由⼀系列的梯度值定义的。每⼀个值定义了⼀个位置与颜⾊。 位置标记了y轴上的位置(0 = 顶,1 = 底)。GradientStop(倾斜点)的颜 ⾊标记了颜⾊的位置。 注意 ⼀个矩形框如果没有width/height(宽度与⾼度)将不可⻅。如果你有⼏个 相互关联width/height(宽度与⾼度)的矩形框,在你组合逻辑中出了错后 可能就会发⽣矩形框不可⻅,请注意这⼀点。 注意 这个函数⽆法创建⼀个梯形,最好使⽤⼀个已有的图像来创建梯形。有⼀种 可能是在旋转梯形时,旋转的矩形⼏何结构不会发⽣改变,但是这会导致⼏ 何元素相同的可⻅区域的混淆。从作者的观点来看类似的情况下最好使⽤设 计好的梯形图形来完成绘制。 显⽰⽂本你需要使⽤Text元素(Text Element)。它最值得注意的属性时字 符串类型的text属性。这个元素会使⽤给出的text(⽂本)与font(字体)来 计算初始化的宽度与⾼度。可以使⽤字体属性组来(font property group)来 改变当前的字体,例如font.family,font.pixelSize,等等。改变⽂本的颜⾊值 只需要改变颜⾊属性就可以了。 Text { text: "The quick brown fox" color: "#303030" font.family: "Ubuntu" font.pixelSize: 28 } 4.2.3 ⽂本元素(Text Element) 《QmlBook》In Chinese 69基本元素(Basic Elements) ⽂本可以使⽤horizontalAlignment与verticalAlignment属性来设置它的对⻬效 果。为了提⾼⽂本的渲染效果,你可以使⽤style和styleColor属性来配置⽂字 的外框效果,浮雕效果或者凹陷效果。对于过⻓的⽂本,你可能需要使⽤省 略号来表⽰,例如A very ... long text,你可以使⽤elide属性来完成这个操 作。elide属性允许你设置⽂本左边,右边或者中间的省略位置。如果你不 想'....'省略号出现,并且希望使⽤⽂字换⾏的⽅式显⽰所有的⽂本,你可以使 ⽤wrapMode属性(这个属性只在明确设置了宽度后才⽣效): Text { width: 40; height: 120 text: 'A very long text' // '...' shall appear in the middle elide: Text.ElideMiddle // red sunken text styling style: Text.Sunken styleColor: '#FF4444' // align text to the top verticalAlignment: Text.AlignTop // only sensible when no elide mode // wrapMode: Text.WordWrap } ⼀个text元素(text element)只显⽰的⽂本,它不会渲染任何背景修饰。除 了显⽰的⽂本,text元素背景是透明的。为⼀个⽂本元素提供背景是你⾃⼰需 要考虑的问题。 注意 知道⼀个⽂本元素(Text Element)的初始宽度与⾼度是依赖于⽂本字符串 《QmlBook》In Chinese 70基本元素(Basic Elements) 和设置的字体这⼀点很重要。⼀个没有设置宽度或者⽂本的⽂本元素(Text Element)将不可⻅,默认的初始宽度是0。 注意 通常你想要对⽂本元素布局时,你需要区分⽂本在⽂本元素内部的边界对⻬ 和由元素边界⾃动对⻬。前⼀种情况你需要使⽤horizontalAlignment和 verticalAlignment属性来完成,后⼀种情况你需要操作元素的⼏何形状或者 使⽤anchors(锚定)来完成。 ⼀个图像元素(Image Element)能够显⽰不同格式的图像(例如 PNG,JPG,GIF,BMP)。想要知道更加详细的图像格式⽀持信息,可以查看 Qt的相关⽂档。source属性(source property)提供了图像⽂件的链接信 息,fillMode(⽂件模式)属性能够控制元素对象的⼤⼩调整⾏为。 Image { x: 12; y: 12 // width: 48 // height: 118 source: "assets/rocket.png" } Image { x: 112; y: 12 width: 48 height: 118/2 source: "assets/rocket.png" fillMode: Image.PreserveAspectCrop clip: true } 4.2.4 图像元素(Image Element) 《QmlBook》In Chinese 71基本元素(Basic Elements) 注意 ⼀个URL可以是使⽤'/'语法的本地路径("./images/home.png")或者⼀个 ⺴络链接("http://example.org/home.png")。 注意 图像元素(Image element)使⽤PreserveAspectCrop可以避免裁剪图像 数据被渲染到图像边界外。默认情况下裁剪是被禁⽤的(clip:false)。你需 要打开裁剪(clip:true)来约束边界矩形的绘制。这对任何可视化元素都是 有效的。 建议 使⽤QQmlImageProvider你可以通过C++代码来创建⾃⼰的图像提供器,这 允许你动态创建图像并且使⽤线程加载。 为了与不同的元素交互,你通常需要使⽤MouseArea(⿏标区域)元素。这 是⼀个矩形的⾮可视化元素对象,你可以通过它来捕捉⿏标事件。当⽤户与 可视化端⼝交互时,mouseArea通常被⽤来与可视化元素对象⼀起执⾏命 令。 Rectangle { id: rect1 4.2.5 ⿏标区域元素(MouseArea Element) 《QmlBook》In Chinese 72基本元素(Basic Elements) x: 12; y: 12 width: 76; height: 96 color: "lightsteelblue" MouseArea { id: area width: parent.width height: parent.height onClicked: rect2.visible = !rect2.visible } } Rectangle { id: rect2 x: 112; y: 12 width: 76; height: 96 border.color: "lightsteelblue" border.width: 4 radius: 8 } 注意 这是QtQuick中⾮常重要的概念,输⼊处理与可视化显⽰分开。这样你的交 互区域可以⽐你显⽰的区域⼤很多。 《QmlBook》In Chinese 73基本元素(Basic Elements) 《QmlBook》In Chinese 74基本元素(Basic Elements) ⼀个组件是⼀个可以重复使⽤的元素,QML提供⼏种不同的⽅法来创建组 件。但是⺫前我们只对其中⼀种⽅法进⾏讲解:⼀个⽂件就是⼀个基础组 件。⼀个以⽂件为基础的组件在⽂件中创建了⼀个QML元素,并且将⽂件以 元素类型来命名(例如Button.qml)。你可以像任何其它的QtQuick模块中使 ⽤元素⼀样来使⽤这个组件。在我们下⾯的例⼦中,你将会使⽤你的代码作 为⼀个Button(按钮)来使⽤。 让我们来看看这个例⼦,我们创建了⼀个包含⽂本和⿏标区域的矩形框。它 类似于⼀个简单的按钮,我们的⺫标就是让它⾜够简单。 Rectangle { // our inlined button ui id: button x: 12; y: 12 width: 116; height: 26 color: "lightsteelblue" border.color: "slategrey" Text { anchors.centerIn: parent text: "Start" } MouseArea { anchors.fill: parent onClicked: { status.text = "Button clicked!" } } } Text { // text changes when button was clicked id: status x: 12; y: 76 width: 116; height: 26 text: "waiting ..." horizontalAlignment: Text.AlignHCenter } 组件(Compontents) 《QmlBook》In Chinese 75组件(Compontents) ⽤户界⾯将会看起来像下⾯这样。左边是初始化的状态,右边是按钮点击后 的效果。 我们的⺫标是提取这个按钮作为⼀个可重复使⽤的组件。我们可以简单的考 虑⼀下我们的按钮会有的哪些API(应⽤程序接⼝),你可以⾃⼰考虑⼀下你 的按钮应该有些什么。下⾯是我考虑的结果: // my ideal minimal API for a button Button { text: "Click Me" onClicked: { // do something } } 我想要使⽤text属性来设置⽂本,然后实现我们⾃⼰的点击操作。我也期望这 个按钮有⼀个⽐较合适的初始化⼤⼩(例如width:240)。 为了完成我们的⺫ 标,我创建了⼀个Button.qml⽂件,并且将我们的代码拷⻉了进去。我们在 根级添加⼀个属性导出⽅便使⽤者修改它。 我们在根级导出了⽂本和点击信号。通常我们命名根元素为root让引⽤更加 《QmlBook》In Chinese 76组件(Compontents) ⽅便。我们使⽤了QML的alias(别名)的功能,它可以将内部嵌套的QML元 素的属性导出到外⾯使⽤。有⼀点很重要,只有根级⺫录的属性才能够被其 它⽂件的组件访问。 // Button.qml import QtQuick 2.0 Rectangle { id: root // export button properties property alias text: label.text signal clicked width: 116; height: 26 color: "lightsteelblue" border.color: "slategrey" Text { id: label anchors.centerIn: parent text: "Start" } MouseArea { anchors.fill: parent onClicked: { root.clicked() } } } 使⽤我们新的Button元素只需要在我们的⽂件中简单的声明⼀下就可以了, 之前的例⼦将会被简化。 Button { // our Button component id: button x: 12; y: 12 text: "Start" 《QmlBook》In Chinese 77组件(Compontents) onClicked: { status.text = "Button clicked!" } } Text { // text changes when button was clicked id: status x: 12; y: 76 width: 116; height: 26 text: "waiting ..." horizontalAlignment: Text.AlignHCenter } 现在你可以在你的⽤户界⾯代码中随意的使⽤Button{ ...}来作为按钮了。⼀ 个真正的按钮将更加复杂,⽐如提供按键反馈或者添加⼀些装饰。 注意 就个⼈⽽⾔,可以更进⼀步的使⽤基础元素对象(Item)作为根元素。这样 可以防⽌⽤户改变我们设计的按钮的颜⾊,并且可以提供出更多相关控制的 API(应⽤程序接⼝)。我们的⺫标是导出⼀个最⼩的API(应⽤程序接 ⼝)。实际上我们可以将根矩形框(Rectangle)替换为⼀个基础元素 (Item),然后将⼀个矩形框(Rectangle)嵌套在这个根元素(root item)就可以完成了。 Item { id: root Rectangle { anchors.fill parent color: "lightsteelblue" border.color: "slategrey" } ... } 使⽤这项技术可以很简单的创建⼀系列可重⽤的组件。 《QmlBook》In Chinese 78组件(Compontents) 转换操作改变了⼀个对象的⼏何状态。QML元素对象通常能够被平移,旋 转,缩放。下⾯我们将讲解这些简单的操作和⼀些更⾼级的⽤法。 我们先从 ⼀个简单的转换开始。⽤下⾯的场景作为我们学习的开始。 简单的位移是通过改变x,y坐标来完成的。旋转是改变rotation(旋转)属性来 完成的,这个值使⽤⾓度作为单位(0~360)。缩放是通过改变scale(⽐ 例)的属性来完成的,⼩于1意味着缩⼩,⼤于1意味着放⼤。旋转与缩放不 会改变对象的⼏何形状,对象的x,y(坐标)与width/height(宽/⾼)也类 似。只有绘制指令是被转换的对象。 在我们展⽰例⼦之前我想要介绍⼀些东⻄:ClickableImage元素 (ClickableImage element),ClickableImage仅仅是⼀个包含⿏标区域的图 像元素。我们遵循⼀个简单的原则,三次使⽤相同的代码描述⼀个⽤户界⾯ 最好可以抽象为⼀个组件。 // ClickableImage.qml // Simple image which can be clicked import QtQuick 2.0 Image { id: root signal clicked MouseArea { anchors.fill: parent onClicked: root.clicked() } } 简单的转换(Simple Transformations) 《QmlBook》In Chinese 79简单的转换(Simple Transformations) 我们使⽤我们可点击图⽚元素来显⽰了三个⽕箭。当点击时,每个⽕箭执⾏ ⼀种简单的转换。点击背景将会重置场景。 // transformation.qml import QtQuick 2.0 Item { // set width based on given background width: bg.width height: bg.height Image { // nice background image id: bg source: "assets/background.png" } MouseArea { id: backgroundClicker // needs to be before the images as order matters // otherwise this mousearea would be before the other elements // and consume the mouse events anchors.fill: parent onClicked: { // reset our little scene 《QmlBook》In Chinese 80简单的转换(Simple Transformations) rocket1.x = 20 rocket2.rotation = 0 rocket3.rotation = 0 rocket3.scale = 1.0 } } ClickableImage { id: rocket1 x: 20; y: 100 source: "assets/rocket.png" onClicked: { // increase the x-position on click x += 5 } } ClickableImage { id: rocket2 x: 140; y: 100 source: "assets/rocket.png" smooth: true // need antialising onClicked: { // increase the rotation on click rotation += 5 } } ClickableImage { id: rocket3 x: 240; y: 100 source: "assets/rocket.png" smooth: true // need antialising onClicked: { // several transformations rotation += 5 scale -= 0.05 } } } 《QmlBook》In Chinese 81简单的转换(Simple Transformations) ⽕箭1在每次点击后X轴坐标增加5像素,⽕箭2每次点击后会旋转。⽕箭3每 次点击后会缩⼩。对于缩放和旋转操作我们都设置了smooth:true来增加反锯 齿,由于性能的原因通常是被关闭的(与剪裁属性clip类似)。当你看到你的 图形中出现锯齿时,你可能就需要打开平滑(smooth)。 注意 为了获得更好的显⽰效果,当缩放图⽚时推荐使⽤已缩放的图⽚来替代,过 量的放⼤可能会导致图⽚模糊不清。当你在缩放图⽚时你最好考虑使⽤ smooth:true来提⾼图⽚显⽰质量。 使⽤MouseArea来覆盖整个背景,点击背景可以初始化⽕箭的值。 注意 在代码中先出现的元素有更低的堆叠顺序(叫做z顺序值z-order),如果你 点击⽕箭1⾜够多次,你会看⻅⽕箭1移动到了⽕箭2下⾯。z轴顺序也可以使 ⽤元素对象的z-property来控制。 《QmlBook》In Chinese 82简单的转换(Simple Transformations) 由于⽕箭2后出现在代码中,⽕箭2将会放在⽕箭1上⾯。这同样适⽤于 MouseArea(⿏标区域),⼀个后出现在代码中的⿏标区域将会与之前的⿏ 标区域重叠,后出现的⿏标区域才能捕捉到⿏标事件。 请记住:⽂档中元素的顺序很重要。 《QmlBook》In Chinese 83简单的转换(Simple Transformations) 有⼀些QML元素被⽤于放置元素对象,它们被称作定位器,QtQuick模块提 供了Row,Column,Grid,Flow⽤来作为定位器。你可以在下⾯的插图中看 到它们使⽤相同内容的显⽰效果。 注意 在我们详细介绍前,我们先介绍⼀些相关的元素,红⾊(red),蓝⾊ (blue),绿⾊(green),⾼亮(lighter)与⿊暗(darker)⽅块,每⼀ 个组件都包含了⼀个48乘48的着⾊区域。下⾯是关于RedSquare(红⾊⽅ 块)的代码: // RedSquare.qml import QtQuick 2.0 Rectangle { width: 48 height: 48 color: "#ea7025" border.color: Qt.lighter(color) } 请注意使⽤了Qt.lighter(color)来指定了基于填充⾊的边界⾼亮⾊。我们 将会在后⾯的例⼦中使⽤到这些元素,希望后⾯的代码能够容易读懂。请记 住每⼀个矩形框的初始化⼤⼩都是48乘48像素⼤⼩。 Column(列)元素将它的⼦对象通过顶部对⻬的列⽅式进⾏排列。spacing 属性⽤来设置每个元素之间的间隔⼤⼩。 定位元素(Positioning Element) 《QmlBook》In Chinese 84定位元素(Positioning Element) // column.qml import QtQuick 2.0 DarkSquare { id: root width: 120 height: 240 Column { id: column anchors.centerIn: parent spacing: 8 RedSquare { } GreenSquare { width: 96 } BlueSquare { } } } // M1<< Row(⾏)元素将它的⼦对象从左到右,或者从右到左依次排列,排列⽅式 取决于layoutDirection属性。spacing属性⽤来设置每个元素之间的间隔⼤ ⼩。 《QmlBook》In Chinese 85定位元素(Positioning Element) // row.qml import QtQuick 2.0 BrightSquare { id: root width: 400; height: 120 Row { id: row anchors.centerIn: parent spacing: 20 BlueSquare { } GreenSquare { } RedSquare { } } } Grid(栅格)元素通过设置rows(⾏数)和columns(列数)将⼦对象排列 在⼀个栅格中。可以只限制⾏数或者列数。如果没有设置它们中的任意⼀ 个,栅格元素会⾃动计算⼦项⺫总数来获得配置,例如,设置rows(⾏数) 为3,添加了6个⼦项⺫到元素中,那么会⾃动计算columns(列数)为2。属 性flow(流)与layoutDirection(布局⽅向)⽤来控制⼦元素的加⼊顺序。 spacing属性⽤来控制所有元素之间的间隔。 《QmlBook》In Chinese 86定位元素(Positioning Element) // grid.qml import QtQuick 2.0 BrightSquare { id: root width: 160 height: 160 Grid { id: grid rows: 2 columns: 2 anchors.centerIn: parent spacing: 8 RedSquare { } RedSquare { } RedSquare { } RedSquare { } } } 最后⼀个定位器是Flow(流)。通过flow(流)属性和layoutDirection(布局 ⽅向)属性来控制流的⽅向。它能够从头到底的横向布局,也可以从左到右 或者从右到左进⾏布局。作为加⼊流中的⼦对象,它们在需要时可以被包装 成新的⾏或者列。为了让⼀个流可以⼯作,必须指定⼀个宽度或者⾼度,可 以通过属性直接设定,或者通过anchor(锚定)布局设置。 《QmlBook》In Chinese 87定位元素(Positioning Element) // flow.qml import QtQuick 2.0 BrightSquare { id: root width: 160 height: 160 Flow { anchors.fill: parent anchors.margins: 20 spacing: 20 RedSquare { } BlueSquare { } GreenSquare { } } } 通常Repeater(重复元素)与定位器⼀起使⽤。它的⼯作⽅式就像for循环与 迭代器的模式⼀样。在这个最简单的例⼦中,仅仅提供了⼀个循环的例⼦。 《QmlBook》In Chinese 88定位元素(Positioning Element) // repeater.qml import QtQuick 2.0 DarkSquare { id: root width: 252 height: 252 property variant colorArray: ["#00bde3", "#67c111", "#ea7025"] Grid{ anchors.fill: parent anchors.margins: 8 spacing: 4 Repeater { model: 16 Rectangle { width: 56; height: 56 property int colorIndex: Math.floor(Math.random()*3) color: root.colorArray[colorIndex] border.color: Qt.lighter(color) Text { anchors.centerIn: parent color: "#f0f0f0" 《QmlBook》In Chinese 89定位元素(Positioning Element) text: "Cell " + index } } } } } 在这个重复元素的例⼦中,我们使⽤了⼀些新的⽅法。我们使⽤⼀个颜⾊数 组定义了⼀组颜⾊属性。重复元素能够创建⼀连串的矩形框(16个,就像模 型中定义的那样)。每⼀次的循环都会创建⼀个矩形框作为repeater的⼦对 象。在矩形框中,我们使⽤了JS数学函数Math.floor(Math.random()*3)来选 择颜⾊。这个函数会给我们⽣成⼀个0~2的随机数,我们使⽤这个数在我们 的颜⾊数组中选择颜⾊。注意之前我们说过JavaScript是QtQuick中的⼀部 分,所以这些典型的库函数我们都可以使⽤。 ⼀个重复元素循环时有⼀个index(索引)属性值。当前的循环索引 (0,1,2,....15)。我们可以使⽤这个索引值来做⼀些操作,例如在我们这个 例⼦中使⽤Text(⽂本)显⽰当前索引值。 注意 ⾼级的⼤数据模型处理和使⽤动态代理的动态视图会在模型与视图(model- view)章节中讲解。当有⼀⼩部分的静态数据需要显⽰时,使⽤重复元素是 最好的⽅式。 《QmlBook》In Chinese 90定位元素(Positioning Element) QML使⽤anchors(锚)对元素进⾏布局。anchoring(锚定)是基础元素对 象的基本属性,可以被所有的可视化QML元素使⽤。⼀个anchors(锚)就 像⼀个协议,并且⽐⼏何变化更加强⼤。Anchors(锚)是相对关系的表达 式,你通常需要与其它元素搭配使⽤。 ⼀个元素有6条锚定线(top顶,bottom底,left左,right右,horizontalCenter ⽔平中,verticalCenter垂直中)。在⽂本元素(Text Element)中有⼀条⽂ 本的锚定基线(baseline)。每⼀条锚定线都有⼀个偏移(offset)值,在 top(顶),bottom(底),left(左),right(右)的锚定线中它们也被称 作边距。对于horizontalCenter(⽔平中)与verticalCenter(垂直中)与 baseline(⽂本基线)中被称作偏移值。 布局元素(Layout Items) 《QmlBook》In Chinese 91布局元素(Layout Items) 1. 元素填充它的⽗元素。 GreenSquare { BlueSquare { width: 12 anchors.fill: parent anchors.margins: 8 text: '(1)' } } 2. 元素左对⻬它的⽗元素。 GreenSquare { BlueSquare { width: 48 y: 8 anchors.left: parent.left anchors.leftMargin: 8 text: '(2)' } } 《QmlBook》In Chinese 92布局元素(Layout Items) 3. 元素的左边与它⽗元素的右边对⻬。 GreenSquare { BlueSquare { width: 48 anchors.left: parent.right text: '(3)' } } 4. 元素中间对⻬。Blue1与它的⽗元素⽔平中间对⻬。Blue2与Blue1中间对 ⻬,并且它的顶部对⻬Blue1的底部。 GreenSquare { BlueSquare { id: blue1 width: 48; height: 24 y: 8 anchors.horizontalCenter: parent.horizontalCenter } BlueSquare { id: blue2 width: 72; height: 24 anchors.top: blue1.bottom anchors.topMargin: 4 anchors.horizontalCenter: blue1.horizontalCenter text: '(4)' } } 5. 元素在它的⽗元素中居中。 GreenSquare { BlueSquare { width: 48 anchors.centerIn: parent 《QmlBook》In Chinese 93布局元素(Layout Items) text: '(5)' } } 6. 元素⽔平⽅向居中对⻬⽗元素并向后偏移12像素,垂直⽅向居中对⻬。 GreenSquare { BlueSquare { width: 48 anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenterOffset: -12 anchors.verticalCenter: parent.verticalCenter text: '(6)' } } 注意 我们的⽅格都打开了拖拽。试着拖放⼏个⽅格。你可以发现第⼀个⽅格⽆法 被拖拽因为它每个边都被固定了,当然第⼀个⽅格的⽗元素能够被拖拽是因 为它的⽗元素没有被固定。第⼆个⽅格能够在垂直⽅向上拖拽是因为它只有 左边被固定了。类似的第三个和第四个⽅格也只能在垂直⽅向上拖拽是因为 它们都使⽤⽔平居中对⻬。第五个⽅格使⽤居中布局,它也⽆法被移动,第 六个⽅格与第五个⽅格类似。拖拽⼀个元素意味着会改变它的x,y坐标。 anchoring(锚定)⽐⼏何变化(例如x,y坐标变化)更强⼤是因为锚定线 (anchored lines)的限制,我们将在后⾯讨论动画时看到这些功能的强 ⼤。 《QmlBook》In Chinese 94布局元素(Layout Items) 我们已经使⽤过MouseArea(⿏标区域)作为⿏标输⼊元素。这⾥我们将更 多的介绍关于键盘输⼊的⼀些东⻄。我们开始介绍⽂本编辑的元素: TextInput(⽂本输⼊)和TextEdit(⽂本编辑)。 ⽂本输⼊允许⽤户输⼊⼀⾏⽂本。这个元素⽀持使⽤正则表达式验证器来限 制输⼊和输⼊掩码的模式设置。 // textinput.qml import QtQuick 2.0 Rectangle { width: 200 height: 80 color: "linen" TextInput { id: input1 x: 8; y: 8 width: 96; height: 20 focus: true text: "Text Input 1" } TextInput { id: input2 x: 8; y: 36 width: 96; height: 20 text: "Text Input 2" } } 输⼊元素(Input Element) 4.7.1 ⽂本输⼊(TextInput) 《QmlBook》In Chinese 95输⼊元素(Input Element) ⽤户可以通过点击TextInput来改变焦点。为了⽀持键盘改变焦点,我们可以 使⽤KeyNavigation(按键向导)这个附加属性。 // textinput2.qml import QtQuick 2.0 Rectangle { width: 200 height: 80 color: "linen" TextInput { id: input1 x: 8; y: 8 width: 96; height: 20 focus: true text: "Text Input 1" KeyNavigation.tab: input2 } TextInput { id: input2 x: 8; y: 36 width: 96; height: 20 text: "Text Input 2" KeyNavigation.tab: input1 } } KeyNavigation(按键向导)附加属性可以预先设置⼀个元素id绑定切换焦点 的按键。 《QmlBook》In Chinese 96输⼊元素(Input Element) ⼀个⽂本输⼊元素(text input element)只显⽰⼀个闪烁符和已经输⼊的⽂ 本。⽤户需要⼀些可⻅的修饰来鉴别这是⼀个输⼊元素,例如⼀个简单的矩 形框。当你放置⼀个TextInput(⽂本输⼊)在⼀个元素中时,你需要确保其 它的元素能够访问它导出的⼤多数属性。 我们提取这⼀段代码作为我们⾃⼰的组件,称作TLineEditV1⽤来重复使⽤。 // TLineEditV1.qml import QtQuick 2.0 Rectangle { width: 96; height: input.height + 8 color: "lightsteelblue" border.color: "gray" property alias text: input.text property alias input: input TextInput { id: input anchors.fill: parent anchors.margins: 4 focus: true } } 注意 如果你想要完整的导出TextInput元素,你可以使⽤property alias input: input来导出这个元素。第⼀个input是属性名字,第⼆个input是元素id。 我们使⽤TLineEditV1组件重写了我们的KeyNavigation(按键向导)的例 ⼦。 Rectangle { ... 《QmlBook》In Chinese 97输⼊元素(Input Element) TLineEditV1 { id: input1 ... } TLineEditV1 { id: input2 ... } } 尝试使⽤Tab按键来导航,你会发现焦点⽆法切换到input2上。这个例⼦中使 ⽤focus:true的⽅法不正确,这个问题是因为焦点被转移到input2元素时,包 含TlineEditV1的顶部元素接收了这个焦点并且没有将焦点转发给 TextInput(⽂本输⼊)。为了防⽌这个问题,QML提供了FocusScope(焦 点区域)。 ⼀个焦点区域(focus scope)定义了如果焦点区域接收到焦点,它的最后⼀ 个使⽤focus:true的⼦元素接收焦点,它将会把焦点传递给最后申请焦点的⼦ 元素。我们创建了第⼆个版本的TLineEdit组件,称作TLineEditV2,使⽤焦 点区域(focus scope)作为根元素。 // TLineEditV2.qml import QtQuick 2.0 FocusScope { width: 96; height: input.height + 8 Rectangle { anchors.fill: parent 4.7.2 焦点区域(FocusScope) 《QmlBook》In Chinese 98输⼊元素(Input Element) color: "lightsteelblue" border.color: "gray" } property alias text: input.text property alias input: input TextInput { id: input anchors.fill: parent anchors.margins: 4 focus: true } } 现在我们的例⼦将像下⾯这样: Rectangle { ... TLineEditV2 { id: input1 ... } TLineEditV2 { id: input2 ... } } 按下Tab按键可以成功的在两个组件之间切换焦点,并且能够正确的将焦点 锁定在组件内部的⼦元素中。 ⽂本编辑(TextEdit)元素与⽂本输⼊(TextInput)⾮常类似,它⽀持多⾏ ⽂本编辑。它不再⽀持⽂本输⼊的限制,但是提供了已绘制⽂本的⼤⼩查询 4.7.3 ⽂本编辑(TextEdit) 《QmlBook》In Chinese 99输⼊元素(Input Element) (paintedHeight,paintedWidth)。我们也创建了⼀个我们⾃⼰的组件 TTextEdit,可以编辑它的背景,使⽤focus scope(焦点区域)来更好的切换 焦点。 // TTextEdit.qml import QtQuick 2.0 FocusScope { width: 96; height: 96 Rectangle { anchors.fill: parent color: "lightsteelblue" border.color: "gray" } property alias text: input.text property alias input: input TextEdit { id: input anchors.fill: parent anchors.margins: 4 focus: true } } 你可以像下⾯这样使⽤这个组件: // textedit.qml import QtQuick 2.0 Rectangle { width: 136 height: 120 color: "linen" 《QmlBook》In Chinese 100输⼊元素(Input Element) TTextEdit { id: input x: 8; y: 8 width: 120; height: 104 focus: true text: "Text Edit" } } 附加属性key允许你基于某个按键的点击来执⾏代码。例如使⽤up,down按 键来移动⼀个⽅块,left,right按键来旋转⼀个元素,plus,minus按键来缩 放⼀个元素。 // keys.qml import QtQuick 2.0 DarkSquare { width: 400; height: 200 GreenSquare { id: square x: 8; y: 8 } focus: true Keys.onLeftPressed: square.x -= 8 Keys.onRightPressed: square.x += 8 Keys.onUpPressed: square.y -= 8 4.7.4 按键元素(Key Element) 《QmlBook》In Chinese 101输⼊元素(Input Element) Keys.onDownPressed: square.y += 8 Keys.onPressed: { switch(event.key) { case Qt.Key_Plus: square.scale += 0.2 break; case Qt.Key_Minus: square.scale -= 0.2 break; } } } 《QmlBook》In Chinese 102输⼊元素(Input Element) 后续添加。 ⾼级⽤法(Advanced Techniques) 《QmlBook》In Chinese 103⾼级⽤法(Advanced Techniques) 注意 最后⼀次构建:2014年1⽉20⽇下午18:00。 这章的源代码能够在assetts folder找到。 到⺫前为⽌,我们已经介绍了简单的图形元素和怎样布局,怎样操作它们。 这⼀章介绍如何控制属性值的变化,通过动画的⽅式在⼀段时间内来改变属 性值。这项技术是建⽴⼀个现代化的平滑界⾯的基础,通过使⽤状态和过渡 来扩展你的⽤户界⾯。每⼀种状态定义了属性的改变,与动画联系起来的状 态改变称作过渡。 动态元素(Fluid Elements) 《QmlBook》In Chinese 104动态元素(Fluid Elements) 动画被⽤于属性的改变。⼀个动画定义了属性值改变的曲线,将⼀个属性值 变化从⼀个值过渡到另⼀个值。动画是由⼀连串的⺫标属性活动定义的,平 缓的曲线算法能够引发⼀个定义时间内属性的持续变化。所有在QtQuick中的 动画都由同⼀个计时器来控制,因此它们始终都保持同步,这也提⾼了动画 的性能和显⽰效果。 注意 动画控制了属性的改变,也就是值的插⼊。这是⼀个基本的概念,QML是基 于元素,属性与脚本的。每⼀个元素都提供了许多的属性,每⼀个属性都在 等待使⽤动画。在这本书中你将会看到这是⼀个壮阔的场景,你会发现你⾃ ⼰在看⼀些动画时欣赏它们的美丽并且肯定⾃⼰的创造性想法。然后请记 住:动画控制了属性的改变,每个元素都有⼤量的属性供你任意使⽤。 // animation.qml import QtQuick 2.0 Image { source: "assets/background.png" Image { x: 40; y: 80 source: "assets/rocket.png" NumberAnimation on x { 动画(Animations) 《QmlBook》In Chinese 105动画(Animations) to: 240 duration: 4000 loops: Animation.Infinite } RotationAnimation on rotation { to: 360 duration: 4000 loops: Animation.Infinite } } } 上⾯这个例⼦在x坐标和旋转属性上应⽤了⼀个简单的动画。每⼀次动画持续 4000毫秒并且永久循环。x轴坐标动画展⽰了⽕箭的x坐标逐渐移⾄240,旋 转动画展⽰了当前⾓度到360度的旋转。两个动画同时运⾏,并且在加载⽤ 户界⾯完成后开始。 现在你可以通过to属性和duration属性来实现动画效果。或者你可以在opacity 或者scale上添加动画作为例⼦,集成这两个参数,你可以实现⽕箭逐渐消失 在太空中,试试吧! 有⼏种类型的动画,每⼀种都在特定情况下都有最佳的效果,下⾯列出了⼀ 些常⽤的动画: PropertyAnimation(属性动画)- 使⽤属性值改变播放的动画 NumberAnimation(数字动画)- 使⽤数字改变播放的动画 ColorAnimation(颜⾊动画)- 使⽤颜⾊改变播放的动画 RotationAnimation(旋转动画)- 使⽤旋转改变播放的动画 除了上⾯这些基本和通常使⽤的动画元素,QtQuick还提供了⼀切特殊场景下 使⽤的动画: 5.1.1 动画元素(Animation Elements) 《QmlBook》In Chinese 106动画(Animations) PauseAnimation(停⽌动画)- 运⾏暂停⼀个动画 SequentialAnimation(顺序动画)- 允许动画有序播放 ParallelAnimation(并⾏动画)- 允许动画同时播放 AnchorAnimation(锚定动画)- 使⽤锚定改变播放的动画 ParentAnimation(⽗元素动画)- 使⽤⽗对象改变播放的动画 SmotthedAnimation(平滑动画)- 跟踪⼀个平滑值播放的动画 SpringAnimation(弹簧动画)- 跟踪⼀个弹簧变换的值播放的动画 PathAnimation(路径动画)- 跟踪⼀个元素对象的路径的动画 Vector3dAnimation(3D容器动画)- 使⽤QVector3d值改变播放的动画 我们将在后⾯学习怎样创建⼀连串的动画。当使⽤更加复杂的动画时,我们 可能需要在播放⼀个动画时中改变⼀个属性或者运⾏⼀个脚本。对于这个问 题,QtQuick提供了⼀个动作元素: PropertyAction(属性动作)- 在播放动画时改变属性 ScriptAction(脚本动作)- 在播放动画时运⾏脚本 在这⼀章中我们将会使⽤⼀些⼩的例⼦来讨论⼤多数类型的动画。 动画可以通过以下⼏种⽅式来应⽤: 属性动画 - 在元素完整加载后⾃动运⾏ 属性动作 - 当属性值改变时⾃动运⾏ 独⽴运⾏动画 - 使⽤start()函数明确指定运⾏或者running属性被设置为 5.1.2 应⽤动画(Applying Animations) 《QmlBook》In Chinese 107动画(Animations) true(⽐如通过属性绑定) 后⾯我们会谈论如何在状态变换时播放动画。 扩展可点击图像元素版本2(ClickableImage Version2) 为了演⽰动画的使⽤⽅法,我们重新实现了ClickableImage组件并且使⽤了 ⼀个⽂本元素(Text Element)来扩展它。 // ClickableImageV2.qml // Simple image which can be clicked import QtQuick 2.0 Item { id: root width: container.childrenRect.width height: container.childrenRect.height property alias text: label.text property alias source: image.source signal clicked Column { id: container Image { id: image } Text { id: label width: image.width horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap color: "#111111" } } MouseArea { anchors.fill: parent onClicked: root.clicked() } 《QmlBook》In Chinese 108动画(Animations) } 为了给图⽚下⾯的元素定位,我们使⽤了Column(列)定位器,并且使⽤基 于列的⼦矩形(childRect)属性来计算它的宽度和⾼度(width and height)。我们导出了⽂本(text)和图形源(source)属性,⼀个点击信号 (clicked signal)。我们使⽤⽂本元素的wrapMode属性来设置⽂本与图像⼀ 样宽并且可以⾃动换⾏。 注意 由于⼏何依赖关系的反向(⽗⼏何对象依赖于⼦⼏何对象)我们不能对 ClickableImageV2设置宽度/⾼度(width/height),因为这样将会破坏我 们已经做好的属性绑定。这是我们内部设计的限制,作为⼀个设计组件的⼈ 你需要明⽩这⼀点。通常我们更喜欢内部⼏何图像依赖于⽗⼏何对象。 三个⽕箭位于相同的y轴坐标(y = 200)。它们都需要移动到y = 40。每⼀个 ⽕箭都使⽤了⼀种的⽅法来完成这个功能。 《QmlBook》In Chinese 109动画(Animations) ClickableImageV3 { id: rocket1 x: 40; y: 200 source: "assets/rocket2.png" text: "animation on property" NumberAnimation on y { to: 40; duration: 4000 } } 第⼀个⽕箭 第⼀个⽕箭使⽤了Animation on 属性变化的策略来完成。动画会在加载完成 后⽴即播放。点击⽕箭可以重置它回到开始的位置。在动画播放时重置第⼀ 个⽕箭不会有任何影响。在动画开始前的⼏分之⼀秒设置⼀个新的y轴坐标让 ⼈感觉挺不安全的,应当避免这样的属性值竞争的变化。 ClickableImageV3 { id: rocket2 x: 152; y: 200 source: "assets/rocket2.png" text: "behavior on property" Behavior on y { NumberAnimation { duration: 4000 } } onClicked: y = 40 // random y on each click // onClicked: y = 40+Math.random()*(205-40) } 第⼆个⽕箭 第⼆个⽕箭使⽤了behavior on 属性⾏为策略的动画。这个⾏为告诉属性值每 时每刻都在变化,通过动画的⽅式来改变这个值。可以使⽤⾏为元素的 enabled : false来设置⾏为失效。当你点击这个⽕箭时它将会开始运⾏(y轴 《QmlBook》In Chinese 110动画(Animations) 坐标逐渐移⾄40)。然后其它的点击对于位置的改变没有任何的影响。你可 以试着使⽤⼀个随机值(例如 40+(Math.random()*(205-40))来设置y轴坐 标。你可以发现动画始终会将移动到新位置的时间匹配在4秒内完成。 ClickableImageV3 { id: rocket3 x: 264; y: 200 source: "assets/rocket2.png" onClicked: anim.start() // onClicked: anim.restart() text: "standalone animation" NumberAnimation { id: anim target: rocket3 properties: "y" from: 205 to: 40 duration: 4000 } } 第三个⽕箭 第三个⽕箭使⽤standalone animation独⽴动画策略。这个动画由⼀个私有的 元素定义并且可以写在⽂档的任何地⽅。点击⽕箭调⽤动画函数start()来启动 动画。每⼀个动画都有start(),stop(),resume(),restart()函数。这个动画 ⾃⾝可以⽐其他类型的动画更早的获取到更多的相关信息。我们只需要定义 ⺫标和⺫标元素的属性需要怎样改变的⼀个动画。我们定义⼀个to属性的 值,在这个例⼦中我们也定义了⼀个from属性的值允许动画可以重复运⾏。 《QmlBook》In Chinese 111动画(Animations) 点击背景能够重新设置所有的⽕箭回到它们的初始位置。第⼀个⽕箭⽆法被 重置,只有重启程序重新加载元素才能重置它。 注意 另⼀个启动/停⽌⼀个动画的⽅法是绑定⼀个动画的running属性。当需要⽤ 户输⼊控制属性时这种⽅法⾮常有⽤: NumberAnimation { ... // animation runs when mouse is pressed running: area.pressed } MouseArea { id: area } 5.1.3 缓冲曲线(Easing Curves) 《QmlBook》In Chinese 112动画(Animations) 属性值的改变能够通过⼀个动画来控制,缓冲曲线属性影响了⼀个属性值改 变的插值算法。我们现在已经定义的动画都使⽤了⼀种线性的插值算法,因 为⼀个动画的默认缓冲类型是Easing.Linear。在⼀个⼩场景下的x轴与y轴坐 标改变可以得到最好的视觉效果。⼀个线性插值算法将会在动画开始时使⽤ from的值到动画结束时使⽤的to值绘制⼀条直线,所以缓冲类型定义了曲线 的变化情况。精⼼为⼀个移动的对象挑选⼀个合适的缓冲类型将会使界⾯更 加⾃然,例如⼀个⻚⾯的滑出,最初使⽤缓慢的速度滑出,然后在最后滑出 时使⽤⾼速滑出,类似翻书⼀样的效果。 注意 不要过度的使⽤动画。⽤户界⾯动画的设计应该尽量⼩⼼,动画是让界⾯更 加⽣动⽽不是充满整个界⾯。眼睛对于移动的东⻄⾮常敏感,很容易干扰⽤ 户的使⽤。 在下⾯的例⼦中我们将会使⽤不同的缓冲曲线,每⼀种缓冲曲线都都使⽤了 ⼀个可点击图⽚来展⽰,点击将会在动画中设置⼀个新的缓冲类型并且使⽤ 这种曲线重新启动动画。 扩展可点击图像V3(ClickableImage V3) 我们给图⽚和⽂本添加了⼀个⼩的外框来增强我们的ClickableImage。添加 ⼀个属性property bool framed: false来作为我们的API,基于framed的值我们 能够设置这个框是否可⻅,并且不破坏之前⽤户的使⽤。下⾯是我们做的修 改。 // ClickableImageV2.qml // Simple image which can be clicked import QtQuick 2.0 Item { id: root width: container.childrenRect.width + 16 《QmlBook》In Chinese 113动画(Animations) height: container.childrenRect.height + 16 property alias text: label.text property alias source: image.source signal clicked // M1>> // ... add a framed rectangle as container property bool framed : false Rectangle { anchors.fill: parent color: "white" visible: root.framed } 这个例⼦的代码⾮常简洁。我们使⽤了⼀连串的缓冲曲线的名称(property variant easings)并且在⼀个Repeater(重复元素)中将它们分配给⼀个 ClickableImage。图⽚的源路径通过⼀个命名⽅案来定义,⼀个叫 做“InQuad”的缓冲曲线在“curves/InQuad.png”中有⼀个对应的图⽚。如果你 点击⼀个曲线图,这个点击将会分配⼀个缓冲类型给动画然后重新启动动 画。动画⾃⾝是⽤来设置⽅块的x坐标属性在2秒内变化的独⽴动画。 // easingtypes.qml import QtQuick 2.0 DarkSquare { id: root width: 600 height: 340 // A list of easing types property variant easings : [ "Linear", "InQuad", "OutQuad", "InOutQuad", "InCubic", "InSine", "InCirc", "InElastic", "InBack", "InBounce" ] Grid { 《QmlBook》In Chinese 114动画(Animations) id: container anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter anchors.margins: 16 height: 200 columns: 5 spacing: 16 // iterates over the 'easings' list Repeater { model: easings ClickableImageV3 { framed: true // the current data entry from 'easings' list text: modelData source: "curves/" + modelData + ".png" onClicked: { // set the easing type on the animation anim.easing.type = modelData // restart the animation anim.restart() } } } } // The square to be animated GreenSquare { id: square x: 40; y: 260 } // The animation to test the easing types NumberAnimation { id: anim target: square from: 40; to: root.width - 40 - square.width properties: "x" duration: 2000 } } 《QmlBook》In Chinese 115动画(Animations) 当你运⾏这个例⼦时,请注意观察动画的改变速度。⼀些动画对于这个对象 看起来很⾃然,⼀些看起来⾮常恼⽕。 除了duration属性与easing.type属性,你也可以对动画进⾏微调。例如 PropertyAnimation属性,⼤多数动画都⽀持附加的easing.amplitude(缓冲 振幅),easing.overshoot(缓冲溢出),easing.period(缓冲周期),这些 属性允许你对个别的缓冲曲线进⾏微调。不是所有的缓冲曲线都⽀持这些参 数。可以查看Qt PropertyAnimation⽂档中的缓冲列表(easing table)来查 看⼀个缓冲曲线的相关参数。 注意 对于⽤户界⾯正确的动画⾮常重要。请记住动画是帮助⽤户界⾯更加⽣动⽽ 不是刺激⽤户的眼睛。 通常使⽤的动画⽐⼀个属性的动画更加复杂。例如你想同时运⾏⼏个动画并 把他们连接起来,或者在⼀个⼀个的运⾏,或者在两个动画之间执⾏⼀个脚 本。动画分组提供了很好的帮助,作为命名建议可以叫做⼀组动画。有两种 ⽅法来分组:平⾏与连续。你可以使⽤SequentialAnimation(连续动画)和 ParallelAnimation(平⾏动画)来实现它们,它们作为动画的容器来包含其 它的动画元素。 当开始时,平⾏元素的所有⼦动画都会平⾏运⾏,它允许你在同⼀时间使⽤ 5.1.4 动画分组(Grouped Animations) 《QmlBook》In Chinese 116动画(Animations) 不同的属性来播放动画。 // parallelanimation.qml import QtQuick 2.0 BrightSquare { id: root width: 300 height: 200 property int duration: 3000 ClickableImageV3 { id: rocket x: 20; y: 120 source: "assets/rocket2.png" onClicked: anim.restart() } ParallelAnimation { id: anim NumberAnimation { target: rocket properties: "y" to: 20 duration: root.duration } NumberAnimation { target: rocket properties: "x" to: 160 duration: root.duration } } } 《QmlBook》In Chinese 117动画(Animations) ⼀个连续的动画将会⼀个⼀个的运⾏⼦动画。 // sequentialanimation.qml import QtQuick 2.0 BrightSquare { id: root width: 300 height: 200 property int duration: 3000 ClickableImageV3 { id: rocket x: 20; y: 120 source: "assets/rocket2.png" onClicked: anim.restart() } SequentialAnimation { id: anim NumberAnimation { target: rocket properties: "y" to: 20 // 60% of time to travel up duration: root.duration*0.6 } NumberAnimation { target: rocket properties: "x" to: 160 // 40% of time to travel sideways duration: root.duration*0.4 } 《QmlBook》In Chinese 118动画(Animations) } } 分组动画也可以被嵌套,例如⼀个连续动画可以拥有两个平⾏动画作为⼦动 画。我们来看看这个⾜球的例⼦。这个动画描述了⼀个从左向右扔⼀个球的 ⾏为: 要弄明⽩这个动画我们需要剖析这个⺫标的运动过程。我们需要记住这个动 画是通过属性变化来实现的动画,下⾯是不同部分的转换: 从左向右的x坐标转换(X1)。 从下往上的y坐标转换(Y1)然后跟着⼀个从上往下的Y坐标转换 《QmlBook》In Chinese 119动画(Animations) (Y2)。 整个动画过程中360度旋转。 这个动画将会花掉3秒钟的时间。 我们使⽤⼀个空的基本元素对象(Item)作为根元素,它的宽度为480,⾼度 为300。 import QtQuick 1.1 Item { id: root width: 480 height: 300 property int duration: 3000 ... } 《QmlBook》In Chinese 120动画(Animations) 我们定义动画的总持续时间作为参考,以便更好的同步各部分的动画。 下⼀步我们需需要添加⼀个背景,在我们这个例⼦中有两个矩形框分别使⽤ 了绿⾊渐变和蓝⾊渐变填充。 Rectangle { id: sky width: parent.width height: 200 gradient: Gradient { GradientStop { position: 0.0; color: "#0080FF" } GradientStop { position: 1.0; color: "#66CCFF" } } } Rectangle { id: ground anchors.top: sky.bottom anchors.bottom: root.bottom width: parent.width gradient: Gradient { GradientStop { position: 0.0; color: "#00FF00" } GradientStop { position: 1.0; color: "#00803F" } } } 上⾯部分的蓝⾊区域⾼度为200像素,下⾯部分的区域使⽤上⾯的蓝⾊区域 的底作为锚定的顶,使⽤根元素的底作为底。 让我们将⾜球加⼊到屏幕上,⾜球是⼀个图⽚,位于路 《QmlBook》In Chinese 121动画(Animations) 径“assets/soccer_ball.png”。⾸先我们需要将它放置在左下⾓接近边界处。 Image { id: ball x: 20; y: 240 source: "assets/soccer_ball.png" MouseArea { anchors.fill: parent onClicked: { ball.x = 20; ball.y = 240 anim.restart() } } } 图⽚与⿏标区域连接,点击球将会重置球的状态,并且动画重新开始。 ⾸先使⽤⼀个连续的动画来播放两次的y轴变换。 SequentialAnimation { id: anim NumberAnimation { target: ball properties: "y" to: 20 duration: root.duration * 0.4 } NumberAnimation { 《QmlBook》In Chinese 122动画(Animations) target: ball properties: "y" to: 240 duration: root.duration * 0.6 } } 在动画总时间的40%的时间⾥完成上升部分,在动画总时间的60%的时间⾥ 完成下降部分,⼀个动画完成后播放下⼀个动画。⺫前还没有使⽤任何缓冲 曲线。缓冲曲线将在后⾯使⽤easing curves来添加,现在我们只关⼼如何使 ⽤动画来完成过渡。 现在我们需要添加x轴坐标转换。x轴坐标转换需要与y轴坐标转换同时进⾏, 所以我们需要将y轴坐标转换的连续动画和x轴坐标转换⼀起压缩进⼀个平⾏ 动画中。 ParallelAnimation { id: anim SequentialAnimation { // ... our Y1, Y2 animation } NumberAnimation { // X1 animation target: ball properties: "x" to: 400 duration: root.duration } } 《QmlBook》In Chinese 123动画(Animations) 最后我们想要旋转这个球,我们需要向平⾏动画中添加⼀个新的动画,我们 选择RotationAnimation来实现旋转。 ParallelAnimation { id: anim SequentialAnimation { // ... our Y1, Y2 animation } NumberAnimation { // X1 animation // X1 animation } RotationAnimation { target: ball properties: "rotation" to: 720 duration: root.duration } } 我们已经完成了整个动画链表,然后我们需要给动画提供⼀个正确的缓冲曲 线来描述⼀个移动的球。对于Y1动画我们使⽤Easing.OutCirc缓冲曲线,它 看起来更像是⼀个圆周运动。Y2使⽤了Easing.OutBounce缓冲曲线,因为在 最后球会发⽣反弹。(试试使⽤Easing.InBounce,你会发现反弹将会⽴刻开 始。)。X1和ROT1动画都使⽤线性曲线。 下⾯是这个动画最后的代码,提供给你作为参考: ParallelAnimation { id: anim SequentialAnimation { NumberAnimation { 《QmlBook》In Chinese 124动画(Animations) target: ball properties: "y" to: 20 duration: root.duration * 0.4 easing.type: Easing.OutCirc } NumberAnimation { target: ball properties: "y" to: 240 duration: root.duration * 0.6 easing.type: Easing.OutBounce } } NumberAnimation { target: ball properties: "x" to: 400 duration: root.duration } RotationAnimation { target: ball properties: "rotation" to: 720 duration: root.duration * 1.1 } } 《QmlBook》In Chinese 125动画(Animations) 通常我们将⽤户界⾯描述为⼀种状态。⼀个状态定义了⼀组属性的改变,并 且会在⼀定的条件下被触发。另外在这些状态转化的过程中可以有⼀个过 渡,定义了这些属性的动画或者⼀些附加的动作。当进⼊⼀个新的状态时, 动作也可以被执⾏。 在QML中,使⽤State元素来定义状态,需要与基础元素对象(Item)的 states序列属性连接。状态通过它的状态名来鉴别,由组成它的⼀系列简单 的属性来改变元素。默认的状态在初始化元素属性时定义,并命名为“”(⼀ 个空的字符串)。 Item { id: root states: [ State { name: "go" PropertyChanges { ... } }, State { name: "stop" PropertyChanges { ... } } ] } 状态的改变由分配⼀个元素新的状态属性名来完成。 注意 另⼀种切换属性的⽅法是使⽤状态元素的when属性。when属性能够被设置 为⼀个表达式的结果,当结果为true时,状态被使⽤。 状态与过渡(States and Transitions) 5.2.1 状态(States) 《QmlBook》In Chinese 126状态与过渡(States and Transitions) Item { id: root states: [ ... ] Button { id: goButton ... onClicked: root.state = "go" } } 例如⼀个交通信号灯有两个信号灯。上⾯的⼀个信号灯使⽤红⾊,下⾯的信 号灯使⽤绿⾊。在这个例⼦中,两个信号灯不会同时发光。让我们看看状态 图。 当系统启动时,它会⾃动切换到停⽌模式作为默认状态。停⽌状态改变了 light1为红⾊并且light2为⿊⾊(关闭)。⼀个外部的事件能够触发现在的状 态变换为“go”状态。在go状态下,我们改变颜⾊属性,light1变为⿊⾊(关 闭),light2变为绿⾊。 《QmlBook》In Chinese 127状态与过渡(States and Transitions) 为了实现这个⽅案,我们给这两个灯绘制⼀个⽤户界⾯的草图,为了简单起 ⻅,我们使⽤两个包含园边的矩形框,设置园半径为宽度的⼀半(宽度与⾼ 度相同)。 Rectangle { id: light1 x: 25; y: 15 width: 100; height: width radius: width/2 color: "black" } Rectangle { id: light2 x: 25; y: 135 width: 100; height: width radius: width/2 color: "black" } 就像在状态图中定义的⼀样,我们有⼀个“go”状态和⼀个“stop”状态,它们将 会分别将交通灯改变为红⾊和绿⾊。我们设置state属性到stop来确保初始化 状态为stop状态。 注意 我们可以只使⽤“go”状态来达到同样的效果,设置颜⾊light1为红⾊,颜⾊ light2为⿊⾊。初始化状态“”(空字符串)定义初始化属性,并且扮演类 似“stop”状态的⾓⾊。 state: "stop" states: [ State { name: "stop" PropertyChanges { target: light1; color: "red" } PropertyChanges { target: light2; color: "black" } 《QmlBook》In Chinese 128状态与过渡(States and Transitions) }, State { name: "go" PropertyChanges { target: light1; color: "black" } PropertyChanges { target: light2; color: "green" } } ] PropertyChanges{ target: light2; color: "black" }在这个例⼦中不是必要的, 因为light2初始化颜⾊已经是⿊⾊了。在⼀个状态中,只需要描述属性如何从 它们的默认状态改变(⽽不是前⼀个状态的改变)。 使⽤⿏标区域覆盖整个交通灯,并且绑定在点击时切换go和stop状态。 MouseArea { anchors.fill: parent onClicked: parent.state = (parent.state == "stop"? "go" : "stop") } 《QmlBook》In Chinese 129状态与过渡(States and Transitions) 我们现在已经成功实现了交通灯的状态切换。为了让⽤户界⾯看起来更加⾃ 然,我们需要使⽤动画效果来增加⼀些过渡。⼀个过渡能够被状态的改变触 发。 注意 可以使⽤⼀个简单逻辑的脚本来替换QML状态。开发⼈员很容易落⼊这种陷 阱,写的代码更像⼀个JavaScript程序⽽不是⼀个QML程序。 ⼀系列的过渡能够被加⼊任何元素,⼀个过渡由状态的改变触发执⾏。你可 以使⽤属性的from:和to:来定义状态改变的指定过渡。这两个属性就像⼀个过 滤器,当过滤器为true时,过渡⽣效。你也可以使⽤“”来表⽰任何状态。例如 from:""; to:"*"表⽰从任⼀状态到另⼀个任⼀状态的默认值,这意味着过渡⽤ 于每个状态的切换。 5.2.2 过渡(Transitions) 《QmlBook》In Chinese 130状态与过渡(States and Transitions) 在这个例⼦中,我们期望从状态“go”到“stop”转换时实现⼀个颜⾊改变的动 画。对于从“stop”到“go”状态的改变,我们期望保持颜⾊的直接改变,不使⽤ 过渡。我们使⽤from和to来限制过渡只在从“go”到“stop”时⽣效。在过渡中我 们给每个灯添加两个颜⾊的动画,这个动画将按照状态的描述来改变属性。 transitions: [ Transition { from: "stop"; to: "go" ColorAnimation { target: light1; properties: "color"; duration: 2000 } ColorAnimation { target: light2; properties: "color"; duration: 2000 } } ] 你可以点击⽤户界⾯来改变状态。试试点击⽤户界⾯,当状态 从“stop”到“go”时,你将会发现改变⽴刻发⽣了。 接下来,你可以修改下这个例⼦,例如缩⼩未点亮的等来突出点亮的等。为 此,你需要在状态中添加⼀个属性⽤来缩放,并且操作⼀个动画来播放缩放 属性的过渡。另⼀个选择是可以添加⼀个“attention”状态,灯会出现⻩⾊闪 烁,为此你需要添加为这个过渡添加⼀个⼀秒连续的动画来显⽰⻩⾊(使 ⽤“to”属性来实现,⼀秒后变为⿊⾊)。也许你也可以改变缓冲曲线来使这个 例⼦更加⽣动。 《QmlBook》In Chinese 131状态与过渡(States and Transitions) 后续添加。 ⾼级⽤法(Advanced Techniques) 《QmlBook》In Chinese 132⾼级⽤法(Advanced Techniques) 注意 最后⼀次构建:2014年1⽉20⽇下午18:00。 这章的源代码能够在assetts folder找到。 在QtQuick中,数据通过model-view(模型-视图)分离。对于每个view(视 图),每个数据元素的可视化都分给⼀个代理(delegate)。QtQuick附带了 ⼀组预定义的模型与视图。想要使⽤这个系统,必须理解这些类,并且知道 如何创建合适的代理来获得正确的显⽰和交互。 模型-视图-代理(Model-View-Delegate) 《QmlBook》In Chinese 133模型-视图-代理(Model-View-Delegate) 对于开发⽤户界⾯,最重要的⼀⽅⾯是保持数据与可视化的分离。例如,⼀ 个电话薄可以使⽤⼀个垂直⽂本链表排列或者使⽤⼀个⺴格联系⼈图⽚排 列。在这两个案例中,数据都是相同的,但是可视化效果却是不同的。这种 ⽅法通常被称作model-view(模型-视图)模式。在这种模式中,数据通常被 称作model(模型),可视化处理称作view(视图)。 在QML中,model(模型)与view(视图)都通过delegate(代理)连接起 来。功能划分如下,model(模型)提供数据。对于每个数据项,可能有多 个值。在上⾯的电话薄例⼦中,每个电话薄条⺫对应⼀个名字,⼀个图⽚和 ⼀个号码。显⽰在view(视图)中的每项数据,都是通过delegate(代理)来 实现可视化。view(视图)的任务是排列这些delegate(代理),每个 delegate(代理)将model item(模型项)的值显⽰给⽤户。 概念(Concept) 《QmlBook》In Chinese 134概念(Concept) 最基本的分离数据与显⽰的⽅法是使⽤Repeater元素。它被⽤于实例化⼀组 元素项,并且很容易与⼀个⽤于填充⽤户界⾯的定位器相结合。 最基本的实现举例,repeater元素⽤于实现⼦元素的标号。每个⼦元素都拥 有⼀个可以访问的属性index,⽤于区分不同的⼦元素。在下⾯的例⼦中,⼀ 个repeater元素创建了10个⼦项,⼦项的数量由model属性控制。对于每个⼦ 项Rectangle包含了⼀个Text元素,你可以将text属性设置为index的值,因此 可以看到⼦项的编号是0~9。 import QtQuick 2.0 Column { spacing: 2 Repeater { model: 10 Rectangle { width: 100 height: 20 radius: 3 color: "lightBlue" Text { anchors.centerIn: parent text: index } } } } 基础模型(Basic Model) 《QmlBook》In Chinese 135基础模型(Basic Model) 这是⼀个不错的编号列表,有时我们想显⽰⼀些更复杂的数据。使⽤⼀个 JavaScript序列来替换整形变量model的值可以达到我们的⺫的。序列可以使 ⽤任何类型的内容,可以是字符串,整数,或者对象。在下⾯的例⼦中,使 ⽤了⼀个字符串链表。我们仍然使⽤index的值作为变量,并且我们也访问 modelData中包含的每个元素的数据。 import QtQuick 2.0 Column { spacing: 2 Repeater { model: ["Enterprise", "Colombia", "Challenger", "Discovery", "Endeavour", "Atlantis"] Rectangle { width: 100 height: 20 radius: 3 color: "lightBlue" Text { anchors.centerIn: parent text: index +": "+modelData } } } } 《QmlBook》In Chinese 136基础模型(Basic Model) 将数据暴露成⼀组序列,你可以通过标号迅速的找到你需要的信息。想象⼀ 下这个模型的草图,这是⼀个最简单的模型,也是通常都会使⽤的模型, ListModel(链表模型)。⼀个链表模型由许多ListElement(链表元素)组 成。在每个链表元素中,可以绑定值到属性上。例如在下⾯这个例⼦中,每 个元素都提供了⼀个名字和⼀个颜⾊。 每个元素中的属性绑定连接到repeater实例化的⼦项上。这意味着变量name 和surfaceColor可以被repeater创建的每个Rectangle和Text项引⽤。这不仅 可以⽅便的访问数据,也可以使源代码更加容易阅读。surfaceColor是名字 左边圆的颜⾊,⽽不是模糊的数据序列列i或者⾏j。 import QtQuick 2.0 《QmlBook》In Chinese 137基础模型(Basic Model) Column { spacing: 2 Repeater { model: ListModel { ListElement { name: "Mercury"; surfaceColor: "gray" } ListElement { name: "Venus"; surfaceColor: "yellow" } ListElement { name: "Earth"; surfaceColor: "blue" } ListElement { name: "Mars"; surfaceColor: "orange" } ListElement { name: "Jupiter"; surfaceColor: "orange" } ListElement { name: "Saturn"; surfaceColor: "yellow" } ListElement { name: "Uranus"; surfaceColor: "lightBlue" } ListElement { name: "Neptune"; surfaceColor: "lightBlue" } } Rectangle { width: 100 height: 20 radius: 3 color: "lightBlue" Text { anchors.centerIn: parent text: name } Rectangle { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 2 width: 16 height: 16 radius: 8 border.color: "black" border.width: 1 《QmlBook》In Chinese 138基础模型(Basic Model) color: surfaceColor } } } } 《QmlBook》In Chinese 139基础模型(Basic Model) repeater的内容的每个⼦项实例化时绑定了默认的属性delegate(代理)。这 意味着例1(第⼀个代码段)的代码与下⾯显⽰的代码是相同的。注意,唯⼀ 的不同是delegate属性名,将会在后⾯详细讲解。 import QtQuick 2.0 Column { spacing: 2 Repeater { model: 10 delegate: Rectangle { width: 100 height: 20 radius: 3 color: "lightBlue" Text { anchors.centerIn: parent text: index } } } } 《QmlBook》In Chinese 140基础模型(Basic Model) Repeater元素适合有限的静态数据,但是在真正使⽤时,模型通常更加复杂 和庞⼤,我们需要⼀个更加智能的解决⽅案。QtQuick提供了ListView和 GridView元素,这两个都是基于Flickable(可滑动)区域的元素,因此⽤户 可以放⼊更⼤的数据。同时,它们限制了同时实例化的代理数量。对于⼀个 ⼤型的模型,这意味着在同⼀个场景下只会加载有限的元素。 动态视图(Dynamic Views) 《QmlBook》In Chinese 141动态视图(Dynamic Views) 《QmlBook》In Chinese 142动态视图(Dynamic Views) 这两个元素的⽤法⾮常类似,我们由ListView开始,然后会描述GridView的 模型起点来进⾏⽐较。 ListView与Repeater元素像素,它使⽤了⼀个model,使⽤delegate来实例 化,并且在两个delegate之间能够设置间隔sapcing。下⾯的列表显⽰了怎样 《QmlBook》In Chinese 143动态视图(Dynamic Views) 设置⼀个简单的链表。 import QtQuick 2.0 Rectangle { width: 80 height: 300 color: "white" ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 100 delegate: numberDelegate spacing: 5 } Component { id: numberDelegate Rectangle { width: 40 height: 40 color: "lightGreen" Text { anchors.centerIn: parent font.pixelSize: 10 text: index } } } } 《QmlBook》In Chinese 144动态视图(Dynamic Views) 如果模型包含的数据⽐屏幕上显⽰的更多,ListView元素只会显⽰部分的链 表内容。然后由于QtQuick的默认⾏为导致的问题,列表视图不会限制被显⽰ 的代理项(delegates)只在限制区域内显⽰。这意味着代理项可以在列表视 图外显⽰,⽤户可以看⻅在列表视图外动态的创建和销毁这些代理项 《QmlBook》In Chinese 145动态视图(Dynamic Views) (delegates)。为了防⽌这个问题,ListView通过设置clip属性为true,来激 活裁剪功能。下⾯的图⽚展⽰了这个结果,左边是clip属性设置为false的对 ⽐。 对于⽤户,ListView(列表视图)是⼀个滚动区域。它⽀持惯性滚动,这意 《QmlBook》In Chinese 146动态视图(Dynamic Views) 味着它可以快速的翻阅内容。默认模式下,它可以在内容最后继续伸展,然 后反弹回去,这个信号告诉⽤户已经到达内容的末尾。 视图末尾的⾏为是由到boundsBehavior属性的控制的。这是⼀个枚举值,并 且可以配置为默认的Flickable.DragAndOvershootBounds,视图可以通过它 的边界线来拖拽和翻阅,配置为Flickable.StopAtBounds,视图将不再可以移 动到它的边界线之外。配置为Flickable.DragOverBounds,⽤户可以将视图 拖拽到它的边界线外,但是在边界线上翻阅将⽆效。 使⽤snapMode属性可以限制⼀个视图内元素的停⽌位置。默认⾏为下是 ListView.NoSnap,允许视图内元素在任何位置停⽌。将snapMode属性设置 为ListView.SnapToItem,视图顶部将会与元素对象的顶部对⻬排列。使⽤ ListView.SnapOneItem,当⿏标或者触摸释放时,视图将会停⽌在第⼀个可 ⻅的元素,这种模式对于浏览⻚⾯⾮常便利。 默认的链表视图只提供了⼀个垂直⽅向的滚动条,但是⽔平滚动条也是需要 的。链表视图的⽅向由属性orientation控制。它能够被设置为默认值 ListView.Vertical或者ListView.Horizontal。下⾯是⼀个⽔平链表视图。 import QtQuick 2.0 Rectangle { width: 480 height: 80 color: "white" ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 100 6.3.1 ⽅向(Orientation) 《QmlBook》In Chinese 147动态视图(Dynamic Views) orientation: ListView.Horizontal delegate: numberDelegate spacing: 5 } Component { id: numberDelegate Rectangle { width: 40 height: 40 color: "lightGreen" Text { anchors.centerIn: parent font.pixelSize: 10 text: index } } } } 按照上⾯的设置,⽔平链表视图默认的元素顺序⽅向是由左到右。可以通过 设置layoutDirection属性来控制元素顺序⽅向,它可以设置为Qt.LeftToRight 或者Qt.RightToLeft。 当使⽤基于触摸⽅式的链表视图时,默认提供的视图已经⾜够使⽤。在使⽤ 6.3.2 键盘导航和⾼亮 《QmlBook》In Chinese 148动态视图(Dynamic Views) 键盘甚⾄仅仅通过⽅向键选择⼀个元素的场景下,需要有标识当前选中元素 的机制。在QML中,这被叫做⾼亮。 视图⽀持设置⼀个当前视图中显⽰代理元素中的⾼亮代理。它是⼀个附加的 代理元素,这个元素仅仅只实例化⼀次,并移动到与当前元素相同的位置。 在下⾯例⼦的演⽰中,有两个属性来完成这个⼯作。⾸先是focus属性设置为 true,它设置链表视图能够获得键盘焦点。然后是highlight属性,指出使⽤的 ⾼亮代理元素。⾼亮代理元素的x,y与height属性由当前元素指定。如果宽度 没有特别指定,当前元素的宽度也可以⽤于⾼亮代理元素。 在例⼦中,ListView.view.width属性被绑定⽤于⾼亮元素的宽度。关于代理元 素的使绑定属性将在后⾯的章节讨论,但是最好知道相同的绑定属性也可以 ⽤于⾼亮代理元素。 import QtQuick 2.0 Rectangle { width: 240 height: 300 color: "white" ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 100 delegate: numberDelegate spacing: 5 highlight: highlightComponent focus: true } Component { 《QmlBook》In Chinese 149动态视图(Dynamic Views) id: highlightComponent Rectangle { width: ListView.view.width color: "lightGreen" } } Component { id: numberDelegate Item { width: 40 height: 40 Text { anchors.centerIn: parent font.pixelSize: 10 text: index } } } } // M1>> 《QmlBook》In Chinese 150动态视图(Dynamic Views) 当使⽤⾼亮与链表视图(ListView)结合时,⼀些属性可以⽤来控制它的⾏ 为。highlightRangeMode控制了⾼亮如何影响视图中当前的显⽰。默认设置 ListView.NoHighLighRange意味着⾼亮与视图中的元素距离不相关。 ListView.StrictlyEnforceRnage确保了⾼亮始终可⻅,如果某个动作尝试将⾼ 亮移出当前视图可⻅范围,当前元素将会⾃动切换,确保了⾼亮始终可⻅。 《QmlBook》In Chinese 151动态视图(Dynamic Views) ListView.ApplyRange,它尝试保持⾼亮代理始终可⻅,但是不会强制切换当 前元素始终可⻅。如果在需要的情况下⾼亮代理允许被移出当前视图。 在默认配置下,视图负责⾼亮移动到指定位置,移动的速度与⼤⼩的改变能 够被控制,使⽤⼀个速度值或者⼀个动作持续时间来完成它。这些属性包括 highlightMoveSpeed,highlightMoveDuration,highlightResizeSpeed和 highlightResizeDuration。默认下速度被设置为每秒400像素,动作持续时间 为-1,表明速度和距离控制了动作的持续时间。如果速度与动作持续时间都 被设置,动画将会采⽤速度较快的结果来完成。 为了更加详细的控制⾼亮的移动,highlightFollowCurrentItem属性设置为 false。这意味着视图将不再负责⾼亮代理的移动。取⽽代之可以通过⼀个⾏ 为(Bahavior)或者⼀个动画来控制它。 在下⾯的例⼦中,⾼亮代理的y坐标属性与ListView.view.currentItem.y属性绑 定。这确保了⾼亮始终跟随当前元素。然⽽,由于我们没有让视图来移动这 个⾼亮代理,我们需要控制这个元素如何移动,通过Behavior on y来完成这 个操作,在下⾯的例⼦中,移动分为三步完成:淡出,移动,淡⼊。注意怎 样使⽤SequentialAnimation和PropertyAnimation元素与NumberAnimation结 合创建更加复杂的移动效果。 Component { id: highlightComponent Item { width: ListView.view.width height: ListView.view.currentItem.height y: ListView.view.currentItem.y Behavior on y { SequentialAnimation { PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 0; duration: 200 } NumberAnimation { duration: 1 } PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 1; duration: 200 } } } 《QmlBook》In Chinese 152动态视图(Dynamic Views) Rectangle { id: highlightRectangle anchors.fill: parent color: "lightGreen" } } } 这⼀节是链表视图最后的内容,我们能够向链表视图中插⼊⼀个⻚眉 (header)元素和⼀个⻚脚(footer)元素。这部分是链表的开始或者结尾 处被作为代理元素特殊的区域。对于⼀个⽔平链表视图,不会存在⻚眉或者 ⻚脚,但是也有开始和结尾处,这取决于layoutDirection的设置。 下⾯这个例⼦展⽰了如何使⽤⼀个⻚眉和⻚脚来突出链表的开始与结尾。这 些特殊的链表元素也有其它的作⽤,例如,它们能够保持链表中的按键加载 更多的内容。 import QtQuick 2.0 Rectangle { width: 80 height: 300 color: "white" ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 4 6.3.3 ⻚眉与⻚脚(Header and Footer) 《QmlBook》In Chinese 153动态视图(Dynamic Views) delegate: numberDelegate spacing: 5 header: headerComponent footer: footerComponent } Component { id: headerComponent Rectangle { width: 40 height: 20 color: "yellow" } } Component { id: footerComponent Rectangle { width: 40 height: 20 color: "red" } } Component { id: numberDelegate Rectangle { width: 40 height: 40 color: "lightGreen" Text { anchors.centerIn: parent font.pixelSize: 10 《QmlBook》In Chinese 154动态视图(Dynamic Views) text: index } } } } 注意 ⻚眉与⻚脚代理元素不遵循链表视图(ListView)的间隔(spacing)属 性,它们被直接放在相邻的链表元素之上或之下。这意味着⻚眉与⻚脚的间 隔必须通过⻚眉与⻚脚元素⾃⼰设置。 《QmlBook》In Chinese 155动态视图(Dynamic Views) 使⽤⺴格视图(GridView)与使⽤链表视图(ListView)的⽅式⾮常类似。 真正不同的地⽅是⺴格视图(GridView)使⽤了⼀个⼆维数组来存放元素, ⽽链表视图(ListView)是使⽤的线性链表来存放元素。 6.3.4 ⺴格视图(The GridView) 《QmlBook》In Chinese 156动态视图(Dynamic Views) 与链表视图(ListView)⽐较,⺴格视图(GridView)不依赖于元素间隔和 ⼤⼩来配置元素。它使⽤单元宽度(cellWidth)与单元⾼度(cellHeight)属 性来控制数组内的⼆维元素的内容。每个元素从左上⾓开始依次放⼊单元 格。 《QmlBook》In Chinese 157动态视图(Dynamic Views) import QtQuick 2.0 Rectangle { width: 240 height: 300 color: "white" GridView { anchors.fill: parent anchors.margins: 20 clip: true model: 100 cellWidth: 45 cellHeight: 45 delegate: numberDelegate } Component { id: numberDelegate Rectangle { width: 40 height: 40 color: "lightGreen" Text { anchors.centerIn: parent font.pixelSize: 10 text: index } } } } 《QmlBook》In Chinese 158动态视图(Dynamic Views) ⼀个⺴格视图(GridView)也包含了⻚脚与⻚眉,也可以使⽤⾼亮代理并且 ⽀持捕捉模式(snap mode)的多种反弹⾏为。它也可以使⽤不同的⽅向 (orientations)与定向(directions)来定位。 定向使⽤flow属性来控制。它可以被设置为GridView.LeftToRight或者 GridView.TopToBottom。模型的值从左往右向⺴格中填充,⾏添加是从上往 下。视图使⽤⼀个垂直⽅向的滚动条。后⾯添加的元素也是由上到下,由左 到右。 此外还有flow属性和layoutDirection属性,能够适配⺴格从左到右或者从右到 左,这依赖于你使⽤的设置值。 《QmlBook》In Chinese 159动态视图(Dynamic Views) 当使⽤模型与视图来⾃定义⽤户界⾯时,代理在创建显⽰时扮演了⼤量的⾓ ⾊。在模型中的每个元素通过代理来实现可视化,⽤户真实可⻅的是这些代 理元素。 每个代理访问到索引号或者绑定的属性,⼀些是来⾃数据模型,⼀些来⾃视 图。来⾃模型的数据将会通过属性传递到代理。来⾃视图的数据将会通过属 性传递视图中与代理相关的状态信息。 通常使⽤的视图绑定属性是ListView.isCurrentItem和ListView.view。第⼀个 是⼀个布尔值,标识这个元素是否是视图当前元素,这个值是只读的,引⽤ ⾃当前视图。通过访问视图,可以创建可复⽤的代理,这些代理在被包含时 会⾃动匹配视图的⼤⼩。在下⾯这个例⼦中,每个代理的width(宽度)属性 与视图的width(宽度)属性绑定,每个代理的背景颜⾊color依赖于绑定的属 性ListView.isCurrentItem属性。 import QtQuick 2.0 Rectangle { width: 120 height: 300 color: "white" ListView { anchors.fill: parent anchors.margins: 20 clip: true model: 100 delegate: numberDelegate spacing: 5 代理(Delegate) 《QmlBook》In Chinese 160代理(Delegate) focus: true } Component { id: numberDelegate Rectangle { width: ListView.view.width height: 40 color: ListView.isCurrentItem?"gray":"lightGray" Text { anchors.centerIn: parent font.pixelSize: 10 text: index } } } } 《QmlBook》In Chinese 161代理(Delegate) 如果在模型中的每个元素与⼀个动作相关,例如点击作⽤于⼀个元素时,这 个功能是代理完成的。这是由事件管理分配给视图的,这个操作控制了视图 中元素的导航,代理控制了特定元素上的动作。 《QmlBook》In Chinese 162代理(Delegate) 最基础的⽅法是在每个代理中创建⼀个MouseArea(⿏标区域)并且响应 onClicked信号。在后⾯章节中将会演⽰这个例⼦。 在某些情况下,视图中的显⽰内容会随着时间⽽改变。由于模型数据的改 变,元素会添加或者移除。在这些情况下,⼀个⽐较好的做法是使⽤可视化 队列给⽤户⼀个⽅向的感觉来帮助⽤户知道哪些数据被加⼊或者移除。 为了⽅便使⽤,QML视图为每个代理绑定了两个信号,onAdd和 onRemove。使⽤动画连接它们,可以⽅便创建识别哪些内容被添加或删除 的动画。 下⾯这个例⼦演⽰了如何动态填充⼀个链表模型(ListModel)。在屏幕下 ⽅,有⼀个添加新元素的按钮。当点击它时,会调⽤模型的append⽅法来添 加⼀个新的元素。这个操作会触发视图创建⼀个新的代理,并发送 GridView.onAdd信号。SequentialAnimation队列动画与这个信号连接绑定, 使⽤代理的scale属性来放⼤视图元素。 当视图中的⼀个代理点击时,将会调⽤模型的remove⽅法将⼀个元素从模型 中移除。这个操作将会导致GridView.onRemove信号的发送,触发另⼀个 SequentialAnimation。这时,代理的销毁将会延迟直到动画完成。为了完成 这个操作,PropertyAction元素需要在动画前设置GridView.delayRemove属 性为true,并在动画后设置为false。这样确保了动画在代理项移除前完成。 import QtQuick 2.0 Rectangle { width: 480 height: 300 color: "white" ListModel { 6.4.1 动画添加与移除元素(Animating Added and Removed Items) 《QmlBook》In Chinese 163代理(Delegate) id: theModel ListElement { number: 0 } ListElement { number: 1 } ListElement { number: 2 } ListElement { number: 3 } ListElement { number: 4 } ListElement { number: 5 } ListElement { number: 6 } ListElement { number: 7 } ListElement { number: 8 } ListElement { number: 9 } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 20 height: 40 color: "darkGreen" Text { anchors.centerIn: parent text: "Add item!" } MouseArea { anchors.fill: parent onClicked: { theModel.append({"number": ++parent.count}); } } property int count: 9 } GridView { 《QmlBook》In Chinese 164代理(Delegate) anchors.fill: parent anchors.margins: 20 anchors.bottomMargin: 80 clip: true model: theModel cellWidth: 45 cellHeight: 45 delegate: numberDelegate } Component { id: numberDelegate Rectangle { id: wrapper width: 40 height: 40 color: "lightGreen" Text { anchors.centerIn: parent font.pixelSize: 10 text: number } MouseArea { anchors.fill: parent onClicked: { if (!wrapper.GridView.delayRemove) theModel.remove(index); } } 《QmlBook》In Chinese 165代理(Delegate) GridView.onRemove: SequentialAnimation { PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: true } NumberAnimation { target: wrapper; property: "scale"; to: 0; duration: 250; easing.type: Easing.InOutQuad } PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: false } } GridView.onAdd: SequentialAnimation { NumberAnimation { target: wrapper; property: "scale"; from: 0; to: 1; duration: 250; easing.type: Easing.InOutQuad } } } } } 在使⽤链表时通常会使⽤当前项激活时展开的机制。这个操作可以被⽤于动 态的将当前项⺫填充到整个屏幕来添加⼀个新的⽤户界⾯,或者为链表中的 当前项提供更多的信息。 在下⾯的例⼦中,当点击链表项时,链表项都会展开填充整个链表视图 (ListView)。额外的间隔区域被⽤于添加更多的信息,这种机制使⽤⼀个 状态来控制,当⼀个链表项展开时,代理项都能输⼊expanded(展开)状 态,在这种状态下⼀些属性被改变。 ⾸先,包装器(wrapper)的⾼度(height)被设置为链表视图(ListView) 的⾼度。标签图⽚被放⼤并且下移,使图⽚从⼩图⽚的位置移向⼤图⽚的位 置。除了这些之外,两个隐藏项,实际视图(factsView)与关闭按键 (closeButton)切换它的opactiy(透明度)显⽰出来。最后设置链表视图 (ListView)。 设置链表视图(ListView)包含了设置内容Y坐标(contentsY),这是视图 顶部可⻅的部分代理的Y轴坐标。另⼀个变化是设置视图的交互 (interactive)为false。这个操作阻⽌了视图的移动,⽤户不再能够通过滚 6.4.2 形变的代理(Shape-Shifting Delegates) 《QmlBook》In Chinese 166代理(Delegate) 动条切换当前项。 由于设置第⼀个链表项为可点击,向它输⼊⼀个expanded(展开)状态,导 致了它的代理项被填充到整个链表并且内容重置。当点击关闭按钮时,清空 状态,导致它的代理项返回上⼀个状态,并且重新设置链表视图 (ListView)有效。 import QtQuick 2.0 Item { width: 300 height: 480 ListView { id: listView anchors.fill: parent delegate: detailsDelegate model: planets } ListModel { id: planets ListElement { name: "Mercury"; imageSource: "images/mercury.jpeg"; facts: "Mercury is the smallest planet in the Solar System. It is the closest planet to the sun. It makes one trip around the Sun once every 87.969 days." } ListElement { name: "Venus"; imageSource: "images/venus.jpeg"; facts: "Venus is the second planet from the Sun. It is a terrestrial planet because it has a solid, rocky surface. The other terrestrial planets are Mercury, Earth and Mars. Astronomers have known Venus for thousands of years." } ListElement { name: "Earth"; imageSource: "images/earth.jpeg"; facts: "The Earth is the third planet from the Sun. It is one of the four terrestrial planets in our Solar System. This means most of its mass is solid. The other three are Mercury, Venus and Mars. The Earth is also called the Blue Planet, 'Planet Earth', and 'Terra'." } ListElement { name: "Mars"; imageSource: "images/mars.jpeg"; facts: "Mars is the fourth planet from the Sun in the Solar System. Mars is dry, rocky and cold. It is home to the largest volcano in the Solar System. Mars is named after the mythological Roman god of war because it is a red planet, which signifies the colour of blood." } } Component { id: detailsDelegate Item { id: wrapper width: listView.width height: 30 《QmlBook》In Chinese 167代理(Delegate) Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top height: 30 color: "#ffaa00" Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter font.pixelSize: parent.height-4 text: name } } Rectangle { id: image color: "black" anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: 2 anchors.topMargin: 2 width: 26 height: 26 Image { anchors.fill: parent fillMode: Image.PreserveAspectFit source: imageSource } } MouseArea { 《QmlBook》In Chinese 168代理(Delegate) anchors.fill: parent onClicked: parent.state = "expanded" } Item { id: factsView anchors.top: image.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom opacity: 0 Rectangle { anchors.fill: parent color: "#cccccc" Text { anchors.fill: parent anchors.margins: 5 clip: true wrapMode: Text.WordWrap font.pixelSize: 12 text: facts } } } Rectangle { id: closeButton anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: 2 anchors.topMargin: 2 width: 26 《QmlBook》In Chinese 169代理(Delegate) height: 26 color: "red" opacity: 0 MouseArea { anchors.fill: parent onClicked: wrapper.state = "" } } states: [ State { name: "expanded" PropertyChanges { target: wrapper; height: listView.height } PropertyChanges { target: image; width: listView.width; height: listView.width; anchors.rightMargin: 0; anchors.topMargin: 30 } PropertyChanges { target: factsView; opacity: 1 } PropertyChanges { target: closeButton; opacity: 1 } PropertyChanges { target: wrapper.ListView.view; contentY: wrapper.y; interactive: false } } ] transitions: [ Transition { NumberAnimation { duration: 200; properties: "height,width,anchors.rightMargin,anchors.topMargin,opacity,contentY" } } ] } } } 《QmlBook》In Chinese 170代理(Delegate) 《QmlBook》In Chinese 171代理(Delegate) 《QmlBook》In Chinese 172代理(Delegate) 《QmlBook》In Chinese 173代理(Delegate) 这个技术展⽰了展开代理来填充视图能够简单的通过代理的形变来完成。例 如当浏览⼀个歌曲的链表时,可以通过放⼤当前项来对该项添加更多的说 明。 《QmlBook》In Chinese 174代理(Delegate) 路径视图(PathView)⾮常强⼤,但也⾮常复杂,这个视图由QtQuick提 供。它创建了⼀个可以让⼦项沿着任意路径移动的视图。沿着相同的路径, 使⽤缩放(scale),透明(opacity)等元素可以更加详细的控制过程。 当使⽤路径视图(PathView)时,你必须定义⼀个代理和⼀个路径。在这些 之上,路径视图(PathView)本⾝也可以⾃定义⼀些属性的区间。通常会使 ⽤pathItemCount属性,它控制了⼀次可⻅的⼦项总数。 preferredHighLightBegin属性控制了⾼亮区间,preferredHighlightEnd与 highlightRangeMode,控制了当前项怎样沿着路径显⽰。 在关注⾼亮区间之前,我们必须先看看路径(path)这个属性。路径 (path)属性使⽤⼀个路径(path)元素来定义路径视图(PathView)内代 理的滚动路径。路径使⽤startx与starty属性来链接路径(path)元素,例如 PathLine,PathQuad和PathCubic。这些元素都使⽤⼆维数组来构造路径。 当路径定义好之后,可以使⽤PathPercent和PathAttribute元素来进⼀步设 置。它们被放置在路径元素之间,并且为经过它们的路径和代理提供更加细 致的控制。PathPercent提供了如何控制每个元素之间覆盖区域部分的路径, 然后反过来控制分布在这条路径上的代理元素,它们被按⽐例的分布播放。 preferredHightlightBegin与preferredHighlightEnd属性由PathView(路径视 图)输⼊到图⽚元素中。它们的值在0~1之间。结束值⼤于等于开始值。例 如设置这些属性值为0.5,当前项只会显⽰当前百分之50的图像在这个路径 上。 在Path中,PathAttribute元素也是被放置在元素之间的,就像PathPercent元 素。它们可以让你指定属性的值然后插⼊的路径中去。这些属性与代理绑定 可以⽤来控制任意的属性。 ⾼级⽤法(Advanced Techniques) 6.5.1 路径视图(The PathView) 《QmlBook》In Chinese 175⾼级⽤法(Advanced Techniques) 下⾯这个例⼦展⽰了路径视图(PathView)如何创建⼀个卡⽚视图,并且⽤ 户可以滑动它。我们使⽤了⼀些技巧来完成这个例⼦。路径由PathLine元素 组成。使⽤PathPercent元素,它确保了中间的元素居中,并且给其它的元素 提供了⾜够的空间。使⽤PathAttribute元素来控制旋转,⼤⼩和深度值(z- value)。 《QmlBook》In Chinese 176⾼级⽤法(Advanced Techniques) 在这个路径之上(path),需要设置路径视图(PathView)的 pathItemCount属性。它控制了路径的浓密度。路径视图的路径 (PathView.onPath)使⽤preferredHighlightBegin与preferredHighlightEnd 来控制可⻅的代理项。 PathView { anchors.fill: parent delegate: flipCardDelegate model: 100 path: Path { startX: root.width/2 startY: 0 PathAttribute { name: "itemZ"; value: 0 } PathAttribute { name: "itemAngle"; value: -90.0; } PathAttribute { name: "itemScale"; value: 0.5; } PathLine { x: root.width/2; y: root.height*0.4; } PathPercent { value: 0.48; } PathLine { x: root.width/2; y: root.height*0.5; } PathAttribute { name: "itemAngle"; value: 0.0; } PathAttribute { name: "itemScale"; value: 1.0; } PathAttribute { name: "itemZ"; value: 100 } PathLine { x: root.width/2; y: root.height*0.6; } PathPercent { value: 0.52; } PathLine { x: root.width/2; y: root.height; } PathAttribute { name: "itemAngle"; value: 90.0; } PathAttribute { name: "itemScale"; value: 0.5; } PathAttribute { name: "itemZ"; value: 0 } } pathItemCount: 16 preferredHighlightBegin: 0.5 preferredHighlightEnd: 0.5 } 《QmlBook》In Chinese 177⾼级⽤法(Advanced Techniques) 代理如下⾯所⽰,使⽤了⼀些从PathAttribute中链接的属性, itemZ,itemAngle和itemScale。需要注意代理链接的属性只在wrapper中可 ⽤。因此,rotxs属性在Rotation元素中定义为可访问值。 另⼀个需要注意的是路径视图(PathView)链接的PathView.onPath属性的 ⽤法。通常对于这个属性都绑定为可⻅,这样允许路径视图(PathView)缓 冲不可⻅的元素。这不是通过剪裁处理来实现的,因为路径视图 (PathView)的代理⽐其它的视图,例如链表视图(ListView)或者栅格视 图(GridView)放置更加随意。 Component { id: flipCardDelegate Item { id: wrapper width: 64 height: 64 visible: PathView.onPath scale: PathView.itemScale z: PathView.itemZ property variant rotX: PathView.itemAngle transform: Rotation { axis { x: 1; y: 0; z: 0 } angle: wrapper.rotX; origin { x: 32; y: 32; } } Rectangle { anchors.fill: parent color: "lightGray" border.color: "black" border.width: 3 } Text { anchors.centerIn: parent text: index font.pixelSize: 30 } 《QmlBook》In Chinese 178⾼级⽤法(Advanced Techniques) } } 当在路径视图(PathView)上使⽤图像转换或者其它更加复杂的元素时,有 ⼀个性能优化的技巧是绑定图像元素(Image)的smooth属性与 PathView.view.moving属性。这意味着图像在移动时可能不够完美,但是能 够⽐较平滑的转换。当视图在移动时,对于平滑缩放的处理是没有意义的, 因为⽤户根本看不⻅这个过程。 由于XML是⼀种常⻅的数据格式,QML提供了XmlListModel元素来包装XML 数据。这个元素能够获取本地或者⺴络上的XML数据,然后通过XPath解析 这些数据。 下⾯这个例⼦展⽰了从RSS流中获取图⽚,源属性(source)引⽤了⼀个⺴ 络地址,这个数据会⾃动下载。 6.5.2 XML模型(A Model from XML) 《QmlBook》In Chinese 179⾼级⽤法(Advanced Techniques) 《QmlBook》In Chinese 180⾼级⽤法(Advanced Techniques) 当数据下载完成后,它会被加⼯作为模型的⼦项。查询属性(query)是⼀个 XPath代理的基础查询,⽤来创建模型项。在这个例⼦中,这个路径 是/rss/channel/item,所以,在⼀个模型⼦项创建后,每⼀个⼦项的标签,都 包含了⼀个频道标签,包含⼀个RSS标签。 每⼀个模型项,⼀些规则需要被提取,由XmlRole元素来代理。每⼀个规则 都需要⼀个名称,这样代理才能够通过属性绑定来访问。每个这样的属性的 值都通过XPath查询来确定。例如标题属性(title)符合title/string()查询,返 回内容中在之间的值。 图像源属性(imageSource)更加有趣,因为它不仅仅是从XML中提取字符 串,也需要加载它。在流数据的⽀持下,每个⼦项包含了⼀个图⽚。使⽤ XPath的函数substring-after与substring-before,可以提取本地的图⽚资源。 这样imageSource属性就可以直接被作为⼀个Image元素的source属性使 ⽤。 import QtQuick 2.0 import QtQuick.XmlListModel 2.0 Item { width: 300 height: 480 Component { id: imageDelegate Item { width: listView.width height: 400 Column { Text { text: title } Image { source: imageSource 《QmlBook》In Chinese 181⾼级⽤法(Advanced Techniques) } } } } XmlListModel { id: imageModel source: "http://feeds.nationalgeographic.com/ng/photography/photo-of-the-day/" query: "/rss/channel/item" XmlRole { name: "title"; query: "title/string()" } XmlRole { name: "imageSource"; query: "substring-before(substring-after(description/string(), 'img src=\"'), '\"')" } } ListView { id: listView anchors.fill: parent model: imageModel delegate: imageDelegate } } 有时,链表的数据需要划分段。例如使⽤⾸字⺟来划分联系⼈,或者⾳乐。 使⽤链表视图可以把平⾯列表按类别划分。 6.5.3 链表分段(Lists with Sections) 《QmlBook》In Chinese 182⾼级⽤法(Advanced Techniques) 为了使⽤分段,section.property与section.criteria必须安装。 section.property定义了哪些属性⽤于内容的划分。在这⾥,最重要的是知道 每⼀段由哪些连续的元素构成,否则相同的属性名可能出现在⼏个不同的地 ⽅。 section.criteria能够被设置为ViewSection.FullString或者 ViewSection.FirstCharacter。默认下使⽤第⼀个值,能够被⽤于模型中有清 晰的分段,例如⾳乐专辑。第⼆个是使⽤⼀个属性的⾸字⺟来分段,这说明 任何属性都可以被使⽤。通常的例⼦是⽤于联系⼈名单的姓。 当段被定义好后,每个⼦项能够使⽤绑定属性ListView.section, ListView.previousSection与ListView.nextSection来访问。使⽤这些属性,可 《QmlBook》In Chinese 183⾼级⽤法(Advanced Techniques) 以检测段的第⼀个与最后⼀个⼦项。 使⽤链表视图(ListView)的section.delegate属性可以给段指定代理组件。 它能够创建段标题,并且可以在任意⼦项之前插⼊这个段代理。使⽤绑定属 性section可以访问当前段的名称。 下⾯这个例⼦使⽤国际分类展⽰了分段的⼀些概念。国籍(nation)作为 section.property,段代理组件(section.delegate)使⽤每个国家作为标题。 在每个段中,spacemen模型中的名字使⽤spaceManDelegate组件来代理显 ⽰。 import QtQuick 2.0 Rectangle { width: 300 height: 290 color: "white" ListView { anchors.fill: parent anchors.margins: 20 clip: true model: spaceMen delegate: spaceManDelegate section.property: "nation" section.delegate: sectionDelegate } Component { id: spaceManDelegate Item { width: 260 height: 20 《QmlBook》In Chinese 184⾼级⽤法(Advanced Techniques) Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 10 font.pixelSize: 12 text: name } } } Component { id: sectionDelegate Rectangle { width: 260 height: 20 color: "lightGray" Text { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: 10 font.pixelSize: 12 font.bold: true text: section } } } ListModel { id: spaceMen ListElement { name: "Abdul Ahad Mohmand"; nation: "Afganistan"; } ListElement { name: "Marcos Pontes"; nation: "Brazil"; } ListElement { name: "Alexandar Panayotov Alexandrov"; nation: "Bulgaria"; } 《QmlBook》In Chinese 185⾼级⽤法(Advanced Techniques) ListElement { name: "Georgi Ivanov"; nation: "Bulgaria"; } ListElement { name: "Roberta Bondar"; nation: "Canada"; } ListElement { name: "Marc Garneau"; nation: "Canada"; } ListElement { name: "Chris Hadfield"; nation: "Canada"; } ListElement { name: "Guy Laliberte"; nation: "Canada"; } ListElement { name: "Steven MacLean"; nation: "Canada"; } ListElement { name: "Julie Payette"; nation: "Canada"; } ListElement { name: "Robert Thirsk"; nation: "Canada"; } ListElement { name: "Bjarni Tryggvason"; nation: "Canada"; } ListElement { name: "Dafydd Williams"; nation: "Canada"; } } } ⼀个模型视图的性能很⼤程度上依赖于代理的创建。例如滚动下拉⼀个链表 视图时,代理从外部加⼊到视图底部,并且从视图顶部移出。如果设置剪裁 (clip)属性为false,并且代理项花了很多时间来初始化,⽤户会感觉到视图 滚动体验很差。 为了优化这个问题,你可以在滚动时使⽤像素来调整。使⽤cacheBuffer属 性,在上诉情况下的垂直滚动,它将会调整在链表视图的上下需要预先准备 好多少像素的代理项,结合异步加载图像元素(Image),例如在它们进⼊ 视图之前加载。 创建更多的代理项将会牺牲⼀些流畅的体验,并且花更多的时间来初始化每 个代理。这并不代表可以解决⼀些更加复杂的代理项的问题。在每次实例化 代理时,它的内容都会被评估和编辑。这需要花费时间,如果它花费了太多 的时间,它将会导致⼀个很差的滚动体验。在⼀个代理中包含太多的元素也 会降低滚动的性能。 为了补救这个问题,我们推荐使⽤动态加载元素。当它们需要时,可以初始 化这些附加的元素。例如,⼀个展开代理可能推迟它的详细内容的实例化, 直到需要使⽤它时。每个代理中最好减少JavaScript的数量。将每个代理中 复杂的JavaScript调⽤放在外⾯来实现。这将会减少每个代理在创建时编译 6.5.4 性能协调(Tunning Performance) 《QmlBook》In Chinese 186⾼级⽤法(Advanced Techniques) JavaScript。 《QmlBook》In Chinese 187⾼级⽤法(Advanced Techniques) 在这个章节中,我们学习了模型,视图与代理。每个数据的⼊⼝是模型,视 图通过可视化代理来实现数据的可视化。将数据从显⽰中分离出来。 ⼀个模型可以是⼀个整数,提供给代理使⽤的索引值(index )。如果 JavaScript数组被作为⼀个模型,模型数据变量(modelData)代表了数组的 数据的当前索引。对于更加复杂的情况,每个数据项需要提供多个值,使⽤ 链表模型(ListModel)与链表元素(ListElement)是⼀个更好的解决办法。 对于静态模型,⼀个Repeater可以被⽤作视图。它可以⾮常⽅便的使⽤⾏ (Row),列(Column),栅格(Grid),或者流(Flow)来创建⽤户界 ⾯。对于动态或者⼤的数据模型,使⽤ListView或者GridView更加适合。它 们会在需要时动态的创建代理,减少在场景下⼀次显⽰的元素的数量。 在视图中的代理可以与数据模型中的属性静态绑定,或者动态绑定。使⽤视 图的onAdd与onRemove信号,可以动态播放的它们的显⽰与消失。 总结(Summary) 《QmlBook》In Chinese 188总结(Summary) 注意 最后⼀次构建:2014年1⽉20⽇下午18:00。 这章的源代码能够在assetts folder找到。 在早些时候的Qt4中加⼊QML时,⼀些开发者讨论如何在QtQuick中绘制⼀个 圆形。类似圆形的问题,⼀些开发者也对于其它的形状的⽀持进⾏了讨论。 在QtQuick中没有圆形,只有矩形。在Qt4中,如果你需要⼀个除了矩形外的 形状,你需要使⽤图⽚或者使⽤你⾃⼰写的C++圆形元素。 Qt5中引进了画布元素(canvas element),允许脚本绘制。画布元素 (canvas element)提供了⼀个依赖于分辨率的位图画布,你可以使⽤ JavaScript脚本来绘制图形,制作游戏或者其它的动态图像。画布元素 (canvas element)是基于HTML5的画布元素来完成的。 画布元素(canvas element)的基本思想是使⽤⼀个2D对象来渲染路径。这 个2D对象包括了必要的绘图函数,画布元素(canvas element)充当绘制画 布。2D对象⽀持画笔,填充,渐变,⽂本和绘制路径创建命令。 Canvas Element 《QmlBook》In Chinese 189画布元素(Canvas Element) 让我们看看⼀个简单的路径绘制的例⼦: import QtQuick 2.0 Canvas { id: root // canvas size width: 200; height: 200 // handler to override for drawing onPaint: { // get context to draw with var ctx = getContext("2d") // setup the stroke ctx.lineWidth = 4 ctx.strokeStyle = "blue" // setup the fill ctx.fillStyle = "steelblue" // begin a new path to draw ctx.beginPath() // top-left start point ctx.moveTo(50,50) // upper line ctx.lineTo(150,50) // right line ctx.lineTo(150,150) // bottom line ctx.lineTo(50,150) // left line through path closing ctx.closePath() // fill using fill style ctx.fill() // stroke using line width and stroke style ctx.stroke() } } 这个例⼦产⽣了⼀个在坐标(50,50),⾼宽为100的填充矩形框,并且使⽤ 了画笔来修饰边界。 《QmlBook》In Chinese 190画布元素(Canvas Element) 画笔的宽度被设置为4个像素,并且定义strokeStyle(画笔样式)为蓝⾊。最 后的形状由设置填充样式(fillStyle)为steelblue颜⾊,然后填充完成的。只 有调⽤stroke或者fill函数,创建的路径才会绘制,它们与其它的函数使⽤是 相互独⽴的。调⽤stroke或者fill将会绘制当前的路径,创建的路径是不可重 ⽤的,只有绘制状态能够被存储和恢复。 在QML中,画布元素(canvas element)充当了绘制的容器。2D绘制对象提 供了实际绘制的⽅法。绘制需要在onPaint事件中完成。 Canvas { width: 200; height: 200 onPaint: { var ctx = getContext("2d") // setup your path // fill or/and stroke } } 画布⾃⾝提供了典型的⼆维笛卡尔坐标系统,左上⾓是(0,0)坐标。Y轴坐 标轴向下,X轴坐标轴向右。 典型绘制命令调⽤如下: 1. 装载画笔或者填充模式 《QmlBook》In Chinese 191画布元素(Canvas Element) 2. 创建绘制路径 3. 使⽤画笔或者填充绘制路径 onPaint: { var ctx = getContext("2d") // setup the stroke ctx.strokeStyle = "red" // create a path ctx.beginPath() ctx.moveTo(50,50) ctx.lineTo(150,50) // stroke path ctx.stroke() } 这将产⽣⼀个从P1(50,50)到P2(150,50)⽔平线。 注意 通常在你重置了路径后你将会设置⼀个开始点,所以,在beginPath()这个操 作后,你需要使⽤moveTo来设置开始点。 《QmlBook》In Chinese 192画布元素(Canvas Element) 在绘制矩形时,我们提供了⼀个便捷的接⼝,⽽不需要调⽤stroke或者fill来 完成。 // convenient.qml import QtQuick 2.0 Canvas { id: root width: 120; height: 120 onPaint: { var ctx = getContext("2d") ctx.fillStyle = 'green' ctx.strokeStyle = "blue" ctx.lineWidth = 4 // draw a filles rectangle ctx.fillRect(20, 20, 80, 80) // cut our an inner rectangle ctx.clearRect(30,30, 60, 60) // stroke a border from top-left to // inner center of the larger rectangle ctx.strokeRect(20,20, 40, 40) } } 便捷的接⼝(Convenient API) 《QmlBook》In Chinese 193便捷的接⼜(Convenient API) 注意 画笔的绘制区域由中间向两边延展。⼀个宽度为4像素的画笔将会在绘制路径 的⾥⾯绘制2个像素,外⾯绘制2个像素。 《QmlBook》In Chinese 194便捷的接⼜(Convenient API) 画布中可以使⽤颜⾊填充也可以使⽤渐变或者图像来填充。 onPaint: { var ctx = getContext("2d") var gradient = ctx.createLinearGradient(100,0,100,200) gradient.addColorStop(0, "blue") gradient.addColorStop(0.5, "lightsteelblue") ctx.fillStyle = gradient ctx.fillRect(50,50,100,100) } 在这个例⼦中,渐变⾊定义在开始点(100,0)到结束点(100,200)。在我 们画布中是⼀个中间垂直的线。渐变⾊在停⽌点定义⼀个颜⾊,范围从0.0到 1.0。这⾥我们使⽤⼀个蓝⾊作为0.0(100,0),⼀个⾼亮刚蓝⾊作为 0.5(100,200)。渐变⾊的定义⽐我们想要绘制的矩形更⼤,所以矩形在它 定义的范围内对渐变进⾏了裁剪。 注意 渐变(Gradients) 《QmlBook》In Chinese 195渐变(Gradients) 渐变⾊是在画布坐标下定义的,⽽不是在绘制路径相对坐标下定义的。画布 中没有相对坐标的概念。 《QmlBook》In Chinese 196渐变(Gradients) 注意 在Qt5的alpha版本中,我们使⽤阴影遇到了⼀些问题。 2D对象的路径可以使⽤阴影增强显⽰效果。阴影是⼀个区域的轮廓线使⽤偏 移量,颜⾊和模糊来实现的。所以你需要指定⼀个阴影颜⾊ (shadowColor),阴影X轴偏移值(shadowOffsetX),阴影Y轴偏移值 (shadowOffsetY)和阴影模糊(shadowBlur)。这些参数的定义都使⽤2D context来定义。2D context是唯⼀的绘制操作接⼝。 阴影也可以⽤来创建发光的效果。在下⾯的例⼦中我们使⽤⽩⾊的光创建了 ⼀个“Earth”的⽂本。在⼀个⿊⾊的背景上可以有更加好的显⽰效果。 ⾸先我们绘制⿊⾊背景: // setup a dark background ctx.strokeStyle = "#333" ctx.fillRect(0,0,canvas.width,canvas.height); 然后定义我们的阴影配置: ctx.shadowColor = "blue"; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; // next line crashes // ctx.shadowBlur = 10; 最后我们使⽤加粗的,80像素宽度的Ubuntu字体来绘制“Earth”⽂本: ctx.font = 'Bold 80px Ubuntu'; 阴影(Shadows) 《QmlBook》In Chinese 197阴影(Shadows) ctx.fillStyle = "#33a9ff"; ctx.fillText("Earth",30,180); 《QmlBook》In Chinese 198阴影(Shadows) QML画布⽀持多种资源的图⽚绘制。在画布中使⽤⼀个图⽚需要先加载图⽚ 资源。在我们的例⼦中我们使⽤Component.onCompleted操作来加载图⽚。 onPaint: { var ctx = getContext("2d") // draw an image ctx.drawImage('assets/ball.png', 10, 10) // store current context setup ctx.save() ctx.strokeStyle = 'red' // create a triangle as clip region ctx.beginPath() ctx.moveTo(10,10) ctx.lineTo(55,10) ctx.lineTo(35,55) ctx.closePath() // translate coordinate system ctx.translate(100,0) ctx.clip() // create clip from triangle path // draw image with clip applied ctx.drawImage('assets/ball.png', 10, 10) // draw stroke around path ctx.stroke() // restore previous setup ctx.restore() } Component.onCompleted: { loadImage("assets/ball.png") } 图⽚(Images) 《QmlBook》In Chinese 199图⽚(Images) 在左边,⾜球图⽚使⽤10×10的⼤⼩绘制在左上⽅的位置。在右边我们对⾜ 球图⽚进⾏了裁剪。图⽚或者轮廓路径都可以使⽤⼀个路径来裁剪。裁剪需 要定义⼀个裁剪路径,然后调⽤clip()函数来实现裁剪。在clip()之前所有的绘 制操作都会⽤来进⾏裁剪。如果还原了之前的状态或者定义裁剪区域为整个 画布时,裁剪是⽆效的。 《QmlBook》In Chinese 200图⽚(Images) 画布有多种⽅式来转换坐标系。这些操作⾮常类似于QML元素的转换。你可 以通过缩放(scale),旋转(rotate),translate(移动)来转换坐标系。 与QML元素的转换不同的是,转换原点通常就是画布原点。例如,从中⼼点 放⼤⼀个封闭的路径,你需要先将画布原点移动到整个封闭的路径的中⼼点 上。使⽤这些转换的⽅法你可以创建⼀些更加复杂的转换。 // transform.qml import QtQuick 2.0 Canvas { id: root width: 240; height: 120 onPaint: { var ctx = getContext("2d") ctx.strokeStyle = "blue" ctx.lineWidth = 4 ctx.beginPath() ctx.rect(-20, -20, 40, 40) ctx.translate(120,60) ctx.stroke() // draw path now rotated ctx.strokeStyle = "green" ctx.rotate(Math.PI/4) ctx.stroke() } } 转换(Transformation) 《QmlBook》In Chinese 201转换(Transformation) 除了移动画布外,也可以使⽤scale(x,y)来缩放x,y坐标轴。旋转使⽤ rotate(angle),angle是⾓度(360度=2*Math.PI)。使⽤ setTransform(m11,m12,m21,m22,dx,dy)来完成矩阵转换。 警告 QML画布中的转换与HTML5画布中的机制有些不同。不确定这是不是⼀个 Bug。 注意 重置矩阵你可以调⽤resetTransform()函数来完成,这个函数会将转换矩阵 还原为单位矩阵。 《QmlBook》In Chinese 202转换(Transformation) 组合允许你绘制⼀个形状然后与已有的像素点集合混合。画布提供了多种组 合模式,使⽤globalCompositeOperation(mode)来设置。 "source-over" "source-in" "source-out" "source-atop" onPaint: { var ctx = getContext("2d") ctx.globalCompositeOperation = "xor" ctx.fillStyle = "#33a9ff" for(var i=0; i<40; i++) { ctx.beginPath() ctx.arc(Math.random()*400, Math.random()*200, 20, 0, 2*Math.PI) ctx.closePath() ctx.fill() } } 下⾯这个例⼦遍历了列表中的组合模式,使⽤对应的组合模式⽣成了⼀个矩 形与圆形的组合。 property var operation : [ 'source-over', 'source-in', 'source-over', 'source-atop', 'destination-over', 'destination-in', 'destination-out', 'destination-atop', 'lighter', 'copy', 'xor', 'qt-clear', 'qt-destination', 组合模式(Composition Mode) 《QmlBook》In Chinese 203组合模式(Composition Mode) 'qt-multiply', 'qt-screen', 'qt-overlay', 'qt-darken', 'qt-lighten', 'qt-color-dodge', 'qt-color-burn', 'qt-hard-light', 'qt-soft-light', 'qt-difference', 'qt-exclusion' ] onPaint: { var ctx = getContext('2d') for(var i=0; i 1.0) { hue -= 1 } ctx.globalAlpha = 0.7 ctx.fillStyle = Qt.hsla(hue, 0.5, 0.5, 1.0) ctx.beginPath() ctx.moveTo(x+5,y) ctx.arc(x,y, x/10, 0, 360) ctx.closePath() 像素缓冲(Pixels Buffer) 《QmlBook》In Chinese 205像素缓冲(Pixels Buffer) ctx.fill() } MouseArea { anchors.fill: parent onClicked: { var url = canvas.toDataURL('image/png') print('image url=', url) image.source = url } } } Image { id: image x: 130; y: 10 width: 100; height: 100 } Timer { interval: 1000 running: true triggeredOnStart: true repeat: true onTriggered: canvas.requestPaint() } } 在我们这个例⼦中,我们每秒在左边的画布中绘制⼀个的圆形。当使⽤⿏标 点击画布内容时,会将内容存储为⼀个图⽚链接。在右边将会展⽰这个存储 的图⽚。 注意 在Qt5的Alpha版本中,检索图像数据似乎不能⼯作。 《QmlBook》In Chinese 206像素缓冲(Pixels Buffer) 在这个例⼦中我们将使⽤画布(Canvas)创建⼀个简单的绘制程序。 在我们场景的顶部我们使⽤⾏定位器排列四个⽅形的颜⾊块。⼀个颜⾊块是 ⼀个简单的矩形,使⽤⿏标区域来检测点击。 Row { id: colorTools anchors { horizontalCenter: parent.horizontalCenter top: parent.top topMargin: 8 } property variant activeSquare: red property color paintColor: "#33B5E5" spacing: 4 Repeater { 画布绘制(Canvas Paint) 《QmlBook》In Chinese 207画布绘制(Canvas Paint) model: ["#33B5E5", "#99CC00", "#FFBB33", "#FF4444"] ColorSquare { id: red color: modelData active: parent.paintColor == color onClicked: { parent.paintColor = color } } } } 颜⾊存储在⼀个数组中,作为绘制颜⾊使⽤。当⽤户点击⼀个矩形时,矩形 内的颜⾊被设置为colorTools的paintColor属性。 为了在画布上跟踪⿏标事件,我们使⽤⿏标区域(MouseArea)覆盖画布元 素,并连接点击和移动操作。 Canvas { id: canvas anchors { left: parent.left right: parent.right top: colorTools.bottom bottom: parent.bottom margins: 8 } property real lastX property real lastY property color color: colorTools.paintColor onPaint: { var ctx = getContext('2d') ctx.lineWidth = 1.5 ctx.strokeStyle = canvas.color ctx.beginPath() ctx.moveTo(lastX, lastY) lastX = area.mouseX lastY = area.mouseY 《QmlBook》In Chinese 208画布绘制(Canvas Paint) ctx.lineTo(lastX, lastY) ctx.stroke() } MouseArea { id: area anchors.fill: parent onPressed: { canvas.lastX = mouseX canvas.lastY = mouseY } onPositionChanged: { canvas.requestPaint() } } } ⿏标点击存储在laxstX与lastY属性中。每次⿏标位置的改变会触发画布的重 绘,这将会调⽤onPaint操作。 最后绘制⽤户的笔划,在onPaint操作中,我们绘制从最近改变的点上开始绘 制⼀条新的路径,然后我们从⿏标区域采集新的点,使⽤选择的颜⾊绘制线 段到新的点上。⿏标位置被存储为新改变的位置。 《QmlBook》In Chinese 209画布绘制(Canvas Paint) https://developer.mozilla.org/en/Canvas_tutorial/Transformations http://en.wikipedia.org/wiki/Spirograph 移植⼀个HTML5画布图像到QML画布⾮常简单。在成百上千的例⼦中,我们 选择了⼀个来移植。 螺旋图形(Spiro Graph) 我们使⽤⼀个来⾃Mozila项⺫的螺旋图形例⼦来作为我们的基础⽰例。原始 的HTML5代码被作为画布教程发布。 下⾯是我们需要修改的代码: Qt Quick要求定义变量使⽤,所以我们需要添加var的定义: for (var i=0;i<3;i++) { ... } 修改绘制⽅法接收Context2D对象: function draw(ctx) { ... } 由于不同的⼤⼩,我们需要对每个螺旋适配转换: ctx.translate(20+j*50,20+i*50); HTML5画布移植(Porting from HTML5 Canvas) 《QmlBook》In Chinese 210HTML5画布移植(Porting from HTML5 Canvas) 最后我们实现onPaint操作。在onPaint中我们请求⼀个context,并且调⽤我 们的绘制⽅法。 onPaint: { var ctx = getContext("2d"); draw(ctx); } 下⾯这个结果就是我们使⽤QML画布移植的螺旋图形。 发光线(Glowing Lines) 下⾯有⼀个更加复杂的移植来⾃W3C组织。原始的发光线有些很不错的地 ⽅,这使得移植更加具有挑战性。 《QmlBook》In Chinese 211HTML5画布移植(Porting from HTML5 Canvas) Pretty Glowing Lines 在HTML5中,context2D对象可以随意在画布上绘制。在QML中,只能在 onPaint操作中绘制。在HTML5中,通常调⽤setInterval使⽤计时器触发线段 的绘制或者清屏。由于QML中不同的操作⽅法,仅仅只是调⽤这些函数不能 实现我们想要的结果,因为我们需要通过onPaint操作来实现。我们也需要修 改颜⾊的格式。让我们看看需要改变哪些东⻄。 修改从画布元素开始。为了简单,我们使⽤画布元素(Canvas)作为我们 QML⽂件的根元素。 import QtQuick 2.0 Canvas { id: canvas width: 800; height: 450 ... } 代替直接调⽤的setInterval函数,我们使⽤两个计时器来请求重新绘制。⼀个 计时器触发间隔较短,允许我们可以执⾏⼀些代码。我们⽆法告诉绘制函数 哪个操作是我想触发的,我们为每个操作定义⼀个布尔标识,当重新绘制请 求时,我们请求⼀个操作并且触发它。 下⾯是线段绘制的代码,清屏操作类似。 ... property bool requestLine: false Timer { id: lineTimer interval: 40 《QmlBook》In Chinese 214HTML5画布移植(Porting from HTML5 Canvas) repeat: true triggeredOnStart: true onTriggered: { canvas.requestLine = true canvas.requestPaint() } } Component.onCompleted: { lineTimer.start() } ... 现在我们已经有了告诉onPaint操作中我们需要执⾏哪个操作的指⽰。当我们 进⼊onPaint处理每个绘制请求时,我们需要提取画布元素中的初始化变量。 Canvas { ... property real hue: 0 property real lastX: width * Math.random(); property real lastY: height * Math.random(); ... } 现在我们的绘制函数应该像这样: onPaint: { var context = getContext('2d') if(requestLine) { line(context) requestLine = false } if(requestBlank) { blank(context) requestBlank = false } } 《QmlBook》In Chinese 215HTML5画布移植(Porting from HTML5 Canvas) 线段绘制函数提取画布作为⼀个参数。 function line(context) { context.save(); context.translate(canvas.width/2, canvas.height/2); context.scale(0.9, 0.9); context.translate(-canvas.width/2, -canvas.height/2); context.beginPath(); context.lineWidth = 5 + Math.random() * 10; context.moveTo(lastX, lastY); lastX = canvas.width * Math.random(); lastY = canvas.height * Math.random(); context.bezierCurveTo(canvas.width * Math.random(), canvas.height * Math.random(), canvas.width * Math.random(), canvas.height * Math.random(), lastX, lastY); hue += Math.random()*0.1 if(hue > 1.0) { hue -= 1 } context.strokeStyle = Qt.hsla(hue, 0.5, 0.5, 1.0); // context.shadowColor = 'white'; // context.shadowBlur = 10; context.stroke(); context.restore(); } 最⼤的变化是使⽤QML的Qt.rgba()和Qt.hsla()。在QML中需要把变量值适配 在0.0到1.0之间。 同样应⽤在清屏函数中。 function blank(context) { context.fillStyle = Qt.rgba(0,0,0,0.1) context.fillRect(0, 0, canvas.width, canvas.height); } 《QmlBook》In Chinese 216HTML5画布移植(Porting from HTML5 Canvas) 下⾯是最终结果(⺫前没有阴影)类似下⾯这样。 查看下⾯的链接获得更多的信息: W3C HTML Canvas 2D Context Specification Mozilla Canvas Documentation HTML5 Canvas Tutorial 《QmlBook》In Chinese 217HTML5画布移植(Porting from HTML5 Canvas) 注意 最后⼀次构建:2014年1⽉20⽇下午18:00。 这章的源代码能够在assetts folder找到。 粒⼦模拟是计算机图形技术的可视化图形效果。典型的效果有:落叶,⽕ 焰,爆炸,流星,云等等。 它不同于其它图形渲染,粒⼦是基于模糊来渲染。它的结果在基于像素下是 不可预测的。粒⼦系统的参数描述了随机模拟的边界。传统的渲染技术实现 粒⼦渲染效果很困难。有⼀个好消息是你可以使⽤QML元素与粒⼦系统交 互。同时参数也可以看做是属性,这些参数可以使⽤传统的动画技术来实现 动态效果。 粒⼦模拟(Particle Simulations) 《QmlBook》In Chinese 218粒⼦模拟(Particle Simulations) 粒⼦模拟的核⼼是粒⼦系统(ParticleSystem),它控制了共享时间线。⼀ 个场景下可以有多个粒⼦系统,每个都有⾃⼰独⽴的时间线。⼀个粒⼦使⽤ 发射器元素(Emitter)发射,使⽤粒⼦画笔(ParticlePainter)实现可视 化,它可以是⼀张图⽚,⼀个QML项或者⼀个着⾊项(shader item)。⼀个 发射器元素(Emitter)也提供向量来控制粒⼦⽅向。⼀个粒⼦被发送后就再 也⽆法控制。粒⼦模型提供粒⼦控制器(Affector),它可以控制已发射粒⼦ 的参数。 在⼀个系统中,粒⼦可以使⽤粒⼦群元素(ParticleGroup)来共享移动时 间。默认下,每个例⼦都属于空("")组。 粒⼦系统(ParticleSystem)- 管理发射器之间的共享时间线。 发射器(Emitter)- 向系统中发射逻辑粒⼦。 粒⼦画笔(ParticlePainter)- 实现粒⼦可视化。 ⽅向(Direction)- 已发射粒⼦的向量空间。 粒⼦组(ParticleGroup)- 每个粒⼦是⼀个粒⼦组的成员。 概念(Concept) 《QmlBook》In Chinese 219概念(Concept) 粒⼦控制器(Affector)- 控制已发射粒⼦。 《QmlBook》In Chinese 220概念(Concept) 让我们从⼀个简单的模拟开始学习。Qt Quick使⽤简单的粒⼦渲染⾮常简 单。下⾯是我们需要的: 绑定所有元素到⼀个模拟的粒⼦系统(ParticleSystem)。 ⼀个向系统发射粒⼦的发射器(Emitter)。 ⼀个ParticlePainter派⽣元素,⽤来实现粒⼦的可视化。 import QtQuick 2.0 import QtQuick.Particles 2.0 Rectangle { id: root width: 480; height: 160 color: "#1f1f1f" ParticleSystem { id: particleSystem } Emitter { id: emitter anchors.centerIn: parent width: 160; height: 80 system: particleSystem emitRate: 10 lifeSpan: 1000 lifeSpanVariation: 500 size: 16 endSize: 32 Tracer { color: 'green' } } ImageParticle { source: "assets/particle.png" 简单的模拟(Simple Simulation) 《QmlBook》In Chinese 221简单的模拟(Simple Simulation) system: particleSystem } } 例⼦的运⾏结果如下所⽰: 我们使⽤⼀个80x80的⿊⾊矩形框作为我们的根元素和背景。然后我们定义 ⼀个粒⼦系统(ParticleSystem)。这通常是粒⼦系统绑定所有元素的第⼀ 步。下⼀个元素是发射器(Emitter),它定义了基于矩形框的发射区域和发 射粒⼦的基础属性。发射器使⽤system属性与粒⼦系统进⾏绑定。 在这个例⼦中,发射器每秒发射10个粒⼦(emitRate:10)到发射器的区域, 每个粒⼦的⽣命周期是1000毫秒(lifeSpan:1000),⼀个已发射粒⼦的⽣命 周期变化是500毫秒(lifeSpanVariation:500)。⼀个粒⼦开始的⼤⼩是16个 像素(size:16),⽣命周期结束时的⼤⼩是32个像素(endSize:32)。 绿⾊边框的矩形是⼀个跟踪元素,⽤来显⽰发射器的⼏何形状。这个可视化 展⽰了粒⼦在发射器矩形框内发射,但是渲染效果不被限制在发射器的矩形 框内。渲染位置依赖于粒⼦的寿命和⽅向。这将帮助我们更加清楚的知道如 何改变粒⼦的⽅向。 发射器发射逻辑粒⼦。⼀个逻辑粒⼦的可视化使⽤粒⼦画笔 (ParticlePainter)来实现,在这个例⼦中我们使⽤了图像粒⼦ (ImageParticle),使⽤⼀个图⽚链接作为源属性。图像粒⼦也有其它的属 性⽤来控制粒⼦的外观。 发射频率(emitRate)- 每秒粒⼦发射数(默认为10个)。 ⽣命周期(lifeSpan)- 粒⼦持续时间(单位毫秒,默认为1000毫秒)。 《QmlBook》In Chinese 222简单的模拟(Simple Simulation) 初始⼤⼩(size),结束⼤⼩(endSize)- 粒⼦在它的⽣命周期的开始 和结束时的⼤⼩(默认为16像素)。 改变这些属性将会彻底改变显⽰结果: Emitter { id: emitter anchors.centerIn: parent width: 20; height: 20 system: particleSystem emitRate: 40 lifeSpan: 2000 lifeSpanVariation: 500 size: 64 sizeVariation: 32 Tracer { color: 'green' } } 增加发射频率为40,⽣命周期增加到2秒,开始⼤⼩为64像素,结束⼤⼩减 少到32像素。 增加结束⼤⼩(endSize)可能会导致⽩⾊的背景出现。请注意粒⼦只有发射 被限制在发射器定义的区域内,⽽粒⼦渲染是不会考虑这个参数的。 《QmlBook》In Chinese 223简单的模拟(Simple Simulation) 我们已经知道通过改变发射器的⾏为就可以改变我们的粒⼦模拟。粒⼦画笔 被⽤来绘制每⼀个粒⼦。 回到我们之前的粒⼦中,我们更新⼀下我们的图⽚ 粒⼦画笔(ImageParticle)。⾸先我们改变粒⼦图⽚为⼀个⼩的星形图⽚: ImageParticle { ... source: 'assets/star.png' } 粒⼦使⽤⾦⾊来进⾏初始化,不同的粒⼦颜⾊变化范围为+/- 20%。 color: '#FFD700' colorVariation: 0.2 为了让场景更加⽣动,我们需要旋转粒⼦。每个粒⼦⾸先按顺时针旋转15 度,不同的粒⼦在+/-5度之间变化。每个例⼦会不断的以每秒45度旋转。每 个粒⼦的旋转速度在+/-15度之间变化: rotation: 15 rotationVariation: 5 rotationVelocity: 45 rotationVelocityVariation: 15 最后,我们改变粒⼦的⼊场效果。 这个效果是粒⼦产⽣时的效果,在这个例 ⼦中,我们希望使⽤⼀个缩放效果: entryEffect: ImageParticle.Scale 粒⼦参数(Particle Parameters) 《QmlBook》In Chinese 224粒⼦参数(Particle Parameters) 现在我们可以看到旋转的星星出现在我们的屏幕上。 下⾯是我们如何改变图⽚粒⼦画笔的代码段。 ImageParticle { source: "assets/star.png" system: particleSystem color: '#FFD700' colorVariation: 0.2 rotation: 0 rotationVariation: 45 rotationVelocity: 15 rotationVelocityVariation: 15 entryEffect: ImageParticle.Scale } 《QmlBook》In Chinese 225粒⼦参数(Particle Parameters) 我们已经看到了粒⼦的旋转,但是我们的粒⼦需要⼀个轨迹。轨迹由速度或 者粒⼦随机⽅向的加速度指定,也可以叫做⽮量空间。 有多种可⽤⽮量空间⽤来定义粒⼦的速度或加速度: ⾓度⽅向(AngleDirection)- 使⽤⾓度的⽅向变化。 点⽅向(PointDirection)- 使⽤x,y组件组成的⽅向变化。 ⺫标⽅向(TargetDirection)- 朝着⺫标点的⽅向变化。 让我们在场景下试着⽤速度⽅向将粒⼦从左边移动到右边。 ⾸先使⽤⾓度⽅向(AngleDirection)。我们使⽤AngleDirection元素作为我 们的发射器(Emitter)的速度属性: velocity: AngleDirection { } 粒⼦的发射将会使⽤指定的⾓度属性。⾓度值在0到360度之间,0度代表指 向右边。在我们的例⼦中,例⼦将会移动到右边,所以0度已经指向右边⽅ 向。粒⼦的⾓度变化在+/-15度之间: velocity: AngleDirection { angle: 0 粒⼦⽅向(Directed Particle) 《QmlBook》In Chinese 226粒⼦⽅向(Directed Particle) angleVariation: 15 } 现在我们已经设置了⽅向,下⾯是指定粒⼦的速度。它由⼀个梯度值定义, 这个梯度值定义了每秒像素的变化。正如我们设置⼤约640像素,梯度值为 100,看起来是⼀个不错的值。这意味着平均⼀个6.4秒⽣命周期的粒⼦可以 穿越我们看到的区域。为了让粒⼦的穿越看起来更加有趣,我们使⽤ magnitudeVariation来设置梯度值的变化,这个值是我们的梯度值的⼀半: velocity: AngleDirection { ... magnitude: 100 magnitudeVariation: 50 } 下⾯是完整的源码,平均的⽣命周期被设置为6..4秒。我们设置发射器的宽 度和⾼度为1个像素,这意味着所有的粒⼦都从相同的位置发射出去,然后基 于我们给定的轨迹运动。 Emitter { 《QmlBook》In Chinese 227粒⼦⽅向(Directed Particle) id: emitter anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: 1; height: 1 system: particleSystem lifeSpan: 6400 lifeSpanVariation: 400 size: 32 velocity: AngleDirection { angle: 0 angleVariation: 15 magnitude: 100 magnitudeVariation: 50 } } 那么加速度做些什么?加速度是每个粒⼦加速度⽮量,它会在运动的时间中 改变速度⽮量。例如我们做⼀个星星按照弧形运动的轨迹。我们将会改变我 们的速度⽅向为-45度,然后移除变量,可以得到⼀个更连贯的弧形轨迹: velocity: AngleDirection { angle: -45 magnitude: 100 } 加速度的⽅向为90度(向下),加速度为速度的四分之⼀: acceleration: AngleDirection { angle: 90 magnitude: 25 } 结果是中间左⽅到右下的⼀个弧。 《QmlBook》In Chinese 228粒⼦⽅向(Directed Particle) 这个值是在不断的尝试与错误中发现的。 下⾯是发射器完整的代码。 Emitter { id: emitter anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: 1; height: 1 system: particleSystem emitRate: 10 lifeSpan: 6400 lifeSpanVariation: 400 size: 32 velocity: AngleDirection { angle: -45 angleVariation: 0 magnitude: 100 } acceleration: AngleDirection { angle: 90 magnitude: 25 } } 《QmlBook》In Chinese 229粒⼦⽅向(Directed Particle) 在下⼀个例⼦中,我们将使⽤点⽅向(PointDirection)⽮量空间来再⼀次演 ⽰粒⼦从左到右的运动。 ⼀个点⽅向(PointDirection)是由x和y组件组成的⽮量空间。例如,如果你 想粒⼦以45度的⽮量运动,你需要为x,y指定相同的值。 在我们的例⼦中,我们希望粒⼦在从左到右的例⼦中建⽴⼀个15度的圆锥。 我们指定⼀个坐标⽅向(PointDirection)作为我们速度⽮量空间: velocity: PointDirection { } 为了达到运动速度每秒100个像素,我们设置x为100,。15度⾓(90度的 1/6),我们指定y变量为100/6: velocity: PointDirection { x: 100 y: 0 xVariation: 0 yVariation: 100/6 } 结果是粒⼦的运动从左到右构成了⼀个15度的圆锥。 《QmlBook》In Chinese 230粒⼦⽅向(Directed Particle) 现在是最后⼀个⽅案,⺫标⽅向(TargetDirection)。⺫标⽅向允许我们指 定发射器或者⼀个QML项的x,y坐标值。当⼀个QML项的中⼼点成为⼀个⺫标 点时,你可以指定⺫标变化值是x⺫标值的1/6来完成⼀个15度的圆锥: velocity: TargetDirection { targetX: 100 targetY: 0 targetVariation: 100/6 magnitude: 100 } 注意 当你期望发射粒⼦朝着指定的x,y坐标值流动时,⺫标⽅向是⾮常好的⽅案。 我没有再贴出结果图,因为它与前⼀个结果相同,取⽽代之的有⼀个问题留 给你。 在下图的红⾊和绿⾊圆指定每个⺫标项的⺫标⽅向速度的加速属性。每个⺫ 标⽅向有相同的参数。那么哪⼀个负责速度,哪⼀个负责加速度? 《QmlBook》In Chinese 231粒⼦⽅向(Directed Particle) 《QmlBook》In Chinese 232粒⼦⽅向(Directed Particle) 到⺫前为⽌我们只使⽤了基于粒⼦画笔的图像来实现粒⼦可视化。Qt也提供 了⼀些其它的粒⼦画笔: 粒⼦项(ItemParticle):基于粒⼦画笔的代理 ⾃定义粒⼦(CustomParticle):基于粒⼦画笔的着⾊器 粒⼦项可以将QML元素项作为粒⼦发射。你需要制定⾃⼰的粒⼦代理。 ItemParticle { id: particle system: particleSystem delegate: itemDelegate } 在这个例⼦中,我们的代理是⼀个随机图⽚(使⽤Math.random()完成),有 着⽩⾊边框和随机⼤⼩。 Component { id: itemDelegate Rectangle { id: container width: 32*Math.ceil(Math.random()*3); height: width color: 'white' Image { anchors.fill: parent anchors.margins: 4 source: 'assets/fruits'+Math.ceil(Math.random()*10)+'.jpg' } } } 粒⼦画笔(Particle Painter) 《QmlBook》In Chinese 233粒⼦画笔(Particle Painter) 每秒发出四个粒⼦,每个粒⼦拥有4秒的⽣命周期。粒⼦⾃动淡⼊淡出。 对于更多的动态情况,也可以由你⾃⼰创建⼀个⼦项,让粒⼦系统来控制 它,使⽤take(item, priority)来完成。粒⼦系统控制你的粒⼦就像控制普通的 粒⼦⼀样。你可以使⽤give(item)来拿回⼦项的控制权。你也可以操作⼦项粒 ⼦,甚⾄可以使⽤freeze(item)来停⽌它,使⽤unfreeze(item)来恢复它。 《QmlBook》In Chinese 234粒⼦画笔(Particle Painter) 粒⼦由粒⼦发射器发出。在粒⼦发射出后,发射器⽆法再改变粒⼦。粒⼦控 制器允许你控制发射后的粒⼦参数。 控制器的每个类型使⽤不同的⽅法来影响粒⼦: ⽣命周期(Age)- 修改粒⼦的⽣命周期 吸引(Attractor)- 吸引粒⼦朝向指定点 摩擦(Friction)- 按当前粒⼦速度成正⽐减慢运动 重⼒(Gravity)- 设置⼀个⾓度的加速度 紊流(Turbulence)- 强制基于噪声图像⽅式的流动 漂移(Wander)- 随机变化的轨迹 组⺫标(GroupGoal)- 改变⼀组粒⼦群的状态 ⼦粒⼦(SpriteGoal)- 改变⼀个⼦粒⼦的状态 ⽣命周期(Age) 允许粒⼦⽼得更快,lifeLeft属性指定了粒⼦的有多少的⽣命周期。 Age { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem advancePosition: true lifeLeft: 1200 once: true Tracer {} } 粒⼦控制(Affecting Particles) 《QmlBook》In Chinese 235粒⼦控制(Affecting Particles) 在这个例⼦中,当粒⼦的⽣命周期达到1200毫秒后,我们将会缩短上⽅的粒 ⼦的⽣命周期⼀次。由于我们设置了advancePosition为true,当粒⼦的⽣命 周期到达1200毫秒后,我们将会再⼀次在这个位置看到粒⼦出现。 吸引(Attractor) 吸引会将粒⼦朝指定的点上吸引。这个点使⽤pointX与pointY来指定,它是 与吸引区域的⼏何形状相对的。strength指定了吸引的⼒度。在我们的例⼦ 中,我们让粒⼦从左向右运动,吸引放在顶部,有⼀半运动的粒⼦会穿过吸 引区域。控制器只会影响在它们⼏何形状内的粒⼦。这种分离让我们可以同 步看到正常的流动与受影响的流动。 Attractor { anchors.horizontalCenter: parent.horizontalCenter width: 160; height: 120 system: particleSystem pointX: 0 pointY: 0 strength: 1.0 Tracer {} } 《QmlBook》In Chinese 236粒⼦控制(Affecting Particles) 很容易看出上半部分粒⼦受到吸引。吸引点被设置为吸引区域的左上⾓(0/0 点),吸引⼒为1.0。 摩擦(Friction) 摩擦控制器使⽤⼀个参数(factor)减慢粒⼦运动,直到达到⼀个阈值。 Friction { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem factor : 0.8 threshold: 25 Tracer {} } 在上部的摩擦区域,粒⼦被按照0.8的参数(factor)减慢,直到粒⼦的速度 达到25像素每秒。这个阈值像⼀个过滤器。粒⼦运动速度⾼于阈值将会按照 给定的参数来减慢它。 《QmlBook》In Chinese 237粒⼦控制(Affecting Particles) 重⼒(Gravity) 重⼒控制器应⽤在加速度上,在我们的例⼦中,我们使⽤⼀个⾓度⽅向将粒 ⼦从底部发射到顶部。右边是为控制区域,左边使⽤重⼒控制器控制,重⼒ ⽅向为90度⽅向(垂直向下),梯度值为50。 Gravity { width: 240; height: 240 system: particleSystem magnitude: 50 angle: 90 Tracer {} } 左边的粒⼦试图爬上去,但是稳定向下的加速度将它们按照重⼒的⽅向拖拽 下来。 《QmlBook》In Chinese 238粒⼦控制(Affecting Particles) 紊流(Turbulence) 紊流控制器,对粒⼦应⽤了⼀个混乱映射⽅向⼒的⽮量。这个混乱映射是由 ⼀个噪声图像定义的。可以使⽤noiseSource属性来定义噪声图像。strength 定义了⽮量对于粒⼦运动的影响有多⼤。 Turbulence { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem strength: 100 Tracer {} } 在这个例⼦中,上部区域被紊流影响。它们的运动看起来是不稳定的。不稳 定的粒⼦偏差值来⾃原路径定义的strength。 《QmlBook》In Chinese 239粒⼦控制(Affecting Particles) 漂移(Wander) 漂移控制器控制了轨迹。affectedParameter属性可以指定哪个参数控制了漂 移(速度,位置或者加速度)。pace属性制定了每秒最多改变的属性。 yVariance指定了y组件对粒⼦轨迹的影响。 Wander { anchors.horizontalCenter: parent.horizontalCenter width: 240; height: 120 system: particleSystem affectedParameter: Wander.Position pace: 200 yVariance: 240 Tracer {} } 在顶部漂移控制器的粒⼦被随机的轨迹改变。在这种情境下,每秒改变粒⼦y ⽅向的位置200次。 《QmlBook》In Chinese 240粒⼦控制(Affecting Particles) 《QmlBook》In Chinese 241粒⼦控制(Affecting Particles) 在本章开始时,我们已经介绍过粒⼦组了,默认下,粒⼦都属于空组 ("")。使⽤GroupGoal控制器可以改变粒⼦组。为了实现可视化,我们创建 了⼀个烟花⽰例,⽕箭进⼊,在空中爆炸形成壮观的烟⽕。 这个例⼦分为两部分。第⼀部分叫做“发射时间(Launch Time)”连接场景, 加⼊粒⼦组,第⼆部分叫做“爆炸烟花(Let there be firework)”,专注于粒 ⼦组的变化。 让我们看看这两部分。 发射时间(Launch Time) ⾸先我们创建⼀个典型的⿊⾊场景: import QtQuick 2.0 import QtQuick.Particles 2.0 Rectangle { 粒⼦组(Particle Group) 《QmlBook》In Chinese 242粒⼦组(Particle Group) id: root width: 480; height: 240 color: "#1F1F1F" property bool tracer: false } tracer使⽤被⽤作场景追踪的开关,然后定义我们的粒⼦系统: ParticleSystem { id: particleSystem } 我们添加两种粒⼦图⽚画笔(⼀个⽤于⽕箭,⼀个⽤于⽕箭喷射烟雾): ImageParticle { id: smokePainter system: particleSystem groups: ['smoke'] source: "assets/particle.png" alpha: 0.3 entryEffect: ImageParticle.None } ImageParticle { id: rocketPainter system: particleSystem groups: ['rocket'] source: "assets/rocket.png" entryEffect: ImageParticle.None } 你可以看到在这些画笔定义中,它们使⽤groups属性来定义粒⼦的归属。只 需要定义⼀个名字,Qt Quick将会隐式的创建这个分组。 现在我们需要将这些⽕箭发射到空中。我们在场景底部创建⼀个粒⼦发射 器,将速度设置为朝上的⽅向。为了模拟重⼒,我们设置⼀个向下的加速 《QmlBook》In Chinese 243粒⼦组(Particle Group) 度: Emitter { id: rocketEmitter anchors.bottom: parent.bottom width: parent.width; height: 40 system: particleSystem group: 'rocket' emitRate: 2 maximumEmitted: 4 lifeSpan: 4800 lifeSpanVariation: 400 size: 32 velocity: AngleDirection { angle: 270; magnitude: 150; magnitudeVariation: 10 } acceleration: AngleDirection { angle: 90; magnitude: 50 } Tracer { color: 'red'; visible: root.tracer } } 发射器属于'rocket'粒⼦组,与我们的⽕箭粒⼦画笔相同。通过粒⼦组将它们 联系在⼀起。发射器将粒⼦发射到'rocket'粒⼦组中,⽕箭画笔将会绘制它 们。 对于烟雾,我们使⽤⼀个追踪发射器,它将会跟在⽕箭的后⾯。我们定 义'smoke'组,并且它会跟在'rocket'粒⼦组后⾯: TrailEmitter { id: smokeEmitter system: particleSystem emitHeight: 1 emitWidth: 4 group: 'smoke' follow: 'rocket' emitRatePerParticle: 96 velocity: AngleDirection { angle: 90; magnitude: 100; angleVariation: 5 } lifeSpan: 200 size: 16 sizeVariation: 4 《QmlBook》In Chinese 244粒⼦组(Particle Group) endSize: 0 } 向下模拟从⽕箭⾥⾯喷射出的烟。emitHeight与emitWidth指定了围绕跟随在 烟雾粒⼦发射后的粒⼦。如果不指定这个值,跟随的粒⼦将会被拿掉,但是 对于这个例⼦,我们想要提升显⽰效果,粒⼦流从⼀个接近于⽕箭尾部的中 间点发射出。 如果你运⾏这个例⼦,你会发现⼀些⽕箭正常⻜起,⼀些⽕箭却⻜出场景。 这不是我们想要的,我们需要在它们离开场景前让他们慢下来,这⾥可以使 ⽤摩擦控制器来设置⼀个最⼩阈值: Friction { groups: ['rocket'] anchors.top: parent.top width: parent.width; height: 80 system: particleSystem threshold: 5 factor: 0.9 } 在摩擦控制器中,你也需要定义哪个粒⼦组受控制器影响。当⽕箭经过从顶 部向下80像素的区域时,所有的⽕箭将会以0.9的factor减慢(你可以试试 100,你会发现它们⽴即停⽌了),直到它们的速度达到每秒5个像素。随着 ⽕箭粒⼦向下的加速度继续⽣效,⽕箭开始向地⾯下沉,直到它们的⽣命周 期结束。 由于在空⽓中向上运动是⾮常困难的,并且⾮常不稳定,我们在⽕箭上升时 模拟⼀些紊流: Turbulence { groups: ['rocket'] anchors.bottom: parent.bottom width: parent.width; height: 160 《QmlBook》In Chinese 245粒⼦组(Particle Group) system: particleSystem strength: 25 Tracer { color: 'green'; visible: root.tracer } } 当然,紊流控制器也需要定义它会影响哪些粒⼦组。紊流控制器的区域从底 部向上160像素(直到摩擦控制器边界上),它们也可以相互覆盖。 当你运⾏程序时,你可以看到⽕箭开始上升,然后在摩擦控制器区域开始减 速,向下的加速度仍然⽣效,⽕箭开始后退。下⼀步我们开始制作爆炸烟 花。 注意 使⽤tracers跟踪区域可以显⽰场景中的不同区域。⽕箭粒⼦发射的红⾊区 域,蓝⾊区域是紊流控制器区域,最后在绿⾊的摩擦控制器区域减速,并且 再次下降是由于向下的加速度仍然⽣效。 爆炸烟花(Let there be fireworks) 让⽕箭变成美丽的烟花,我们需要添加⼀个粒⼦组来封装这个变化: 《QmlBook》In Chinese 246粒⼦组(Particle Group) ParticleGroup { name: 'explosion' system: particleSystem } 我们使⽤GroupGoal控制器来改变粒⼦组。这个组控制器被放置在屏幕中间 垂直线附近,它将会影响'rocket'粒⼦组。使⽤groupGoal属性,我们设置⺫ 标组改变为我们之前定义的'explosion'组: GroupGoal { id: rocketChanger anchors.top: parent.top width: parent.width; height: 80 system: particleSystem groups: ['rocket'] goalState: 'explosion' jump: true Tracer { color: 'blue'; visible: root.tracer } } jump属性定义了粒⼦组的变化是⽴即变化⽽不是在某个时间段后变化。 注意 在Qt5的alpha发布版中,粒⼦组的持续改变⽆法⼯作,有好的建议吗? 由于⽕箭粒⼦变为我们的爆炸粒⼦,当⽕箭粒⼦进⼊GroupGoal控制器区域 时,我们需要在粒⼦组中添加⼀个烟花: // inside particle group TrailEmitter { id: explosionEmitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 750 emitRatePerParticle: 200 《QmlBook》In Chinese 247粒⼦组(Particle Group) size: 32 velocity: AngleDirection { angle: -90; angleVariation: 180; magnitude: 50 } } 爆炸释放粒⼦到'sparkle'粒⼦组。我们稍后会定义这个组的粒⼦画笔。轨迹发 射器跟随⽕箭粒⼦每秒发射200个⽕箭爆炸粒⼦。粒⼦的⽅向向上,并改变 180度。 由于向'sparkle'粒⼦组发射粒⼦,我们需要定义⼀个粒⼦画笔⽤于绘制这个组 的粒⼦: ImageParticle { id: sparklePainter system: particleSystem groups: ['sparkle'] color: 'red' colorVariation: 0.6 source: "assets/star.png" alpha: 0.3 } 闪烁的烟花是红⾊的星星,使⽤接近透明的颜⾊来渲染出发光的效果。 为了使烟花更加壮观,我们也需要添加给我们的粒⼦组添加第⼆个轨迹发射 器,它向下发射锥形粒⼦: // inside particle group TrailEmitter { id: explosion2Emitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 250 emitRatePerParticle: 100 size: 32 velocity: AngleDirection { angle: 90; angleVariation: 15; magnitude: 400 } 《QmlBook》In Chinese 248粒⼦组(Particle Group) } 其它的爆炸轨迹发射器与这个设置类似,就这样。 下⾯是最终结果。 下⾯是⽕箭烟花的所有代码。 import QtQuick 2.0 import QtQuick.Particles 2.0 Rectangle { id: root width: 480; height: 240 color: "#1F1F1F" property bool tracer: false ParticleSystem { id: particleSystem } ImageParticle { id: smokePainter 《QmlBook》In Chinese 249粒⼦组(Particle Group) system: particleSystem groups: ['smoke'] source: "assets/particle.png" alpha: 0.3 } ImageParticle { id: rocketPainter system: particleSystem groups: ['rocket'] source: "assets/rocket.png" entryEffect: ImageParticle.Fade } Emitter { id: rocketEmitter anchors.bottom: parent.bottom width: parent.width; height: 40 system: particleSystem group: 'rocket' emitRate: 2 maximumEmitted: 8 lifeSpan: 4800 lifeSpanVariation: 400 size: 32 velocity: AngleDirection { angle: 270; magnitude: 150; magnitudeVariation: 10 } acceleration: AngleDirection { angle: 90; magnitude: 50 } Tracer { color: 'red'; visible: root.tracer } } TrailEmitter { id: smokeEmitter system: particleSystem group: 'smoke' follow: 'rocket' size: 16 sizeVariation: 8 emitRatePerParticle: 16 velocity: AngleDirection { angle: 90; magnitude: 100; angleVariation: 15 } lifeSpan: 200 Tracer { color: 'blue'; visible: root.tracer } } 《QmlBook》In Chinese 250粒⼦组(Particle Group) Friction { groups: ['rocket'] anchors.top: parent.top width: parent.width; height: 80 system: particleSystem threshold: 5 factor: 0.9 } Turbulence { groups: ['rocket'] anchors.bottom: parent.bottom width: parent.width; height: 160 system: particleSystem strength:25 Tracer { color: 'green'; visible: root.tracer } } ImageParticle { id: sparklePainter system: particleSystem groups: ['sparkle'] color: 'red' colorVariation: 0.6 source: "assets/star.png" alpha: 0.3 } GroupGoal { id: rocketChanger anchors.top: parent.top width: parent.width; height: 80 system: particleSystem groups: ['rocket'] goalState: 'explosion' jump: true Tracer { color: 'blue'; visible: root.tracer } } 《QmlBook》In Chinese 251粒⼦组(Particle Group) ParticleGroup { name: 'explosion' system: particleSystem TrailEmitter { id: explosionEmitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 750 emitRatePerParticle: 200 size: 32 velocity: AngleDirection { angle: -90; angleVariation: 180; magnitude: 50 } } TrailEmitter { id: explosion2Emitter anchors.fill: parent group: 'sparkle' follow: 'rocket' lifeSpan: 250 emitRatePerParticle: 100 size: 32 velocity: AngleDirection { angle: 90; angleVariation: 15; magnitude: 400 } } } } 《QmlBook》In Chinese 252粒⼦组(Particle Group) 粒⼦是⼀个⾮常强⼤且有趣的⽅法,⽤来表达图像现象的⼀种⽅式,⽐如 烟, ⽕花,随机可视元素。Qt5的扩展API⾮常强⼤,我们仅仅只使⽤了⼀些 浅显的。有⼀些元素我们还没有使⽤过,⽐如精灵(spirites),尺⼨表 (size tables),颜⾊表(color tables)。粒⼦看起来⾮常有趣,它在界⾯ 上创建引⼈注⺫的东⻄是⾮常有潜⼒的。在⼀个⽤户界⾯中使⽤⾮常多的粒 ⼦效果将会导致⽤户对它产⽣这是⼀个游戏的印象。粒⼦的真正⼒量也是⽤ 来创建游戏。 总结(Summary) 《QmlBook》In Chinese 253总结(Summary) 注意 最后⼀次构建:2014年1⽉20⽇下午18:00。 这章的源代码能够在assetts folder找到。 http://labs.qt.nokia.com/2012/02/02/qt-graphical-effects-in-qt-labs/ http://labs.qt.nokia.com/2011/05/03/qml-shadereffectitem-on- qgraphicsview/ http://qt-project.org/doc/qt-4.8/declarative-shadereffects.html http://www.opengl.org/registry/doc/GLSLangSpec.4.20.6.clean.pdf http://www.khronos.org/registry/gles/specs/2.0/GLSL_ES_Specification _1.0.17.pdf http://www.lighthouse3d.com/opengl/glsl/ http://wiki.delphigl.com/index.php/Tutorial_glsl Qt5Doc qtquick-shaders 着⾊器允许我们利⽤SceneGraph的接⼝直接调⽤在强⼤的GPU上运⾏的 OpenGL来创建渲染效果。着⾊器使⽤ShaderEffect与ShaderEffectSource元 素来实现。着⾊器本⾝的算法使⽤OpenGL Shading Language(OpenGL着 ⾊语⾔)来实现。 实际上这意味着你需要混合使⽤QML代码与着⾊器代码。执⾏时,会将着⾊ 器代码发送到GPU,并在GPU上编译执⾏。QML着⾊器元素(Shader QML Elements)允许你与OpenGL着⾊器程序的属性交互。 着⾊器效果(Shader Effect) 《QmlBook》In Chinese 254着⾊器效果(Shader Effect) 让我们⾸先来看看OpenGL着⾊器。 《QmlBook》In Chinese 255着⾊器效果(Shader Effect) OpenGL的渲染管线分为⼏个步骤。⼀个简单的OpenGL渲染管线将包含⼀个 顶点着⾊器和⼀个⽚段着⾊器。 顶点着⾊器接收顶点数据,并且在程序最后赋值给gl_Position。然后,顶点 将会被裁剪,转换和栅格化后作为像素输出。 ⽚段(像素)进⼊⽚段着⾊ 器,进⼀步对⽚段操作并将结果的颜⾊赋值给gl_FragColor。顶点着⾊器调 ⽤多边形每个⾓的点(顶点=3D中的点),负责这些点的3D处理。⽚段(⽚ 度=像素)着⾊器调⽤每个像素并决定这个像素的颜⾊。 OpenGL着⾊器(OpenGL Shader) 《QmlBook》In Chinese 256OpenGL着⾊器(OpenGL Shader) 为了对着⾊器编程,Qt Quick提供了两个元素。ShaderEffectSource与 ShaderEffect。ShaderEffect将会使⽤⾃定义的着⾊器,ShaderEffectSource 可以将⼀个QML元素渲染为⼀个纹理然后再渲染这个纹理。由于 ShaderEffect能够应⽤⾃定义的着⾊器到它的矩形⼏何形状,并且能够使⽤ 在着⾊器中操作资源。⼀个资源可以是⼀个图⽚,它被作为⼀个纹理或者着 ⾊器资源。 默认下着⾊器使⽤这个资源并且不作任何改变进⾏渲染。 import QtQuick 2.0 Rectangle { width: 480; height: 240 color: '#1e1e1e' Row { anchors.centerIn: parent spacing: 20 Image { id: sourceImage width: 80; height: width source: 'assets/tulips.jpg' } ShaderEffect { id: effect width: 80; height: width property variant source: sourceImage } ShaderEffect { id: effect2 width: 80; height: width // the source where the effect shall be applied to property variant source: sourceImage // default vertex shader code vertexShader: " 着⾊器元素(Shader Elements) 《QmlBook》In Chinese 257着⾊器元素(Shader Elements) uniform highp mat4 qt_Matrix; attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; varying highp vec2 qt_TexCoord0; void main() { qt_TexCoord0 = qt_MultiTexCoord0; gl_Position = qt_Matrix * qt_Vertex; }" // default fragment shader code fragmentShader: " varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; void main() { gl_FragColor = texture2D(source, qt_TexCoord0) * qt_Opacity; }" } } } 在上边这个例⼦中,我们在⼀⾏中显⽰了3张图⽚,第⼀张是原始图⽚,第⼆ 张使⽤默认的着⾊器渲染出来的图⽚,第三张使⽤了Qt5源码中默认的顶点与 ⽚段着⾊器的代码进⾏渲染的图⽚。 《QmlBook》In Chinese 258着⾊器元素(Shader Elements) 注意 如果你不想看到原始图⽚,⽽只想看到被着⾊器渲染后的图⽚,你可以设置 Image为不可⻅(visible:false)。着⾊器仍然会使⽤图⽚数据,但是图像元 素(Image Element)将不会被渲染。 让我们仔细看看着⾊器代码。 vertexShader: " uniform highp mat4 qt_Matrix; attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; varying highp vec2 qt_TexCoord0; void main() { qt_TexCoord0 = qt_MultiTexCoord0; gl_Position = qt_Matrix * qt_Vertex; }" 着⾊器代码来⾃Qt这边的⼀个字符串,绑定了顶点着⾊器(vertexShader) 与⽚段着⾊器(fragmentShader)属性。每个着⾊器代码必须有⼀个main() {....}函数,它将被GPU执⾏。Qt已经默认提供了以qt_开头的变量。 下⾯是这些变量简短的介绍: uniform-在处理过程中不能够改变的值。 attribute-连接外部数据 varying-着⾊器之间的共享数据 highp-⾼精度值 lowp-低精度值 mat4-4x4浮点数(float)矩阵 vec2-包含两个浮点数的向量 《QmlBook》In Chinese 259着⾊器元素(Shader Elements) sampler2D-2D纹理 float-浮点数 可以查看OpenGL ES 2.0 API Quick Reference Card获得更多信息。 现在我们可以更好的理解下⾯这些变量: qt_Matrix:model-view-projection(模型-视图-投影)矩阵 qt_Vertex:当前顶点坐标 qt_MultiTexCoord0:纹理坐标 qt_TexCoord0:共享纹理坐标 我们已经有可以使⽤的投影矩阵(projection matrix),当前顶点与纹理坐 标。纹理坐标与作为资源(source)的纹理相关。在main()函数中,我们保 存纹理坐标,留在后⾯的⽚段着⾊器中使⽤。每个顶点着⾊器都需要赋值给 gl_Postion,在这⾥使⽤项⺫矩阵乘以顶点,得到我们3D坐标系中的点。 ⽚段着⾊器从顶点着⾊器中接收我们的纹理坐标,这个纹理仍然来⾃我们的 QML资源属性(source property)。在着⾊器代码与QML之间传递变量是如 此的简单。此外我们的透明值,在着⾊器中也可以使⽤,变量是 qt_Opacity。每 个⽚段着⾊器需要给gl_FragColor变量赋值,在这⾥默认着 ⾊器代码使⽤资源纹理(source texture)的像素颜⾊与透明值相乘。 fragmentShader: " varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; void main() { gl_FragColor = texture2D(source, qt_TexCoord0) * qt_Opacity; }" 《QmlBook》In Chinese 260着⾊器元素(Shader Elements) 在后⾯的例⼦中,我们将会展⽰⼀些简单的着⾊器例⼦。⾸先我们会集中在 ⽚段着⾊器上,然后在回到顶点着⾊器上。 《QmlBook》In Chinese 261着⾊器元素(Shader Elements) ⽚段着⾊器调⽤每个需要渲染的像素。我们将开发⼀个红⾊透镜,它将会增 加图⽚的红⾊通道的值。 配置场景(Setting up the scene) ⾸先我们配置我们的场景,在区域中央使⽤⼀个⺴格显⽰我们的源图⽚ (source image)。 import QtQuick 2.0 Rectangle { width: 480; height: 240 color: '#1e1e1e' Grid { anchors.centerIn: parent spacing: 20 rows: 2; columns: 4 Image { id: sourceImage width: 80; height: width source: 'assets/tulips.jpg' } } } ⽚段着⾊器(Fragement Shader) 《QmlBook》In Chinese 262⽚段着⾊器(Fragement Shader) 红⾊着⾊器(A red Shader) 下⼀步我们添加⼀个着⾊器,显⽰⼀个红⾊矩形框。由于我们不需要纹理, 我们从顶点着⾊器中移除纹理。 vertexShader: " uniform highp mat4 qt_Matrix; attribute highp vec4 qt_Vertex; void main() { gl_Position = qt_Matrix * qt_Vertex; }" fragmentShader: " uniform lowp float qt_Opacity; void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0) * qt_Opacity; }" 在⽚段着⾊器中,我们简单的给gl_FragColor赋值为vec4(1.0, 0.0, 0.0, 1.0),它代表红⾊, 并且不透明(alpha=1.0)。 《QmlBook》In Chinese 263⽚段着⾊器(Fragement Shader) 使⽤纹理的红⾊着⾊器(A red shader with texture) 现在我们想要将这个红⾊应⽤在纹理的每个像素上。我们需要将纹理加回顶 点着⾊器。由于我们不再在顶点着⾊器中做任何其它的事情,所以默认的顶 点着⾊器已经满⾜我们的要求。 ShaderEffect { id: effect2 width: 80; height: width property variant source: sourceImage visible: root.step>1 fragmentShader: " varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; void main() { gl_FragColor = texture2D(source, qt_TexCoord0) * vec4(1.0, 0.0, 0.0, 1.0) * qt_Opacity; }" } 完整的着⾊器重新包含我们的源图⽚作为属性,由于我们没有特殊指定,使 ⽤默认的顶点着⾊器,我没有重写顶点着⾊器。 《QmlBook》In Chinese 264⽚段着⾊器(Fragement Shader) 在⽚段着⾊器中,我们提取纹理⽚段texture2D(source,qt_TexCoord0),并且 与红⾊⼀起应⽤。 红⾊通道属性(The red channel property) 这样的代码⽤来修改红⾊通道的值看起来不是很好,所以我们想要将这个值 包含在QML这边。我们在ShaderEffect中增加⼀个redChannel属性,并在我 们的⽚段着⾊器中申明⼀个uniform lowpfloat redChannel。这就是从⼀个着 ⾊器代码中标记⼀个值到QML这边的⽅法,⾮常简单。 ShaderEffect { id: effect3 width: 80; height: width property variant source: sourceImage property real redChannel: 0.3 visible: root.step>2 fragmentShader: " varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; uniform lowp float redChannel; void main() { gl_FragColor = texture2D(source, qt_TexCoord0) * vec4(redChannel, 1.0, 1.0, 1.0) * qt_Opacity; }" 《QmlBook》In Chinese 265⽚段着⾊器(Fragement Shader) } 为了让这个透镜更真实,我们改变vec4颜⾊为vec4(redChannel, 1.0, 1.0, 1.0),这样其它颜⾊与1.0相乘,只有红⾊部分使⽤我们的redChannel变量。 红⾊通道的动画(The red channel animated) 由于redChannel属性仅仅是⼀个正常的属性,我们也可以像其它QML中的属 性⼀样使⽤动画。我们使⽤QML属性在GPU上改变这个值,来影响我们的着 ⾊器,这真酷! ShaderEffect { id: effect4 width: 80; height: width property variant source: sourceImage property real redChannel: 0.3 visible: root.step>3 NumberAnimation on redChannel { from: 0.0; to: 1.0; loops: Animation.Infinite; duration: 4000 } fragmentShader: " 《QmlBook》In Chinese 266⽚段着⾊器(Fragement Shader) varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; uniform lowp float redChannel; void main() { gl_FragColor = texture2D(source, qt_TexCoord0) * vec4(redChannel, 1.0, 1.0, 1.0) * qt_Opacity; }" } 下⾯是最后的结果。 在这4秒内,第⼆排的着⾊器红⾊通道的值从0.0到1.0。图⽚从没有红⾊信息 (0.0 red)到⼀个正常的图⽚(1.0 red)。 《QmlBook》In Chinese 267⽚段着⾊器(Fragement Shader) 在这个更加复杂的例⼦中,我们使⽤⽚段着⾊器创建⼀个波浪效果。波浪的 形成是基于sin曲线,并且它影响了使⽤的纹理坐标的颜⾊。 import QtQuick 2.0 Rectangle { width: 480; height: 240 color: '#1e1e1e' Row { anchors.centerIn: parent spacing: 20 Image { id: sourceImage width: 160; height: width source: "assets/coastline.jpg" } ShaderEffect { width: 160; height: width property variant source: sourceImage property real frequency: 8 property real amplitude: 0.1 property real time: 0.0 NumberAnimation on time { from: 0; to: Math.PI*2; duration: 1000; loops: Animation.Infinite } fragmentShader: " varying highp vec2 qt_TexCoord0; uniform sampler2D source; uniform lowp float qt_Opacity; uniform highp float frequency; uniform highp float amplitude; uniform highp float time; void main() { highp vec2 pulse = sin(time - frequency * qt_TexCoord0); 波浪效果(Wave Effect) 《QmlBook》In Chinese 268波浪效果(Wave Effect) highp vec2 coord = qt_TexCoord0 + amplitude * vec2(pulse.x, -pulse.x); gl_FragColor = texture2D(source, coord) * qt_Opacity; }" } } } 波浪的计算是基于⼀个脉冲与纹理坐标的操作。我们使⽤⼀个基于当前时间 与使⽤的纹理坐标的sin波浪⽅程式来实现脉冲。 highp vec2 pulse = sin(time - frequency * qt_TexCoord0); 离开了时间的因素,我们仅仅只有扭曲,⽽不是像波浪⼀样运动的扭曲。 我们使⽤不同的纹理坐标作为颜⾊。 highp vec2 coord = qt_TexCoord0 + amplitude * vec2(pulse.x, -pulse.x); 纹理坐标受我们的x脉冲值影响,结果就像⼀个移动的波浪。 《QmlBook》In Chinese 269波浪效果(Wave Effect) 如果我们没有在⽚段着⾊器中使⽤像素的移动,这个效果可以⾸先考虑使⽤ 顶点着⾊器来完成。 《QmlBook》In Chinese 270波浪效果(Wave Effect) 顶点着⾊器⽤来操作ShaderEffect提供的顶点。正常情况下,ShaderEffect有 4个顶点(左上top-left,右上top-right,左下bottom-left,右下bottom- right)。每个顶点使⽤vec4类型记录。为了实现顶点着⾊器的可视化,我们 将编写⼀个吸收的效果。这个效果通常被⽤来让⼀个矩形窗⼝消失为⼀个 点。 配置场景(Setting up the scene) ⾸先我们再⼀次配置场景。 import QtQuick 2.0 Rectangle { width: 480; height: 240 color: '#1e1e1e' Image { id: sourceImage width: 160; height: width 顶点着⾊器(Vertex Shader) 《QmlBook》In Chinese 271顶点着⾊器(Vertex Shader) source: "assets/lighthouse.jpg" visible: false } Rectangle { width: 160; height: width anchors.centerIn: parent color: '#333333' } ShaderEffect { id: genieEffect width: 160; height: width anchors.centerIn: parent property variant source: sourceImage property bool minimized: false MouseArea { anchors.fill: parent onClicked: genieEffect.minimized = !genieEffect.minimized } } } 这个场景使⽤了⼀个⿊⾊背景,并且提供了⼀个使⽤图⽚作为资源纹理的 ShaderEffect。使⽤image元素的原图⽚是不可⻅的,只是给我们的吸收效果 提供资源。此外我们在ShaderEffect的位置添加了⼀个同样⼤⼩的⿊⾊矩形 框,这样我们可以更加明确的知道我们需要点击哪⾥来重置效果。 《QmlBook》In Chinese 272顶点着⾊器(Vertex Shader) 点击图⽚将会触发效果,MouseArea覆盖了ShaderEffect。在onClicked操作 中,我们绑定了⾃定义的布尔变量属性minimized。我们稍后使⽤这个属性来 触发效果。 最⼩化与正常化(Minimize and normalize) 在我们配置好场景后,我们定义⼀个real类型的属性,叫做minimize,这个属 性包含了我们当前最⼩化的值。这个值在0.0到1.0之间,由⼀个连续的动画 来控制它。 property real minimize: 0.0 SequentialAnimation on minimize { id: animMinimize running: genieEffect.minimized PauseAnimation { duration: 300 } NumberAnimation { to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } SequentialAnimation on minimize { id: animNormalize running: !genieEffect.minimized NumberAnimation { to: 0; duration: 700; easing.type: Easing.InOutSine } 《QmlBook》In Chinese 273顶点着⾊器(Vertex Shader) PauseAnimation { duration: 1300 } } 这个动画绑定了由minimized属性触发。现在我们已经配置好我们的环境,最 后让我们看看顶点着⾊器的代码。 vertexShader: " uniform highp mat4 qt_Matrix; attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; varying highp vec2 qt_TexCoord0; uniform highp float minimize; uniform highp float width; uniform highp float height; void main() { qt_TexCoord0 = qt_MultiTexCoord0; highp vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, height, minimize); pos.x = mix(qt_Vertex.x, width, minimize); gl_Position = qt_Matrix * pos; }" 顶点着⾊器被每个顶点调⽤,在我们这个例⼦中,⼀共调⽤了四次。默认下 提供qt已定义的参数,如qt_Matrix,qt_Vertex,qt_MultiTexCoord0, qt_TexCoord0。我们在之前已经讨论过这些变量。此外我们从ShaderEffect 中链接minimize,width与height的值到我们的顶点着⾊器代码中。在main函 数中,我们将当前纹理值保存在qt_TexCoord()中,让它在⽚段着⾊器中可 ⽤。现在我们拷⻉当前位置,并修改顶点的x,y的位置。 highp vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, height, minimize); pos.x = mix(qt_Vertex.x, width, minimize); mix(...)函数提供了⼀种在两个参数之间(0.0到1.0)的线性插值的算法。在 《QmlBook》In Chinese 274顶点着⾊器(Vertex Shader) 我们的例⼦中,在当前y值与⾼度值之间基于minimize的值插值获得y值,x的 值获取类似。记住minimize的值是由我们的连续动画控制,并且在0.0到1.0 之间(反之亦然)。 这个结果的效果不是真正吸收效果,但是已经能朝着这个⺫标完成了⼀⼤ 步。 基础弯曲(Primitive Bending) 我们已经完成了最⼩化我们的坐标。现在我们想要修改⼀下对x值的操作,让 它依赖当前的y值。这个改变很简单。y值计算在前。x值的插值基于当前顶点 的y坐标。 highp float t = pos.y / height; pos.x = mix(qt_Vertex.x, width, t * minimize); 这个结果造成当y值⽐较⼤时,x的位置更靠近width的值。也就是说上⾯2个 顶点根本不受影响,它们的y值始终为0,下⾯两个顶点的x坐标值更靠近 width的值,它们最后转向同⼀个x值。 《QmlBook》In Chinese 275顶点着⾊器(Vertex Shader) import QtQuick 2.0 Rectangle { width: 480; height: 240 color: '#1e1e1e' Image { id: sourceImage width: 160; height: width source: "assets/lighthouse.jpg" visible: false } Rectangle { width: 160; height: width anchors.centerIn: parent color: '#333333' } ShaderEffect { id: genieEffect width: 160; height: width anchors.centerIn: parent property variant source: sourceImage property real minimize: 0.0 property bool minimized: false 《QmlBook》In Chinese 276顶点着⾊器(Vertex Shader) SequentialAnimation on minimize { id: animMinimize running: genieEffect.minimized PauseAnimation { duration: 300 } NumberAnimation { to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } SequentialAnimation on minimize { id: animNormalize running: !genieEffect.minimized NumberAnimation { to: 0; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1300 } } vertexShader: " uniform highp mat4 qt_Matrix; uniform highp float minimize; uniform highp float height; uniform highp float width; attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; varying highp vec2 qt_TexCoord0; void main() { qt_TexCoord0 = qt_MultiTexCoord0; // M1>> highp vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, height, minimize); highp float t = pos.y / height; pos.x = mix(qt_Vertex.x, width, t * minimize); gl_Position = qt_Matrix * pos; 更好的弯曲(Better Bending) 现在简单的弯曲并不能真正的满⾜我们的要求,我们将添加⼏个部件来提升 它的效果。⾸先我们增加动画,⽀持⼀个⾃定义的弯曲属性。这是⾮常必要 的,由于弯曲⽴即发⽣,y值的最⼩化需要被推迟。两个动画在同⼀持续时间 《QmlBook》In Chinese 277顶点着⾊器(Vertex Shader) 计算总和(300+700+100与700+1300)。 property real bend: 0.0 property bool minimized: false // change to parallel animation ParallelAnimation { id: animMinimize running: genieEffect.minimized SequentialAnimation { PauseAnimation { duration: 300 } NumberAnimation { target: genieEffect; property: 'minimize'; to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } // adding bend animation SequentialAnimation { NumberAnimation { target: genieEffect; property: 'bend' to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1300 } } } 此外,为了使弯曲更加平滑,不再使⽤y值影响x值的弯曲函数,pos.x现在依 赖新的弯曲属性动画: highp float t = pos.y / height; t = (3.0 - 2.0 * t) * t * t; pos.x = mix(qt_Vertex.x, width, t * bend); 弯曲从0.0平滑开始,逐渐加快,在1.0时逐渐平滑。下⾯是这个函数在指定 《QmlBook》In Chinese 278顶点着⾊器(Vertex Shader) 范围内的曲线图。对于我们,只需要关注0到1的区间。 想要获得最⼤化的视觉改变,需要增加我们的顶点数量。可以使⽤⺴眼 (mesh)来增加顶点: mesh: GridMesh { resolution: Qt.size(16, 16) } 现在ShaderEffect被分布为16x16顶点的⺴格,替换了之前2x2的顶点。这样 顶点之间的插值将会看起来更加平滑。 你可以看⻅曲线的变化,在最后让弯曲变得⾮常平滑。这让弯曲有了更加强 《QmlBook》In Chinese 279顶点着⾊器(Vertex Shader) ⼤的效果。 侧⾯收缩(Choosing Sides) 最后⼀个增强,我们希望能够收缩边界。边界朝着吸收的点消失。直到现在 它总是在朝着width值的点消失。添加⼀个边界属性,我们能够修改这个点在 0到width之间。 ShaderEffect { ... property real side: 0.5 vertexShader: " ... uniform highp float side; ... pos.x = mix(qt_Vertex.x, side * width, t * bend); " } 包装(Packing) 最后将我们的效果包装起来。将我们吸收效果的代码提取到⼀个叫做 《QmlBook》In Chinese 280顶点着⾊器(Vertex Shader) GenieEffect的⾃定义组件中。它使⽤ShaderEffect作为根元素。移除掉 MouseArea,这不应该放在组件中。绑定minimized属性来触发效果。 import QtQuick 2.0 ShaderEffect { id: genieEffect width: 160; height: width anchors.centerIn: parent property variant source mesh: GridMesh { resolution: Qt.size(10, 10) } property real minimize: 0.0 property real bend: 0.0 property bool minimized: false property real side: 1.0 ParallelAnimation { id: animMinimize running: genieEffect.minimized SequentialAnimation { PauseAnimation { duration: 300 } NumberAnimation { target: genieEffect; property: 'minimize'; to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } SequentialAnimation { NumberAnimation { target: genieEffect; property: 'bend' to: 1; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1300 } } } ParallelAnimation { id: animNormalize running: !genieEffect.minimized 《QmlBook》In Chinese 281顶点着⾊器(Vertex Shader) SequentialAnimation { NumberAnimation { target: genieEffect; property: 'minimize'; to: 0; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1300 } } SequentialAnimation { PauseAnimation { duration: 300 } NumberAnimation { target: genieEffect; property: 'bend' to: 0; duration: 700; easing.type: Easing.InOutSine } PauseAnimation { duration: 1000 } } } vertexShader: " uniform highp mat4 qt_Matrix; attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; uniform highp float height; uniform highp float width; uniform highp float minimize; uniform highp float bend; uniform highp float side; varying highp vec2 qt_TexCoord0; void main() { qt_TexCoord0 = qt_MultiTexCoord0; highp vec4 pos = qt_Vertex; pos.y = mix(qt_Vertex.y, height, minimize); highp float t = pos.y / height; t = (3.0 - 2.0 * t) * t * t; pos.x = mix(qt_Vertex.x, side * width, t * bend); gl_Position = qt_Matrix * pos; }" } 你现在可以像这样简单的使⽤这个效果: 《QmlBook》In Chinese 282顶点着⾊器(Vertex Shader) import QtQuick 2.0 Rectangle { width: 480; height: 240 color: '#1e1e1e' GenieEffect { source: Image { source: 'assets/lighthouse.jpg' } MouseArea { anchors.fill: parent onClicked: parent.minimized = !parent.minimized } } } 我们简化了代码,移除了背景矩形框,直接使⽤图⽚完成效果,替换了在⼀ 个单独的图像元素中加载它。 《QmlBook》In Chinese 283顶点着⾊器(Vertex Shader) 在最后的⾃定义效果例⼦中,我们将带来⼀个剧幕效果。这个效果是2011年 5⽉Qt实验室发布的着⾊器效果中的⼀部分。⺫前⺴址已经转到 blog.qt.digia.com,不知道还能不能找到。 当时我⾮常喜欢这些效果,剧幕效果是我最喜爱的⼀个。我喜欢剧幕打开然 后遮挡后⾯的背景对象。 我将代码移植适配到Qt5上,这⾮常简单。同时我做了⼀些简化让它能够更好 的展⽰。如果你对整个例⼦有兴趣,可以访问Qt实验室的博客。 只有⼀个⼩组件作为背景,剧幕实际上是⼀张图⽚,叫做fabric.jpg,它是 ShaderEffect的资源。整个效果使⽤顶点着⾊器来摆动剧幕,使⽤⽚段着⾊ 器提供阴影的效果。下⾯是⼀个简单的图⽚,让你更加容易理解代码。 剧幕效果(Curtain Effect) 《QmlBook》In Chinese 284剧幕效果(Curtain Effect) 剧幕的波形阴影通过⼀个在剧幕宽度上的sin曲线使⽤7的振幅来计算 (7*PI=221.99..)另⼀个重要的部分是摆动,当剧幕打开或者关闭时,使⽤ 动画来播放剧幕的topWidth。bottomWidth使⽤SpringAnimation来跟随 topWidth变化。这样我们就能创建出底部摆动的剧幕效果。计算得到的swing 提供了摇摆的强度,⽤来对顶点的y值进⾏插值。 剧幕效果放在CurtainEffect.qml组件中,fabric图像作为纹理资源。在阴影的 使⽤上没有新的东⻄加⼊,唯⼀不同的是在顶点着⾊器中操作gl_Postion和 ⽚段着⾊器中操作gl_FragColor。 import QtQuick 2.0 ShaderEffect { anchors.fill: parent mesh: GridMesh { resolution: Qt.size(50, 50) } property real topWidth: open?width:20 property real bottomWidth: topWidth property real amplitude: 0.1 property bool open: false property variant source: effectSource Behavior on bottomWidth { 《QmlBook》In Chinese 285剧幕效果(Curtain Effect) SpringAnimation { easing.type: Easing.OutElastic; velocity: 250; mass: 1.5; spring: 0.5; damping: 0.05 } } Behavior on topWidth { NumberAnimation { duration: 1000 } } ShaderEffectSource { id: effectSource sourceItem: effectImage; hideSource: true } Image { id: effectImage anchors.fill: parent source: "assets/fabric.jpg" fillMode: Image.Tile } vertexShader: " attribute highp vec4 qt_Vertex; attribute highp vec2 qt_MultiTexCoord0; uniform highp mat4 qt_Matrix; varying highp vec2 qt_TexCoord0; varying lowp float shade; uniform highp float topWidth; uniform highp float bottomWidth; uniform highp float width; uniform highp float height; uniform highp float amplitude; void main() { qt_TexCoord0 = qt_MultiTexCoord0; highp vec4 shift = vec4(0.0, 0.0, 0.0, 0.0); 《QmlBook》In Chinese 286剧幕效果(Curtain Effect) highp float swing = (topWidth - bottomWidth) * (qt_Vertex.y / height); shift.x = qt_Vertex.x * (width - topWidth + swing) / width; shade = sin(21.9911486 * qt_Vertex.x / width); shift.y = amplitude * (width - topWidth + swing) * shade; gl_Position = qt_Matrix * (qt_Vertex - shift); shade = 0.2 * (2.0 - shade ) * ((width - topWidth + swing) / width); }" fragmentShader: " uniform sampler2D source; varying highp vec2 qt_TexCoord0; varying lowp float shade; void main() { highp vec4 color = texture2D(source, qt_TexCoord0); color.rgb *= 1.0 - shade; gl_FragColor = color; }" } 这个效果在curtaindemo.qml⽂件中使⽤。 import QtQuick 2.0 Rectangle { id: root width: 480; height: 240 color: '#1e1e1e' Image { anchors.centerIn: parent source: 'assets/wiesn.jpg' } CurtainEffect { id: curtain anchors.fill: parent 《QmlBook》In Chinese 287剧幕效果(Curtain Effect) } MouseArea { anchors.fill: parent onClicked: curtain.open = !curtain.open } } 剧幕效果通过⾃定义的open属性打开。我们使⽤了⼀个MouseArea来触发打 开和关闭剧幕。 《QmlBook》In Chinese 288剧幕效果(Curtain Effect) 图像效果库是⼀个着⾊器效果的集合,是由Qt开发者提供制作的。它是⼀个 很好的⼯具,你可以将它应⽤在你的程序中,它也是⼀个学习如何创建着⾊ 器的例⼦。 图像效果库附带了⼀个⼿动测试平台,这个⼯具可以帮助你测试发现不同的 效果 测试⼯具在$QTDIR/qtgraphicaleffects/tests/manual/testbed下。 效果库包含了⼤约20种效果,下⾯是效果列表和⼀些简短的描述。 种类 效果 描述 混合(Blend) 混合(Blend) 使⽤混合模式合并两个资源项 颜⾊(Color) 亮度与对⽐度 (BrightnessContrast) 调整亮度与对⽐度 着⾊(Colorize) 设置HSL颜⾊空间颜⾊ 颜⾊叠加 (ColorOverlay) 应⽤⼀个颜⾊层 降低饱和度 (Desaturate) 减少颜⾊饱和度 伽⻢调整 调整发光度 Qt图像效果库(Qt GraphicsEffect Library) 《QmlBook》In Chinese 289Qt图像效果库(Qt GraphicsEffect Library) (GammaAdjust) 调整发光度 ⾊调饱和度 (HueSaturation) 调整HSL颜⾊空间颜⾊ ⾊阶调整(LevelAdjust) 调整RGB颜⾊空间颜⾊ 渐变 (Gradient) 圆锥渐变 (ConicalGradient) 绘制⼀个圆锥渐变 线性渐变 (LinearGradient) 绘制⼀个线性渐变 射线渐变 (RadialGradient) 绘制⼀个射线渐变 失真 (Distortion) 置换(Displace) 按照指定的置换源移动源项的 像素 阴影(Drop Shadow) 阴影 (DropShadow) 绘制⼀个阴影 内阴影(InnerShadow) 绘制⼀个内阴影 模糊 (Blur) 快速模糊(FastBlur) 应⽤⼀个快速模糊效果 ⾼斯模糊 (GaussianBlur) 应⽤⼀个⾼质量模糊效果 蒙版模糊(MaskedBlur) 应⽤⼀个多种强度的模糊效果 递归模糊 (RecursiveBlur) 重复模糊,提供⼀个更强的模 糊效果 运动模糊 (Motion Blur) ⽅向模糊 (DirectionalBlur) 应⽤⼀个⽅向的运动模糊效果 放射模糊(RadialBlur) 应⽤⼀个放射运动模糊效果 变焦模糊(ZoomBlur) 应⽤⼀个变焦运动模糊效果 发光(Glow) 发光(Glow) 绘制⼀个外发光效果 矩形发光 (RectangularGlow) 绘制⼀个矩形外发光效果 蒙版(Mask) 透明蒙版 (OpacityMask) 使⽤⼀个源项遮挡另⼀个源项 阈值蒙版 (ThresholdMask) 使⽤⼀个阈值,⼀个源项遮挡 另⼀个源项 下⾯是⼀个使⽤快速模糊效果的例⼦: 《QmlBook》In Chinese 290Qt图像效果库(Qt GraphicsEffect Library) import QtGraphicalEffects 1.0 Rectangle { width: 480; height: 240 color: '#1e1e1e' Row { anchors.centerIn: parent spacing: 16 Image { id: sourceImage source: "assets/tulips.jpg" width: 200; height: width sourceSize: Qt.size(parent.width, parent.height) smooth: true } FastBlur { width: 200; height: width source: sourceImage radius: blurred?32:0 property bool blurred: false Behavior on radius { NumberAnimation { duration: 1000 } } MouseArea { id: area anchors.fill: parent onClicked: parent.blurred = !parent.blurred } } } } 左边是原图⽚。点击右边的图⽚将会触发blurred属性,模糊在1秒内从0到 32。左边显⽰模糊后的图⽚。 《QmlBook》In Chinese 291Qt图像效果库(Qt GraphicsEffect Library) 《QmlBook》In Chinese 292Qt图像效果库(Qt GraphicsEffect Library) 在QtMultimedia模块中的multimedia元素可以播放和记录媒体资源,例如声 ⾳,视频,或者图⽚。解码和编码的操作由特定的后台完成。例如在Linux上 的gstreamer框架,Windows上的DirectShow,和OS X上的QuickTime。 multimedia元素不是QtQuick核⼼的接⼝。它的接⼝通过导⼊QtMultimedia 5.0来加⼊,如下所⽰: import QtMultimedia 5.0 多媒体(Multimedia) 《QmlBook》In Chinese 293多媒体(Multimedia) 在QML应⽤程序中,最基本的媒体应⽤是播放媒体。使⽤MediaPlayer元素 可以完成它,如果源是⼀个图⽚或者视频,可以选择结合VideoOutput元素。 MediaPlayer元素有⼀个source属性指向需要播放的媒体。当媒体源被绑定 后,简单的调⽤play函数就可以开始播放。 如果你想播放⼀个可视化的媒体,例如图⽚或者视频等,你需要配置⼀个 VideoOutput元素。MediaPlayer播放通过source属性与视频输出绑定。 在下⾯的例⼦中,给MediaPlayer元素⼀个视频⽂件作为source。⼀个 VideoOutput被创建和绑定到媒体播放器上。⼀旦主要部件完全初始化,例如 在Component.onCompleted中,播放器的play函数被调⽤。 import QtQuick 2.0 import QtMultimedia 5.0 import QtSystemInfo 5.0 Item { width: 1024 height: 600 MediaPlayer { id: player source: "trailer_400p.ogg" } VideoOutput { anchors.fill: parent source: player } Component.onCompleted: { player.play(); } ScreenSaver { 媒体播放(Playing Media) 《QmlBook》In Chinese 294媒体播放(Playing Media) screenSaverEnabled: false; } } // M1>> 除了上⾯介绍的视频播放,这个例⼦也包括了⼀⼩段代码⽤于禁⽌屏幕保 护。这将阻⽌视频被中断。通过设置ScreenSaver元素的 screenSaverEnabled属性为false来完成。通过导⼊QtSystemInfo 5.0可以使 ⽤ScreenSaver元素。 基础操作例如当播放媒体时可以通过MediaPlayer元素的volume属性来控制 ⾳量。还有⼀些其它有⽤的属性。例如,duration与position属性可以⽤来创 建⼀个进度条。如果seekable属性为true,当拨动进度条时可以更新position 属性。下⾯这个例⼦展⽰了在上⾯的例⼦基础上如何添加基础播放。 Rectangle { id: progressBar anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 100 height: 30 color: "lightGray" Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: player.duration>0?parent.width*player.position/player.duration:0 color: "darkGray" } MouseArea { 《QmlBook》In Chinese 295媒体播放(Playing Media) anchors.fill: parent onClicked: { if (player.seekable) player.position = player.duration * mouse.x/width; } } } 默认情况下position属性每秒更新⼀次。这意味着进度条将只会在⼤跨度下的 时间周期下才会更新,需要媒体持续时间⾜够⻓,进度条像素⾜够宽。然 ⽽,这个可以通过mediaObject属性的notifyInterval属性改变。它可以设置每 个position之间更新的毫秒数,增加⽤户界⾯的平滑度。 Connections { target: player onMediaObjectChanged: { if (player.mediaObject) player.mediaObject.notifyInterval = 50; } } 当使⽤MediaPlayer创建⼀个媒体播放器时,最好使⽤status属性来监听播放 器。这个属性是⼀个枚举,它枚举了播放器可能出现的状态,从 MediaPlayer.Buffered到MediaPlayer.InvalidMedia。下⾯是这些状态值的总 结: MediaPlayer.UnknownStatus - 未知状态 MediaPlayer.NoMedia - 播放器没有指定媒体资源,播放停⽌ MediaPlayer.Loading - 播放器正在加载媒体 MediaPlayer.Loaded - 媒体已经加载完毕,播放停⽌ 《QmlBook》In Chinese 296媒体播放(Playing Media) MediaPlayer.Stalled - 加载媒体已经停⽌ MediaPlayer.Buffering - 媒体正在缓冲 MediaPlayer.Buffered - 媒体缓冲完成 MediaPlayer.EndOfMedia - 媒体播放完毕,播放停⽌ MediaPlayer.InvalidMedia - ⽆法播放媒体,播放停⽌ 正如上⾯提到的这些枚举项,播放状态会随着时间变化。调⽤play,pause或 者stop将会切换状态,但由于媒体的原因也会影响这些状态。例如,媒体播 放完毕,它将会⽆效,导致播放停⽌。当前的播放状态可以使⽤ playbackState属性跟踪。这个值可能是MediaPlayer.PlayingState, MediaPlayer.PasuedState或者MediaPlayer.StoppedState。 使⽤autoPlay属性,MediaPlayer在source属性改变时将会尝试进⼊播放状 态。类似的属性autoLoad将会导致播放器在source属性改变时尝试加载媒 体。默认下autoLoad是被允许的。 当然也可以让MediaPlayer循环播放⼀个媒体项。loops属性控制source将会 被重复播放多少次。设置属性为MediaPlayer.Infinite将会导致不停的重播。 ⾮常适合持续的动画或者⼀个重复的背景⾳乐。 《QmlBook》In Chinese 297媒体播放(Playing Media) 当播放声⾳效果时,从请求播放到真实响应播放的响应时间⾮常重要。在这 种情况下,SoundEffect元素将会派上⽤场。设置source属性,⼀个简单调⽤ play函数会直接开始播放。 当敲击屏幕时,可以使⽤它来完成⾳效反馈,如下所⽰: SoundEffect { id: beep source: "beep.wav" } Rectangle { id: button anchors.centerIn: parent width: 200 height: 100 color: "red" MouseArea { anchors.fill: parent onClicked: beep.play() } } 这个元素也可以⽤来完成⼀个配有⾳效的转换。为了从转换触发,使⽤ ScriptAction元素。 SoundEffect { id: swosh source: "swosh.wav" } 声⾳效果(Sounds Effects) 《QmlBook》In Chinese 298声⾳效果(Sounds Effects) transitions: [ Transition { ParallelAnimation { ScriptAction { script: swosh.play(); } PropertyAnimation { properties: "rotation"; duration: 200; } } } ] 除了调⽤play函数,在MediaPlayer中类似属性也可以使⽤。⽐如volume和 loops。loops可以设置为SoundEffect.Infinite来提供⽆限重复播放。停⽌播放 调⽤stop函数。 注意 当后台使⽤PulseAudio时,stop将不会⽴即停⽌,但会阻⽌继续循环。这是 由于底层API的限制造成的。 《QmlBook》In Chinese 299声⾳效果(Sounds Effects) VideoOutput元素不被限制与MediaPlayer元素绑定使⽤的。它也可以直接⽤ 来加载实时视频资源显⽰⼀个流媒体。应⽤程序使⽤Camera元素作为资源。 来⾃Camera的视频流给⽤户提供了⼀个实时流媒体。 import QtQuick 2.0 import QtMultimedia 5.0 Item { width: 1024 height: 600 VideoOutput { anchors.fill: parent source: camera } Camera { id: camera } } 视频流(Video Streams) 《QmlBook》In Chinese 300视频流(Video Streams) Camera元素⼀个关键特性就是可以⽤来拍照。我们将在⼀个简单的定格动画 程序中使⽤到它。在这章中,你将学习如何显⽰⼀个视图查找器,截图和追 踪拍摄的图⽚。 ⽤户界⾯如下所⽰。它由三部分组成,背景是⼀个视图查找器,右边有⼀列 按钮,底部有⼀连串拍摄的图⽚。我们想要拍摄⼀系列的图⽚,然后点击 Play Sequence按钮。这将回放图⽚,并创建⼀个简单的定格电影。 相机的视图查找器部分是在VideoOutput中使⽤⼀个简单的Camera元素作为 资源。这将给⽤户显⽰⼀个来⾃相机的流媒体视频。 VideoOutput { anchors.fill: parent source: camera } Camera { 捕捉图像(Capturing Images) 《QmlBook》In Chinese 301捕捉图像(Capturing Images) id: camera } 使⽤⼀个⽔平放置的ListView显⽰来⾃ListModel的图⽚,这个部件叫做 imagePaths。在背景中使⽤⼀个半透明的Rectangle。 ListModel { id: imagePaths } ListView { id: listView anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.bottomMargin: 10 height: 100 orientation: ListView.Horizontal spacing: 10 model: imagePaths delegate: Image { source: path; fillMode: Image.PreserveAspectFit; height: 100; } Rectangle { anchors.fill: parent anchors.topMargin: -10 color: "black" opacity: 0.5 } } 为了拍摄图像,你需要知道Camera元素包含了⼀组⼦对象⽤来完成各种⼯ 作。使⽤Camera.imageCapture⽤来捕捉图像。当你调⽤capture⽅法时,⼀ 《QmlBook》In Chinese 302捕捉图像(Capturing Images) 张图⽚就被拍摄下来了。Camera.imageCapture的结果将会发送 imageCaptured信号,接着发送imageSaved信号。 Button { id: shotButton width: 200 height: 75 text: "Take Photo" onClicked: { camera.imageCapture.capture(); } } 为了拦截⼦元素的信号,需要⼀个Connections元素。在这个例⼦中,我们不 需要显⽰预览图⽚,仅仅只是将结果图⽚加⼊底部的ListView中。就如下⾯ 的例⼦展⽰的⼀样,图⽚保存的路径由信号的path参数提供。 Connections { target: camera.imageCapture onImageSaved: { imagePaths.append({"path": path}) listView.positionViewAtEnd(); } } 为了显⽰预览,连接imageCaptured信号,并且使⽤preview信号参数作为 Image元素的source。requestId信号参数与imageCaptured和imageSaved⼀ 起发送。这个值由capture⽅法返回。这样,就可以完整的跟踪拍摄的图⽚ 了。预览的图⽚⾸先被使⽤,然后替换为保存的图⽚。然⽽在这个例⼦中我 们不需要这样做。 最后是⾃动回放的部分。使⽤Timer元素来驱动它,并且加上⼀些 《QmlBook》In Chinese 303捕捉图像(Capturing Images) JavaScript。_imageIndex变量被⽤来跟踪当前显⽰的图⽚。当最后⼀张图⽚ 被显⽰时,回放停⽌。在例⼦中,当播放序列时,root.state被⽤来隐藏⽤户 界⾯。 property int _imageIndex: -1 function startPlayback() { root.state = "playing"; setImageIndex(0); playTimer.start(); } function setImageIndex(i) { _imageIndex = i; if (_imageIndex >= 0 && _imageIndex < imagePaths.count) image.source = imagePaths.get(_imageIndex).path; else image.source = ""; } Timer { id: playTimer interval: 200 repeat: false onTriggered: { if (_imageIndex + 1 < imagePaths.count) { setImageIndex(_imageIndex + 1); playTimer.start(); } else { setImageIndex(-1); root.state = ""; } } 《QmlBook》In Chinese 304捕捉图像(Capturing Images) } 《QmlBook》In Chinese 305捕捉图像(Capturing Images) Qt 5 multimedia接⼝没有提供播放列表。幸好,它⾮常容易实现。通过设置 模型⼦项与MediaPlayer元素可以实现它,如下所⽰。当playstate通过player 控制时,Playlist元素负责设置MediaPlayer的source。 Playlist { id: playlist mediaPlayer: player items: ListModel { ListElement { source: "trailer_400p.ogg" } ListElement { source: "trailer_400p.ogg" } ListElement { source: "trailer_400p.ogg" } } } MediaPlayer { id: player } Playlist元素的第⼀部分如下,注意使⽤setIndex函数来设置source元素的索 引值。我们也实现了next与previous函数来操作链表。 Item { id: root property int index: 0 property MediaPlayer mediaPlayer property ListModel items: ListModel {} ⾼级⽤法(Advanced Techniques) 10.5.1 实现⼀个播放列表(Implementing a Playlist) 《QmlBook》In Chinese 306⾼级⽤法(Advanced Techniques) function setIndex(i) { console.log("setting index to: " + i); index = i; if (index < 0 || index >= items.count) { index = -1; mediaPlayer.source = ""; } else mediaPlayer.source = items.get(index).source; } function next() { setIndex(index + 1); } function previous() { setIndex(index + 1); } 让播放列表⾃动播放下⼀个元素的诀窍是使⽤MediaPlayer的status属性。当 得到MediaPlayer.EndOfMedia状态时,索引值增加,恢复播放,或者当列表 达到最后时,停⽌播放。 Connections { target: root.mediaPlayer onStopped: { if (root.mediaPlayer.status == MediaPlayer.EndOfMedia) { root.next(); if (root.index == -1) root.mediaPlayer.stop(); else 《QmlBook》In Chinese 307⾼级⽤法(Advanced Techniques) root.mediaPlayer.play(); } } } 《QmlBook》In Chinese 308⾼级⽤法(Advanced Techniques) Qt的媒体应⽤程序接⼝提供了播放和捕捉视频和⾳频的机制。通过 VideoOutput元素,视频源能够在我们的⽤户界⾯上显⽰。通过MediaPlayer 元素,可以操作⼤多数的播放,SoundEffect被⽤于低延迟的声⾳。Camera 元素被⽤来截图或者显⽰⼀个实时的视频流。 总结(Summary) 《QmlBook》In Chinese 309总结(Summary) Qt5在C++中有丰富的⺴络相关的类。例如在http协议层上使⽤请求回答⽅式 的⾼级封装类如QNetworkRequest,QNetworkReply, QNetworkAccessManageer。也有在TCP/IP或者UDP协议层封装的低级类如 QTcpSocket,QTcpServer和QUdpSocket。还有⼀些额外的类⽤来管理代 理,⺴络缓冲和系统⺴络配置。 这章将不再阐述关于C++⺴络⽅⾯的知识,这章是关于QtQuick与⺴络的知 识。我们应该怎样连接QML/JS⽤户界⾯与⺴络服务,或者如何通过⺴络服务 来为我们⽤户界⾯提供服务。已经有很好的教材和⽰例覆盖了关于Qt/C++的 ⺴络编程。然后你只需要阅读这章相关的C++集成来满⾜你的QtQuick就可以 了。 ⺴络(Networking) 《QmlBook》In Chinese 310⽹络(Networking) 通过HTTP加载⼀个简单的⽤户界⾯,我们需要⼀个web服务器,它为UI⽂件 服务。但是⾸先我们需要有⽤户界⾯,我们在项⺫⾥创建⼀个创建了红⾊矩 形框的main.qml。 // main.qml import QtQuick 2.0 Rectangle { width: 320 height: 320 color: '#ff0000' } 我们加载⼀段python脚本来提供这个⽂件: $ cd # python -m SimpleHTTPServer 8080 现在我们可以通过http://localhost:8000/main.qml来访问,你可以像下⾯这样 测试: $ curl http://localhost:8000/main.qml 或者你可以⽤浏览器来访问。浏览器⽆法识别QML,并且⽆法通过⽂档来渲 染。我们需要创建⼀个可以浏览QML⽂档的浏览器。为了渲染⽂档,我们需 要指出qmlscene的位置。不幸的是qmlscene只能读取本地⽂件。我们为了突 破这个限制,我们可以使⽤⾃⼰写的qmlscene或者使⽤QML动态加载。我们 选择动态加载的⽅式。我们选择⼀个加载元素来加载远程的⽂档。 通过HTTP服务UI(Serving UI via HTTP) 《QmlBook》In Chinese 311通过HTTP服务UI(Serving UI via HTTP) // remote.qml import QtQuick 2.0 Loader { id: root source: 'http://localhost:8080/main2.qml' onLoaded: { root.width = item.width root.height = item.height } } 我们现在可以使⽤qmlscene来加载remote.qml⽂档。这⾥仍然有⼀个⼩问 题。加载器将会调整加载项的⼤⼩。我们的qmlscene需要适配⼤⼩。可以使 ⽤--resize-to-root选项来运⾏qmlscene。 $ qmlscene --resize-to-root remote.qml 按照root元素调整⼤⼩,告诉qmlscene按照root元素的⼤⼩调它的窗⼝⼤ ⼩。remote现在从本地服务器加载main.qml,并且可以⾃动调整加载的⽤户 界⾯。⽅便且简单。 注意 如果你不想使⽤⼀个本地服务器,你可以使⽤来⾃GitHub的gist服务。Gist 是⼀个在线剪切板服务,就像PasteBin等等。可以 在https://gist.github.com下使⽤。我创建了⼀个简单的gist例⼦,地址 是https://gist.github.com/jryannel/7983492。这将会返回⼀个绿⾊矩形 框。由于gist连接提供的是HTML代码,我们需要连接⼀个/raw来读取原始⽂ 件⽽不是HTML代码。 // remote.qml import QtQuick 2.0 《QmlBook》In Chinese 312通过HTTP服务UI(Serving UI via HTTP) Loader { id: root source: 'https://gist.github.com/jryannel/7983492/raw' onLoaded: { root.width = item.width root.height = item.height } } 从⺴络加载另⼀个⽂件,你只需要引⽤组件名。例如⼀个Button.qml,只要 它们在同⼀个远程⽂件夹下就能够像正常⼀样访问。 我们做了⼀个⼩实验。我们在远程端添加⼀个按钮作为可以复⽤的组件。 - src/main.qml - src/Button.qml 我们修改main.qml来使⽤button: import QtQuick 2.0 Rectangle { width: 320 height: 320 color: '#ff0000' Button { anchors.centerIn: parent text: 'Click Me' onClicked: Qt.quit() } } 11.1.1 ⺴络组件(Networked Components) 《QmlBook》In Chinese 313通过HTTP服务UI(Serving UI via HTTP) 再次加载我们的web服务器: $ cd src # python -m SimpleHTTPServer 8080 再次使⽤http加载远mainQML⽂件: $ qmlscene --resize-to-root remote.qml 我们看到⼀个错误: http://localhost:8080/main2.qml:11:5: Button is not a type 所以,在远程加载时,QML⽆法解决Button组件的问题。如果代码使⽤本地 加载qmlscene src/main.qml,将不会有问题。Qt能够直接解析本地⽂件,并 且检测哪些组件可⽤,但是使⽤http的远程访问没有“list-dir”函数。我们可以 在main.qml中使⽤import声明来强制QML加载元素: import "http://localhost:8080" as Remote ... Remote.Button { ... } 再次运⾏qmlscene后,它将正常⼯作: $ qmlscene --resize-to-root remote.qml 这是完整的代码: 《QmlBook》In Chinese 314通过HTTP服务UI(Serving UI via HTTP) // main2.qml import QtQuick 2.0 import "http://localhost:8080" 1.0 as Remote Rectangle { width: 320 height: 320 color: '#ff0000' Remote.Button { anchors.centerIn: parent text: 'Click Me' onClicked: Qt.quit() } } ⼀个更好的选择是在服务器端使⽤qmldir⽂件来控制输出: // qmldir Button 1.0 Button.qml 然后更新main.qml: import "http://localhost:8080" 1.0 as Remote ... Remote.Button { ... } 当从本地⽂件系统使⽤组件时,它们的创建没有延迟。当组件通过⺴络加载 时,它们的创建是异步的。创建时间的影响是未知的,当其它组件已经完成 时,⼀个组件可能还没有完成加载。当通过⺴络加载组件时,需要考虑这 些。 《QmlBook》In Chinese 315通过HTTP服务UI(Serving UI via HTTP) 当使⽤HTML项⺫时,通常需要使⽤模板驱动开发。服务器使⽤模板机制⽣ 成代码在服务器端对⼀个HTML根进⾏扩展。例如⼀个照⽚列表的列表头将 使⽤HTML编码,动态图⽚链表将会使⽤模板机制动态⽣成。通常这也可以 使⽤QML解决,但是仍然有⼀些问题。 ⾸先,HTML开发者这样做的原因是为了克服HTML后端的限制。在HTML中 没有组件模型,动态机制⽅⾯不得不使⽤这些机制或者在客户端边使⽤ javascript编程。很多的JS框架产⽣(jQuery,dojo,backbone, angular,...)可以⽤来解决这个问题,把更多的逻辑问题放在使⽤⺴络服务 连接的客户端浏览器。客户端使⽤⼀个web服务的接⼝(例如JSON服务,或 者XML数据服务)与服务器通信。这也适⽤于QML。 第⼆个问题是来⾃QML的组件缓冲。当QML访问⼀个组件时,缓冲渲染树 (render-tree),并且只加载缓冲版本来渲染。磁盘上的修改版本或者远程 的修改在没有重新启动客户端时不会被检测到。为了克服这个问题,我们需 要跟踪。我们使⽤URL后缀来加载链接(例如 http://localhost:8080/main.qml#1234),“#1234”就是后缀标识。HTTP服务 器总是为相同的⽂档服务,但是QML将使⽤完整的链接来保存这个⽂档,包 括链接标识。每次我们访问的这个链接的标识获得改变,QML缓冲⽆法获得 这个信息。这个后缀标识可以是当前时间的毫秒或者⼀个随机数。 Loader { source: 'http://localhost:8080/main.qml#' + new Date().getTime() } 总之,模板可以实现,但是不推荐,⽆法完整发挥QML的⻓处。⼀个更好的 ⽅法是使⽤web服务提供JSON或者XML数据服务。 模板(Templating) 《QmlBook》In Chinese 316模板(Templating) 从c++⽅⾯来看,Qt中完成http请求通常是使⽤QNetworkRequest和 QNetworkReply,然后使⽤Qt/C++将响应推送到集成的QML。所以我们尝试 使⽤QtQuick的⼯具给我们的⺴络信息尾部封装了⼩段信息,然后推送这些信 息。为此我们使⽤⼀个帮助对象来构造http请求,和循环响应。它使⽤java脚 本的XMLHttpRequest对象的格式。 XMLHttpRequest对象允许⽤户注册⼀个响应操作函数和⼀个链接。⼀个请求 能够使⽤http动作来发送(如get,post,put,delete,等等)。当响应到达 时,会调⽤注册的操作函数。操作函数会被调⽤多次。每次调⽤请求的状态 都已经改变(例如信息头部已接收,或者响应完成)。 下⾯是⼀个简短的例⼦: function request() { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { print('HEADERS_RECEIVED'); } else if(xhr.readyState === XMLHttpRequest.DONE) { print('DONE'); } } xhr.open("GET", "http://example.com"); xhr.send(); } 从⼀个响应中你可以获取XML格式的数据或者是原始⽂本。可以遍历XML结 果但是通常使⽤原始⽂本来匹配JSON格式响应。使⽤JSON.parse(text)可 以JSON⽂档将转换为JS对象使⽤。 ... HTTP请求(HTTP Requests) 《QmlBook》In Chinese 317HTTP请求(HTTP Requests) } else if(xhr.readyState === XMLHttpRequest.DONE) { var object = JSON.parse(xhr.responseText.toString()); print(JSON.stringify(object, null, 2)); } 在响应操作中,我们访问原始响应⽂本并且将它转换为⼀个javascript对象。 JSON对象是⼀个可以使⽤的JS对象(在javascript中,⼀个对象可以是对象 或者⼀个数组)。 注意 toString()转换似乎让代码更加稳定。在不使⽤显式的转换下我有⼏次都解析 错误。不确定是什么问题引起的。 让我们看看更加真实的例⼦。⼀个典型的例⼦是使⽤⺴络相册服务来取得公 共订阅中新上传的图⽚。我们可以使 ⽤http://api.flicker.com/services/feeds/photos_public.gne链接。不幸的是它 默认返回XML流格式的数据,在qml中可以很⽅便的使⽤XmlListModel来解 析。为了达到只关注JSON数据的⺫的,我们需要在请求中附加⼀些参数可 以得到JSON响应:http://api.flickr.com/services/feeds/photo_public.gne? format=json&nojsoncallback=1。这将会返回⼀个没有JSON回调的JSON响 应。 注意 ⼀个JSON回调将JSON响应包装在⼀个函数调⽤中。这是⼀个HTML编 程中的快捷⽅式,使⽤脚本标记来创建⼀个JSON请求。响应将触发本地定 义的回调函数。在QML中没有JSON回调的⼯作机制。 使⽤curl来查看响应: curl "http://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=munich" 11.3.1 Flickr调⽤(Flickr Call) 《QmlBook》In Chinese 318HTTP请求(HTTP Requests) 响应如下: { "title": "Recent Uploads tagged munich", ... "items": [ { "title": "Candle lit dinner in Munich", "media": {"m":"http://farm8.staticflickr.com/7313/11444882743_2f5f87169f_m.jpg"}, ... },{ "title": "Munich after sunset: a train full of \"must haves\" =", "media": {"m":"http://farm8.staticflickr.com/7394/11443414206_a462c80e83_m.jpg"}, ... } ] ... } JSON⽂档已经定义了结构体。⼀个对象包含⼀个标题和⼦项的属性。标题 是⼀个字符串,⼦项是⼀组对象。当转换⽂本为⼀个JSON⽂档后,你可以 单独访问这些条⺫,它们都是可⽤的JS对象或者结构体数组。 // JS code obj = JSON.parse(response); print(obj.title) // => "Recent Uploads tagged munich" for(var i=0; i', methods = ['GET']) def get_color(name): for color in colors: if color["name"] == name: return jsonify( color ) 我们再次使⽤curl测试,例如获取⼀个红⾊的接⼝。 curl -i -GET http://localhost:5000/colors/red 这将返回⼀个JSON数据的颜⾊。 ⺫前我们仅仅使⽤了HTTP GET⽅法。为了在服务器端创建⼀个接⼝,我们 使⽤POST⽅法,并且将新的颜⾊信息发使⽤POST数据发送。后缀与获取所 11.5.2 读取接⼝(Read Entry) 11.5.3 创建接⼝(Create Entry) 《QmlBook》In Chinese 325REST接⼜(REST API) 有颜⾊相同,但是我们需要使⽤⼀个POST请求。 @app.route('/colors', methods= ['POST']) def create_color(): color = { 'name': request.json['name'], 'value': request.json['value'] } colors.append(color) return jsonify( color ), 201 curl⾮常灵活,允许我们使⽤JSON数据作为新的接⼝包含在POST请求中。 curl -i -H "Content-Type: application/json" -X POST -d '{"name":"gray1","value":"#333"}' http://localhost:5000/colors 我们使⽤PUT HTTP⽅法来添加新的update接⼝。后缀与取得⼀个颜⾊接⼝ 相同。当颜⾊更新后,我们获取更新后JSON数据的颜⾊。 @app.route('/colors/', methods= ['PUT']) def update_color(name): for color in colors: if color["name"] == name: color['value'] = request.json.get('value', color['value']) return jsonify( color ) 在curl请求中,我们⽤JSON数据来定义更新值,后缀名⽤来识别哪个颜⾊需 要更新。 curl -i -H "Content-Type: application/json" -X PUT -d '{"value":"#666"}' http://localhost:5000/colors/red 11.5.4 更新接⼝(Update Entry) 《QmlBook》In Chinese 326REST接⼜(REST API) 使⽤DELETE HTTP来完成删除接⼝。使⽤与颜⾊相同的后缀,但是使⽤ DELETE HTTP⽅法。 @app.route('/colors/', methods=['DELETE']) def delete_color(name): success = False for color in colors: if color["name"] == name: colors.remove(color) success = True return jsonify( { 'result' : success } ) 这个请求看起来与GET请求⼀个颜⾊类似。 curl -i -X DELETE http://localhost:5000/colors/red 现在我们能够读取所有颜⾊,读取指定颜⾊,创建新的颜⾊,更新颜⾊和删 除颜⾊。我们知道使⽤HTTP后缀来访问我们的接⼝。 动作 HTTP协议 后缀 读取所有 GET http://localhost:5000/colors 创建接⼝ POST http://localhost:5000/colors 读取接⼝ GET http://localhost:5000/colors/name 更新接⼝ PUT http://localhost:5000/colors/name 删除接⼝ DELETE http://localhost:500/colors/name REST服务已经完成,我们现在只需要关注QML和客户端。为了创建⼀个简 单好⽤的接⼝,我们需要映射每个动作为⼀个独⽴的HTTP请求,并且给我们 的⽤户提供⼀个简单的接⼝。 11.5.5 删除接⼝(Delete Entry) 《QmlBook》In Chinese 327REST接⼜(REST API) 为了展⽰REST客户端,我们写了⼀个⼩的颜⾊表格。这个颜⾊表格显⽰了 通过HTTP请求从web服务取得的颜⾊。我们的⽤户界⾯提供以下命令: 获取颜⾊链表 创建颜⾊ 读取最后的颜⾊ 更新最后的颜⾊ 删除最后的颜⾊ 我们将我们的接⼝包装在⼀个JS⽂件中,叫做colorservice.js,并将它导⼊到 我们的UI中作为服务(Service)。在服务模块中,我们创建了帮助函数来为 我们构造HTTP请求: // colorservice.js function request(verb, endpoint, obj, cb) { print('request: ' + verb + ' ' + BASE + (endpoint?'/' + endpoint:'')) var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { print('xhr: on ready state change: ' + xhr.readyState) if(xhr.readyState === XMLHttpRequest.DONE) { if(cb) { var res = JSON.parse(xhr.responseText.toString()) cb(res); } } } xhr.open(verb, BASE + (endpoint?'/' + endpoint:'')); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept', 'application/json'); var data = obj?JSON.stringify(obj):'' xhr.send(data) } 11.5.6 REST客户端(REST Client) 《QmlBook》In Chinese 328REST接⼜(REST API) 包含四个参数。verb,定义了使⽤HTTP的动作(GET,POST,PUT, DELETE)。第⼆个参数是作为基础地址的后缀(例 如’http://localhost:5000/colors')。第三个参数是可选对象,作为JSON数据 发送给服务的数据。最后⼀个选项是定义当响应返回时的回调。回调接收⼀ 个响应数据的响应对象。 在我们发送请求前,我们需要明确我们发送和接收的JSON数据修改的请求 头。 // colorservice.js function get_colors(cb) { // GET http://localhost:5000/colors request('GET', null, null, cb) } function create_color(entry, cb) { // POST http://localhost:5000/colors request('POST', null, entry, cb) } function get_color(name, cb) { // GET http://localhost:5000/colors/ request('GET', name, null, cb) } function update_color(name, entry, cb) { // PUT http://localhost:5000/colors/ request('PUT', name, entry, cb) } function delete_color(name, cb) { // DELETE http://localhost:5000/colors/ request('DELETE', name, null, cb) } 这些代码在服务实现中。在UI中我们使⽤服务来实现我们的命令。我们有⼀ 《QmlBook》In Chinese 329REST接⼜(REST API) 个存储id的ListModel和存储数据的gridModel为GridView提供数据。命令使⽤ Button元素来发送。 读取服务器颜⾊链表。 // rest.qml import "colorservice.js" as Service ... // read colors command Button { text: 'Read Colors'; onClicked: { Service.get_colors( function(resp) { print('handle get colors resp: ' + JSON.stringify(resp)); gridModel.clear(); var entries = resp.data; for(var i=0; i', text) text = '' } } WebSocket { id: socket url: "ws://localhost:3000" active: true onTextMessageReceived: { box.append('<', message) } onStatusChanged: { if (socket.status == WebSocket.Error) { box.append('#', 'socket error ' + socket.errorString) } else if (socket.status == WebSocket.Open) { box.append('#', 'socket open') } else if (socket.status == WebSocket.Closed) { box.append('#', 'socket closed') } } } } 你⾸先需要运⾏服务器,然后是客户端。在我们简单例⼦中没有客户端重连 的机制。 运⾏服务器 $ cd ws_server $ node server.js 运⾏客户端 $ cd ws_client $ qmlscene ws_client.qml 《QmlBook》In Chinese 341Web Sockets 当输⼊⽂本并点击发送后,你可以看到类似下⾯这样。 《QmlBook》In Chinese 342Web Sockets 这章我们讨论了关于QML的⺴络应⽤。请记住Qt已在本地端提供了丰富的⺴ 络接⼝可以在QML中使⽤。但是这⼀章的我们是想推动QML的⺴络运⽤和如 何与云服务集成。 总结(Summary) 《QmlBook》In Chinese 343总结(Summary) 本章将介绍在Qt5中使⽤QtQuick存储数据。QtQuick只提供了有限的⽅法来 直接存储本地数据。在这样的场景下,它更多的扮演了⼀个浏览者的⾓⾊。 在⼤多数项⺫中,存储数据由C++后端来完成,并需要将这个功能导⼊到 QtQuick前端。QtQucik没有提供类似Qt C++的主机⽂件系统接⼝来读取和写 ⼊⽂件。所以后端⼯程师需要编写⼀个这样的插件或者使⽤⺴络通道与本地 服务器通信来提供这些功能。 每个应⽤程序都需要持续的存储少量或者⼤量的信息。可以存储在本地⽂件 系统或者远程服务器上。⼀些信息将会被结构化、简单化例如程序配置信 息,⼀些信息将会巨⼤并且复杂例如⽂档⽂件,⼀些信息将会巨⼤并且结构 化需要与某种数据库连接。在这章我们将会讨论如何使⽤QtQuick通过⺴络和 本地的⽅式存储数据。 存储(Storage) 《QmlBook》In Chinese 344存储(Storage) Qt⾃⾝就提供了基于系统⽅式的应⽤程序配置(⼜名选项,偏好)C++类 QSettings。它使⽤基于当前操作系统的⽅式存储配置。此外,它⽀持通⽤的 INI⽂件格式⽤来操作跨平台的配置⽂件。 在Qt5.2中,配置(Settings)被加⼊到QML中。编程接⼝仍然在实验模块 中,这意味着接⼝可能在未来会改变。这⾥需要注意。 这⾥有⼀个⼩例⼦,对⼀个矩形框配置颜⾊。每次⽤户点击窗⼝⽣成⼀个新 的随机颜⾊。应⽤程序关闭后重启你将会看到你最后看到的颜⾊。 默认的颜 ⾊是⽤来初始化根矩形框的颜⾊。 import QtQuick 2.0 import Qt.labs.settings 1.0 Rectangle { id: root width: 320; height: 240 color: '#000000' Settings { id: settings property alias color: root.color } MousArea { anchors.fill: parent onClicked: root.color = Qt.hsla(Math.random(), 0.5, 0.5, 1.0); } } 每次颜⾊值的变化都被存储在配置中。这可能不是我们需要的。只有在要求 使⽤标准属性的时候才存储配置。 Rectangle { Settings 《QmlBook》In Chinese 345配置(Settings) id: root color: settings.color Settings { id: settings property color color: '#000000' } function storeSettings() { // executed maybe on destruction settings.color = root.color } } 可以使⽤category属性存储不同种类的配置。 Settings { category: 'window' property alias x: window.x property alias y: window.x property alias width: window.width property alias height: window.height } 配置同城根据你的应⽤程序名称,组织和域存储。这些信息通常在你的C++ main函数中设置。 int main(int argc, char** argv) { ... QCoreApplication::setApplicationName("Awesome Application"); QCoreApplication::setOrganizationName("Awesome Company"); QCoreApplication::setOrganizationDomain("org.awesome"); ... } 《QmlBook》In Chinese 346配置(Settings) Qt Quick⽀持⼀个与浏览器由区别的本地存储编程接⼝。需要使⽤"import QtQuick.LocalStorage 2.0"语句来导⼊后才能使⽤这个编程接⼝。 通常使⽤基于给定的数据库名称和版本号使⽤系统特定位置的唯⼀⽂件ID号 来存储数据到⼀个SQLITE数据库中。⽆法列出或者删除已有的数据库。你可 以使⽤QQmlEngine::offlineStoragePate()来寻找本地存储。 使⽤这个编程接⼝你⾸选需要创建⼀个数据库对象,然后在这个数据库上创 建数据库事务。每个事务可以包含⼀个或多个SQL查询。当⼀个SQL查询在 事务中失败后,事务会回滚。 例如你可以使⽤本地存储从⼀个简单的注释表中读取⼀个⽂本列: import QtQuick 2.2 import QtQuick.LocalStorage 2.0 Item { Component.onCompleted: { var db = LocalStorage.openDatabaseSync("MyExample", "1.0", "Example database", 10000); db.transaction( function(tx) { var result = tx.executeSql('select * from notes'); for(var i = 0; i < result.rows.length; i++) { print(result.rows[i].text); } } }); } } 本地存储 - SQL(Local Storage - SQL) 疯狂的矩形框(Crazy Rectangle) 《QmlBook》In Chinese 347本地存储 - SQL(Local Storage - SQL) 假设我们想要存储⼀个矩形在场景中的位置。 下⾯是我们的基础代码。 import QtQuick 2.2 Item { width: 400 height: 400 Rectangle { id: crazy objectName: 'crazy' width: 100 height: 100 《QmlBook》In Chinese 348本地存储 - SQL(Local Storage - SQL) x: 50 y: 50 color: "#53d769" border.color: Qt.lighter(color, 1.1) Text { anchors.centerIn: parent text: Math.round(parent.x) + '/' + Math.round(parent.y) } MouseArea { anchors.fill: parent drag.target: parent } } } 可以⾃由的拖动这个矩形。当关闭这个应⽤程序并再次打开时,这个矩形框 仍然在相同的位置。 现在我们将添加矩形的x/y坐标值存储到SQL数据库中。⾸先我们需要添加⼀ 个初始化、读取和保存数据库功能。这些功能在组件构造和组件销毁时被调 ⽤。 import QtQuick 2.2 import QtQuick.LocalStorage 2.0 Item { // reference to the database object property var db; function initDatabase() { // initialize the database object } function storeData() { // stores data to DB } function readData() { 《QmlBook》In Chinese 349本地存储 - SQL(Local Storage - SQL) // reads and applies data from DB } Component.onCompleted: { initDatabase(); readData(); } Component.onDestruction: { storeData(); } } 你也可以调⽤已有的JS库提取相关数据库代码来完成所有的逻辑。如果这个 逻辑变得更加复杂这将是最好的解决⽅案。 在数据库初始化函数中,我们创建⼀个数据库对象并且确保SQL表已经被创 建。 function initDatabase() { print('initDatabase()') db = LocalStorage.openDatabaseSync("CrazyBox", "1.0", "A box who remembers its position", 100000); db.transaction( function(tx) { print('... create table') tx.executeSql('CREATE TABLE IF NOT EXISTS data(name TEXT, value TEXT)'); }); } 应⽤程序下⼀步调⽤读取函数来读取数据库中已有的数据。这⾥我们需要区 分数据库表中是否已有数据。我们观察有多少条语句返回来检查是否已有数 据。 function readData() { print('readData()') if(!db) { return; } 《QmlBook》In Chinese 350本地存储 - SQL(Local Storage - SQL) db.transaction( function(tx) { print('... read crazy object') var result = tx.executeSql('select * from data where name="crazy"'); if(result.rows.length === 1) { print('... update crazy geometry') // get the value column var value = result.rows[0].value; // convert to JS object var obj = JSON.parse(value) // apply to object crazy.x = obj.x; crazy.y = obj.y; } }); } 我们希望在将数据作为⼀个JSON字符串存储在⼀列值中。这与典型的SQL 不同,但是可以很好的与JS代码结合。所以我们将它存储为⼀个JS对象的可 以使⽤JSON stringif/parse⽅法的数据替代使⽤x,y作为属性值放在数据库表 中。最后我们获取⼀个包含x,y属性有效的JS对象,我们可以将它应⽤在我们 疯狂的矩形中。 为了保存数据,我们需要区分更新和插⼊的情况。当⼀个记录已经存在时我 们使⽤更新,如果没有记录则将它插⼊在“crazy”下。 function storeData() { print('storeData()') if(!db) { return; } db.transaction( function(tx) { print('... check if a crazy object exists') var result = tx.executeSql('SELECT * from data where name = "crazy"'); // prepare object to be stored as JSON var obj = { x: crazy.x, y: crazy.y }; if(result.rows.length === 1) {// use update print('... crazy exists, update it') result = tx.executeSql('UPDATE data set value=? where name="crazy"', [JSON.stringify(obj)]); } else { // use insert print('... crazy does not exists, create it') 《QmlBook》In Chinese 351本地存储 - SQL(Local Storage - SQL) result = tx.executeSql('INSERT INTO data VALUES (?,?)', ['crazy', JSON.stringify(obj)]); } }); } 替代选择所有记录的设置,我们也可以使⽤SQLITE计数函数:SELECT COUNT(*) from data where name = "crazy",将返回使⽤⾏选择查询的结 果。否则这将是⼀个通⽤的SQL代码。所谓额外的特性,我们在查询中使 ⽤?绑定SQL值。 现在你可与拖动这个矩形框当你退出应⽤程序时会将x/y坐标值存储到数据 库,下次启动应⽤程序时矩形框将使⽤存储的x/y坐标值定位。 《QmlBook》In Chinese 352本地存储 - SQL(Local Storage - SQL) 直接从QML中存储信息,上⾯的这些⽅法是主要存储⽅法。事实上QtQuick 最有效的存储⽅法是使⽤C++扩展接⼝调⽤本地存储系统或者类似Qt云存储 使⽤⺴络编程接⼝调⽤远程存储系统。 其它存储接⼝(Other Storage APIs) 《QmlBook》In Chinese 353其它存储接⼜(Other Storage APIs) 到现在,我们已经将QML作为⼀个⼯具⽤来构造静态场景和静态场景的导 航。根据不同的状态和逻辑规则,⼀个实时动态的⽤户界⾯已经被创建。通 过使⽤QML和JavaScript以更加动态的⽅式,进⼀步的扩⼤灵活性。组件可 以在运⾏时加载和实例化,元素能够被销毁。动态创建的⽤户界⾯能够被存 储在磁盘上,并且恢复。 动态QML(Dynamic QML) 《QmlBook》In Chinese 354动态QML(Dynamic QML) 动态加载QML不同组成部分最简单的⽅法是使⽤加载元素项(Loader element)。它作为⼀个占位符项⽤来加载项。项的加载通过资源属性 (source property)或者资源组件(sourceCompontent)属性控制。加载元 素项通过给定的URL链接加载项,然后实例化⼀个组件。 加载元素项(loader)作为⼀个占位符⽤于被加载项的加载。它的⼤⼩基于 被加载项的⼤⼩⽽定,反之亦然。如果加载元素定义了⼤⼩,或者通过锚定 (anchoring)定义了宽度和⾼度,被加载项将会被设置为加载元素项的⼤ ⼩。如果加载元素项没有设置⼤⼩,它将会根据被加载项的⼤⼩⽽定。 下⾯例⼦演⽰了使⽤加载元素项(Loader Element)将两个分离的⽤户界⾯ 部分加载到⼀个相同的空间。这个主意是我们有⼀个快速拨号界⾯,可以是 数字界⾯或模拟界⾯。如下⾯插图所⽰。表盘周围的数字不受被加载项影 响。 动态加载组件(Loading Components Dynamically) 《QmlBook》In Chinese 355动态加载组件(Loading Components Dynamically) ⾸先,在应⽤程序中声明⼀个加载元素项(Loader element)。注意,资源 属性(source property)已经被忽略。这是因为资源取决于当前⽤户界⾯状 态。 《QmlBook》In Chinese 356动态加载组件(Loading Components Dynamically) Loader { id: dialLoader anchors.fill: parent } 拨号加载器(dialLoader)的⽗项的状态属性改变元素驱动根据不同状态加 载不同的QML⽂件。在这个例⼦中,资源属性(source property)是⼀个相 对⽂件路径,但是它可以是⼀个完整的URL链接,通过⺴络获取加载项。 states: [ State { name: "analog" PropertyChanges { target: analogButton; color: "green"; } PropertyChanges { target: dialLoader; source: "Analog.qml"; } }, State { name: "digital" PropertyChanges { target: digitalButton; color: "green"; } PropertyChanges { target: dialLoader; source: "Digital.qml"; } } ] 为了使被加载项更加⽣动,它的速度属性必须根项的速度属性绑定。不能够 使⽤直接绑定来绑定属性,因为项不是总在加载,并且这会随着时间⽽改 变。需要使⽤⼀个东⻄来替换绑定元素(Binding Element)。绑定⺫标属性 (target property),每次加载元素项改变时会触发已加载完成 (onLoaded)信号。 Loader { id: dialLoader anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top 《QmlBook》In Chinese 357动态加载组件(Loading Components Dynamically) anchors.bottom: analogButton.top onLoaded: { binder.target = dialLoader.item; } } Binding { id: binder property: "speed" value: speed } 当被加载项加载完成后,加载完成信号(onLoaded)会触发加载QML的动 作。类似的,QML完成加载以来与组建构建完成 (Component.onCompleted)信号。所有组建都可以使⽤这个信号,⽆论它 们何时加载。例如,当整个⽤户界⾯完成加载后,整个应⽤程序的根组建可 以使⽤它来启动⾃⼰。 《QmlBook》In Chinese 358动态加载组件(Loading Components Dynamically) 动态创建QML元素时,⽆法使⽤onSignalName静态配置来连接信号。必须 使⽤连接元素(Connection element)来完成连接信号。它可以连接⼀个⺫ 标元素任意数量的信号。 通过设置连接元素(Connection element)的⺫标属性,信号可以像正常的 ⽅法连接。也就是使⽤onSignalName⽅法。不管怎样,通过改变⺫标属性可 以在不同的时间监控不同的元素。 在上⾯这个例⼦中,⽤户界⾯由两个可点击区域组成。当其中⼀个区域点击 后,会使⽤⼀个闪烁的动画。左边区域的代码段如下所⽰。在⿏标区域 (MouseArea)中,左点击动画(leftClickedAnimation)被触发,导致区域 闪烁。 Rectangle { id: leftRectangle 间接连接(Connecting Indirectly) 《QmlBook》In Chinese 359间接连接(Connecting Indirectly) width: 290 height: 200 color: "green" MouseArea { id: leftMouseArea anchors.fill: parent onClicked: leftClickedAnimation.start(); } Text { anchors.centerIn: parent font.pixelSize: 30 color: "white" text: "Click me!" } } 除了两个可点击区域,还使⽤了⼀个连接元素(Connection element)。当 状态为激活时会触发第三个动画,即元素的⺫标。 Connections { id: connections onClicked: activeClickedAnimation.start(); } 为了确定⿏标区域的⺫标,定义了两种状态。注意我们⽆法使⽤属性改变元 素(PropertyChanges element)来设置⺫标属性,因为它已经包含了⼀个⺫ 标属性。利⽤状态改变脚本(StateChangeScript)来完成。 states: [ State { name: "left" StateChangeScript { script: connections.target = leftMouseArea 《QmlBook》In Chinese 360间接连接(Connecting Indirectly) } }, State { name: "right" StateChangeScript { script: connections.target = rightMouseArea } } ] 当尝试运⾏这个例⼦时,需要注意当多个信号被处理调⽤所有操作时,执⾏ 的顺序是未定义的。 当创建⼀个连接元素(Connection element)未指定⺫标属性时,默认的属 性是⽗对象。这意味着需要显式的设置NULL来避免捕获来⾃⽗对象的信号, 直到⺫标被设置。这种⾏为使得基于连接元素(Connection element)创建 ⾃定义信号处理组件成为可能。使⽤这种⽅式可以将信号的处理代码封装和 再使⽤。 在下⾯这个例⼦中,闪烁组件能够被放在任何的⿏标区域(MouseArea)中. 点击后会触发动画,导致⽗对象闪烁。在同⼀个⿏标区域(MouseArea)的 实际任务被触发时也可以被调⽤。这从实际的动作中,分离了标准的⽤户反 馈,闪烁。 import QtQuick 2.0 Connections { onClicked: { // Automatically targets the parent } } 只需要简单的在每个⿏标区域(MouseArea)实例化⼀个闪烁组件来实现闪 烁。 《QmlBook》In Chinese 361间接连接(Connecting Indirectly) import QtQuick 2.0 Item { // A background flasher that flashes the background of any parent MouseArea } 当你使⽤⼀个连接元素(Connection element)来监控不同类型的⺫标元素 的信号时,你可能会发现在在某些场景下会有来⾃不同⺫标的可⽤信号。这 将导致连接元素(Connections element)由于丢失信号输出运⾏错误(run- time errors)。为了避免这个问题,设置忽略未知信号 (ignoreUnknownSignal)属性为true,可以忽略这些错误。 《QmlBook》In Chinese 362间接连接(Connecting Indirectly) 与⽆法直接连接动态创建元素的信号类似,也⽆法脱离桥接元素(bridge element)与动态创建元素绑定属性。为了绑定任意元素的属性,包括动态创 建元素,需要使⽤绑定元素(Binding element)。 绑定元素(Bindging element)允许你指定⼀个⺫标元素(target element),⼀个属性⽤来绑定,⼀个值⽤来绑定这个属性。通过使⽤绑定元 素(Binding elelemt),例如,绑定⼀个动态加载元素(dynamically loaded element)的属性。在这个章节中有个⼊⻔实例如下所⽰。 Loader { id: dialLoader anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: analogButton.top onLoaded: { binder.target = dialLoader.item; } } Binding { id: binder property: "speed" value: speed } 通常不会设置⼀个绑定的⺫标元素,或者不会有⼀个给定的属性。当绑定激 活时使⽤绑定元素的属性来限制时间。例如,它可以⽤来限制⽤户界⾯的特 定模式。 (间接绑定)Binding Indirectly 《QmlBook》In Chinese 363间接绑定(Binding Indirectly) 加载元素使得动态填充⽤户界⾯成为可能。但是接⼝的结构仍然是静态的。 通过JavaScript可以更近⼀步的完成QML元素的动态实例化。 在我们深⼊讨论动态创建元素的细节之前,我们需要明⽩⼯作的流程。当从 ⼀个⽂件或者⺴络加载⼀块QML时,组件已经被创建。组件封装了解释执⾏ 的QML代码⽤来创建项。这意味着⼀块QML代码和实例化项是分为两个步骤 进⾏的。⾸先在组件中解释执⾏QML代码,然后组件被⽤来实例化创建项对 象。 除了从存储在⽂件或者服务器上的QML代码创建元素,也可以直接从包含 QML代码的⽂本字符串中创建QML对象。动态创建项也类似的⽅式再处理⼀ 次就可以了。 创建与销毁对象(Creating and Destroying Objects) 《QmlBook》In Chinese 364创建与销毁对象(Creating and Destroying Objects) 加载⼀块QML代码时,它⾸先会被解释执⾏为⼀个组件。这⼀步包含了加载 依赖和验证代码。QML的来源可以是本地⽂件,Qt资源⽂件,或者⼀个指定 的URL⺴络地址。这意味着加载时间不确定。例如⼀个不需要加载任何依赖 位于内存(RAM)中的Qt资源⽂件加载⾮常快,或者⼀个需要加载多种依赖 位于⼀个缓慢的服务器中加载需要很⻓的时间。 创建⼀个组件的状态可以⽤来跟踪它的状态属性。可以使⽤的状态值包括组 件为空(Component.NULL)、组件加载中(Component.Loading)、组件 可⽤(Component.Ready)和组件错误(Component.Error)。从空 (NULL)状态到加载中(Loading)再到可⽤(Ready)通常是⼀个⼯作 流。在任何⼀个阶段状态都可以变为错误(Error)。在这种情况下,组件⽆ 法被⽤来创建新的对象实例。Component.errorString()函数⽤来检索⽤户可 读的错误描述。 当加载连接缓慢的组件时,可以使⽤进度(progress)属性。它的范围从0.0 意味着为加载任何东⻄,到1.0表明加载已完成。当组件的状态改变为可⽤ (Ready)时,组件可以⽤实例化对象。下⾯的代码演⽰了如何实现这样的 ⽅法,考虑组件变为可⽤或者创建失败,同时组件加载时间可能会⽐较慢。 var component; function createImageObject() { component = Qt.createComponent("dynamic-image.qml"); if (component.status === Component.Ready || component.status === Component.Error) finishCreation(); else component.statusChanged.connect(finishCreation); } function finishCreation() { if (component.status === Component.Ready) (动态加载和实例化项)Dynamically Loading and Instantiating Items 《QmlBook》In Chinese 365动态加载和实例化项(Dynamically Loading and Instantiating Items) { var image = component.createObject(root, {"x": 100, "y": 100}); if (image == null) console.log("Error creating image"); } else if (component.status === Component.Error) console.log("Error loading component:", component.errorString()); } 上⾯的代码是源⽂件中的JavaScript代码,来⾃main QML⽂件。 import QtQuick 2.0 import "create-component.js" as ImageCreator Item { id: root width: 1024 height: 600 Component.onCompleted: ImageCreator.createImageObject(); } ⼀个组件的创建对象(createObject)对象函数⽤于创建⼀个实例化对象, 如上所⽰。这不仅仅⽤于动态动态加载组件,也⽤语⾔QML代码中的组件内 联。这样产⽣的对象可以像其它的对象⼀样⽤于QML场景中。唯⼀的不同是 这些对象没有id。 创建对象(createObject)函数接受两个参数。第⼀个参数是⽗对象。第⼆ 个参数是按照格式{"name": value, "name": value}组成的⼀串属性和值。下⾯ 的例⼦演⽰了这种⽤法。注意,属性参数是可选的。 var image = component.createObject(root, {"x": 100, "y": 100}); 《QmlBook》In Chinese 366动态加载和实例化项(Dynamically Loading and Instantiating Items) 注意 ⼀个动态创建的组件实例不同于⼀个内联组件元素(in-line Component element)。内联组件元素也提供了函数⽤来动态实例化对象。 《QmlBook》In Chinese 367动态加载和实例化项(Dynamically Loading and Instantiating Items) 有时,可以很⽅便的从QML⽂本字符串中实例化⼀个对象。别的不说,这⽐ 将代码从源⽂件中分离后拿出来快。为了实现这个功能,需要使⽤ Qt.createQmlObject函数。 这个函数接受三个参数:qml,parent和filepath。qml参数包含了⽤来实例化 的QML代码字符串。parent参数为新创建的对象提供了⼀个⽗对象。filepath 参数⽤于存储创建对象时的错误报告。这个函数的结果返回⼀个新的对象或 者⼀个NULL。 警告 createQmlObject函数通常会⽴即返回结果。为了成功调⽤这个函数,所有 的依赖调⽤需要保证已经被加载。这意味着如果函数调⽤了未加载的组件, 这个调⽤就会失败并且返回null。为了更好的处理这个问题,必须使⽤ createComponent/createObject⽅法。 使⽤Qt.createQmlObject函数创建对象与其它的动态创建对象类似。这说明 与其它创建的QML对象⼀样,也没有id。在下⾯的例⼦中,当根元素创建完 成后,从内联QML代码中实例化了⼀个新的矩形元素(Rectangle element)。 import QtQuick 2.0 Item { id: root width: 1024 height: 600 function createItem() { Qt.createQmlObject("import QtQuick 2.0; Rectangle { x: 100; y: 100; width: 100; 从⽂本中动态实例化项(Dynamically Instantiating Items from Text) 《QmlBook》In Chinese 368从⽂本中动态实例化项(Dynamically Instantiating Items from Text) height:100; color: \"blue\" }", root, "dynamicItem"); } Component.onCompleted: root.createItem(); } 《QmlBook》In Chinese 369从⽂本中动态实例化项(Dynamically Instantiating Items from Text) 在QML场景下,动态创建的对象可以像其它的对象⼀样处理。然⽽,也有⼀ 些缺陷需要处理。最重要的是创建环境的概念。 ⼀个动态创建对象的创建环境是它被创建时的环境。这与它的⽗对象所在的 环境不⼀定相同。当创建环境被销毁,会影响涉及绑定属性的对象。这意味 着在对象的整个⽣命周期,在代码的⼀个地⽅实现动态对象创建是⾮常重要 的。 动态创建的对象也可以动态销毁。当这样做时,有⼀个法则:永远不要尝试 销毁⼀个你没有创建的对象。这也包括你已经创建的元素,但不要使⽤动态 机制⽐如Component.createObject或者createQmlObject。 对象的销毁依赖于它的析构函数被调⽤。这个函数接收⼀个可选参数⽤于指 定这个对象还可以存在多少毫秒后被销毁。这是⾮常有⽤的,例如让对象完 成⼀个完整的过渡。 item = Qt.createQmlObject(...); ... item.destroy(); 注意 可以从⼀个对象内部实现销毁,例如创建⼀个可以⾃销毁的弹出窗⼝。 管理动态创建的元素(Managing Dynamically Created Elements) 《QmlBook》In Chinese 370管理动态创建的元素(Managing Dynamically Created Elements) 处理动态对象时,通常需要跟踪已创建的对象。另⼀个常⻅的功能是能够存 储和恢复动态对象的状态。在我们动态填充时,使⽤链表模型(ListModel) 可以⾮常⽅便的处理这些问题。 在下⾯的例⼦中包含了两种元素,⽕箭和⻜机,能够被⽤户创建和移动。为 了控制整个场景动态创建元素,我们使⽤⼀个模型来跟踪项。 待完成 插图 模型是⼀个链表模型(ListModel),⽤已创建的项进⾏填充。实例化时跟踪 对象引⽤的资源URL。后者不是需要严格跟踪的对象,但是以后会派上⽤ 场。 import QtQuick 2.0 import "create-object.js" as CreateObject Item { id: root ListModel { id: objectsModel } function addPlanet() { CreateObject.create("planet.qml", root, itemAdded); } function addRocket() { CreateObject.create("rocket.qml", root, itemAdded); } 跟踪动态对象(Tracking Dynamic Objects) 《QmlBook》In Chinese 371跟踪动态对象(Tracking Dynamic Objects) function itemAdded(obj, source) { objectsModel.append({"obj": obj, "source": source}) } 你可以从上⾯的例⼦中看到,create-object.js是⼀种使得JavaScript引进更加 简单的、普遍的⽅法。创建⽅法使⽤了三个参数:⼀个资源URL,⼀个根元 素和⼀个完成的回调函数。回调需要两个参数:⼀个新创建的对象引⽤和⼀ 个资源URL。 这意味着每⼀次调⽤addPlanet或者addRocket时,当新建对象被创建完成后 悔调⽤itemAdded函数。后者会将对象的引⽤和资源URL添加到 objectsModel模型中。 可以在很多⽅⾯使⽤objectsModel。在⽰例中,clearItems函数依赖它。这个 函数证明了两个事情。⾸先,如何遍历模型和执⾏⼀个任务,即调⽤析构函 数来移除每⼀个项。其次,它强调了模型不会更新已经销毁的对象。此外移 除模型项已连接的对象问题,模型项的对象属性设置为null,为了补救这个问 题,代码显式的清除了已移除对象的模型项。 function clearItems() { while(objectsModel.count > 0) { objectsModel.get(0).obj.destroy(); objectsModel.remove(0); } } 通过设置XmlListModel模型的xml属性可以处理XML⽂档字符串。代码如下, 模型展⽰了反序列化函数。反序列化函数通过设置dsIndex引⽤模型的第⼀个 项来启动反序列化,然后调⽤项的创建。然后回调dsItemAdded设置新创建 对象的x,y属性,然后更新索引创建下⼀个对象。 XmlListModel { id: xmlModel query: "/scene/item" 《QmlBook》In Chinese 372跟踪动态对象(Tracking Dynamic Objects) XmlRole { name: "source"; query: "source/string()" } XmlRole { name: "x"; query: "x/string()" } XmlRole { name: "y"; query: "y/string()" } } function deserialize() { dsIndex = 0; CreateObject.create(xmlModel.get(dsIndex).source, root, dsItemAdded); } function dsItemAdded(obj, source) { itemAdded(obj, source); obj.x = xmlModel.get(dsIndex).x; obj.y = xmlModel.get(dsIndex).y; dsIndex ++; if (dsIndex < xmlModel.count) CreateObject.create(xmlModel.get(dsIndex).source, root, dsItemAdded); } property int dsIndex 这个例⼦演⽰了如何使⽤模型跟踪已创建的模型项,和基于信息对模型项序 列化和反序列化。这可以⽤于存储⼀个动态填充场景,例如窗⼝部件。在这 个例⼦中,模型被⽤于跟踪每⼀个模型项。 另⼀种解决⽅案是⽤于⼀个场景根项下的⼦项属性来跟踪⼦项。然后,这要 求项⾃⼰知道资源URL⽤于创建它们⾃⼰。这也要求场景只能动态创建⼦ 项,以避免序列化或者反序列化静态分配的对象。 《QmlBook》In Chinese 373跟踪动态对象(Tracking Dynamic Objects) 在这⼀章中,我们主要讨论了动态创建QML元素。折让我们可以⾃由的创建 QML场景,了解了⽤户可配置与插件结构。 动态加载⼀个QML元素最简单的⽅法是使⽤加载元素(Loader element)。 它可以作为⼀个占位符内容被加载。 使⽤⼀种更加动态的⽅法,Qt.createQmlObject⽅法可以⽤于实例化QML字 符串。然后这种⽅法有局限性。最全⾯的解决⽅案是动态创建使⽤ Qt.createComponent函数创建组件。然后通过调⽤组件的createObject函数 来创建对象。 由于绑定与信号连接依赖于对象id,或者访问实例化对象。对于动态创建的 对象需要另外⼀种⽅法,为了创建绑定,需要使⽤绑定元素(Binding element),连接元素(Connections element)使得与动态创建对象连接信 号成为可能。 对于动态创建项,最⼤的挑战是跟踪它们。可以使⽤链表模型(ListModel) 来完成这件事。有了⼀个模型⽤来跟踪动态创建项,可以实现序列化和反序 列化函数,可以存储和恢复动态创建场景。 总结(Summary) 《QmlBook》In Chinese 374总结(Summary) JavaScript是web客户端开发的通⽤语⾔。基于node js它也开始引导web服务 器的开发。因此它使⾮常适合在声明式QML语⾔上添加的命令性语⾔。QML 本⾝作为⼀个申明式语⾔⽤于表达⽤户界⾯层次,但是这仅限于表达操作。 有时你需要⼀个⽅法表达业务,使⽤JavaScript来完成。 注意 在Qt社区有⼀个开放性的问题是在⺫前Qt程序中关于混合使⽤ QML/JS/QtC++的正确性。通常建议的混合⽅式是在你的应⽤程序中将JS部 分限制在最⼩,将你的业务逻辑部分放在QtC++中,UI逻辑放在QML/JS 中。 这本书趋向这种边界的划分,通常对于⼀个产品的开发这不⼀定是正确的混 合⽅式,不是对于所有⼈都适⽤。最重要的是根据你的团队技能和个⼈品味 ⽽定。在接受推荐的时候保持你的怀疑。 下⾯有⼀个简短的例⼦是关于如何在QML中混合适⽤JS: Button { width: 200 height: 300 property bool toggle: false text: "Click twice to exit" // JS function function doToggle() { toggle = !toggle } onTriggered: { // this is JavaScript doToggle(); if(toggle) { JavaScript 《QmlBook》In Chinese 375JavaScript Qt.quit() } } } 因此在QML中JavaScript作为⼀个单独的JS函数,作为⼀个JS模块可以在很 多地⽅使⽤,它可以与每⼀个右边的属性绑定。 import "util.js" as Util // import a pure JS module Button { width: 200 height: width*2 // JS on the right side of property binding // standalone function (not really useful) function log(msg) { console.log("Button> " + msg); } onTriggered: { // this is JavaScript log(); Qt.quit(); } } 在使⽤QML定义⽤户界⾯时,使⽤JavaScript完成功能。那么你需要写多少 的JavaScript呢?这取决于你的⻛格和你对JS开发的熟悉程度。JS是⼀种松 散型语⾔,这使得你很难发现类型缺陷。函数参数接受不同类型的变量值, 会导致⾮常难发现严重的Bug。发现缺陷的⽅法是严格的单元测试或者验收 测试。因此如果你在JS中开发真正的逻辑(不是粘贴代码)你应该使⽤测试 优先的⽅法。通常使⽤这种混合开发⾮常成功的团队(Qt/C++与 QML/JS),他们都会最⼩化前段逻辑中使⽤的JS,在后端Qt C++中完成更 加复杂的⼯作。后端遵循严格的单元测试,这样前段的开发者可以信任这些 代码并且专注于⽤户界⾯的需求。 《QmlBook》In Chinese 376JavaScript 注意 通常:后端开发者由功能驱动,前段开发者由⽤户场景驱动。 《QmlBook》In Chinese 377JavaScript 浏览器在运⾏时渲染HTML,执⾏HTML中相关的JavaScript。现今的web应 ⽤中相对于HTML包含了更多的JavaScript。浏览器中JavaScript运⾏在⼀些 浏览器附加的标准ECMAScript环境。⼀个典型的浏览器中的JS环境知道访问 浏览器窗⼝的窗⼝对象。也简单的基于JQuery的DOM选择器来提供CSS选择 器。额外使⽤setTimeout函数在超时时调⽤函数。除了这些,JS存在于⼀个 标准的JavaScript环境,类似于QML/JS。 不同的是JS出现在HTML与QML中的⽅式。在HTML中,你只能在事件操作 (event handlers),例如⻚⾯加载(page loaded),⿏标点击(mouse pressed)中添加JS。例如通常在⻚⾯加载中初始化你的JS,这在QML中与 组件加载完成(Component.onCompleted)类似。例如你不能使⽤JS来绑定 属性(⾄少不是直接绑定,AngularJS增强了DOM树允许这种操作,但这和 典型HTML相去甚远)。 所以在QML中JS是⼀种更加优秀的语⾔,并且与QML的渲染树⾼度集成。使 得语⾔更具有可读性。除了这些,开发过HTML/JS应⽤程序的⼈会觉得在 QML/JS中开发⾮常容易上⼿。 浏览器/HTML与QtQuick/QML对⽐ (Browser/HTML vs QtQuick/QML) 《QmlBook》In Chinese 378浏览器/HTML与QtQuick/QML对⽐(Browser/HTML vs QtQuick/QML) 这章不会对JavaScript作详细的介绍。有其它的书可以参考,请访问Mozilla Developer Network JavaScript表⾯上是⼀种⾮常通⽤的语⾔,与许多其它语⾔没有什么不同: function countDown() { for(var i=0; i<10; i++) { console.log('index: ' + i) } } function countDown2() { var i=10; while( i>0 ) { i--; } } 但是注意JS有函数作⽤域,没有C++中的块作⽤域(查看Functions and function scope)。 语句if ... else,break,continue也可以使⽤。switch相对于C++中只可以切 换整数值,JavaScript可以切换其它类型的值: function getAge(name) { // switch over a string switch(name) { case "father": return 58; case "mother": return 56; } return unknown; JavaScript语法(The Language) 《QmlBook》In Chinese 379JavaScript语法(The Language) } JS可以将⼏种值认为是false,如false,0,“”,undefined,null。例如⼀个函 数范围默认值undefined。使⽤‘===’操作符验证是否为false。‘==’等于操作将 会对类型转换做验证。如果条件允许,直接使⽤等于操作符‘===’可以更快更 好的验证⼀致性(查看Comparison operators)。 在JavaScript底层有它⾃⼰的实现⽅式,例如数组: function doIt() { var a = [] // empty arrays a.push(10) // addend number on arrays a.push("Monkey") // append string on arrays console.log(a.length) // prints 2 a[0] // returns 10 a[1] // returns Monkey a[2] // returns undefined a[99] = "String" // a valid assignment console.log(a.length) // prints 100 a[98] // contains the value undefined } 当然对⽐C++或者Java这种OO语⾔,JS的⼯作⽅式是不同的。JS不是⼀种 纯粹的OO语⾔,它也可以称作基于原型语⾔。每个对象都有⼀个原型对象。 对象是基于这个原型对象创建的。请阅读这本关于JavaScript的书了解更 多Javascript the Good Parts by Douglas Crockford 。 可以使⽤在线JS控制台或者⼩⽚断的QML代码来测试⼩的JS⽚段代码: import QtQuick 2.0 Item { function runJS() { console.log("Your JS code goes here"); } Component.onCompleted: { 《QmlBook》In Chinese 380JavaScript语法(The Language) runJS(); } } 《QmlBook》In Chinese 381JavaScript语法(The Language) 在使⽤JS⼯作时,有⼀些对象和⽅法会被频繁的使⽤。下⾯是它们的⼀个⼩ 的集合。 Math.floor(v),Math.ceil(v),Math.round(v) - 从浮点数获取最⼤,最⼩和随 机整数 Math.random() - 创建⼀个在0到1之间的随机数 Object.keys(o) - 获取对象的索引值(包括QObject) JSON.parse(s), JSON.stringify(o) - 转换在JS对象和JSON字符串 Number.toFixed(p) - 修正浮点数精度 Date - ⽇期时间操作 你可以可以在这⾥找到它们的使⽤⽅法:JavaScript reference You can find them also at: JavaScript reference 下⾯有⼀些简单的例⼦演⽰了如何在QML中使⽤JS。会给你⼀些如何在QML 中使⽤JS⼀些启发。 打印所有项的键(Print all keys from QML Item) Item { id: root Component.onCompleted: { var keys = Object.keys(root); for(var i=0; i 'value' } } 当前时间(Current Date) Item { Timer { id: timeUpdater interval: 100 running: true repeat: true onTriggered: { var d = new Date(); console.log(d.getSeconds()); } } } 使⽤名称调⽤函数(Call a function by name) Item { id: root function doIt() { 《QmlBook》In Chinese 383JS对象(JS Objects) console.log("doIt()") } Component.onCompleted: { // Call using function execution root["doIt"](); var fn = root["doIt"]; // Call using JS call method (could pass in a custom this object and arguments) fn.call() } } 《QmlBook》In Chinese 384JS对象(JS Objects) 下⾯这个⼩的例⼦我们将创建⼀个JS控制台。我们需要⼀个输⼊区域允许⽤ 户输⼊表达式,和⼀个结果输出列表。由于这更像⼀个桌⾯应⽤程序,我们 使⽤QtQuick控制模块。 注意 在你下⼀个项⺫中包含⼀个JS控制台可以更好的⽤来测试。增加Quake- Terminal效果也有助于对你的客户留下更好的印象。为了更好的使⽤它,你 需要评估JS控制台的控制范围,例如当前可⻅屏幕,核⼼数据模型,⼀个单 例对象或者其它的东⻄。 我们在Qt Creator中使⽤QtQuick controls创建⼀个Qt Quick UI项⺫。把这个 创建JS控制台(Creating a JS Console) 《QmlBook》In Chinese 385创建JS控制台(Creating a JS Console) 项⺫取名为JSConsole。完成引导后,我们已经有了⼀个基础的应⽤程序框 架,这个框架包含⼀个应⽤程序窗⼝和⼀个菜单。 我们使⽤⼀个TextFiled来输⼊⽂本,使⽤⼀个Button来对输⼊求值。表达式 求值结果使⽤⼀个机遇链表模型(ListModel)的链表视图(ListView)显 ⽰,每⼀个链表项使⽤两个标签显⽰表达式和求值结果。 // part of JSConsole.qml ApplicationWindow { id: root ... ColumnLayout { anchors.fill: parent anchors.margins: 9 RowLayout { Layout.fillWidth: true TextField { id: input Layout.fillWidth: true focus: true onAccepted: { // call our evaluation function on root root.jsCall(input.text) } } Button { text: qsTr("Send") onClicked: { // call our evaluation function on root root.jsCall(input.text) } } } Item { Layout.fillWidth: true Layout.fillHeight: true Rectangle { anchors.fill: parent 《QmlBook》In Chinese 386创建JS控制台(Creating a JS Console) color: '#333' border.color: Qt.darker(color) opacity: 0.2 radius: 2 } ScrollView { id: scrollView anchors.fill: parent anchors.margins: 9 ListView { id: resultView model: ListModel { id: outputModel } delegate: ColumnLayout { width: ListView.view.width Label { Layout.fillWidth: true color: 'green' text: "> " + model.expression } Label { Layout.fillWidth: true color: 'blue' text: "" + model.result } Rectangle { height: 1 Layout.fillWidth: true color: '#333' opacity: 0.2 } } } } } } } 求值函数jsCall不会做求值操作,⽽是将它的内容移动到JS模块 《QmlBook》In Chinese 387创建JS控制台(Creating a JS Console) (jsconsole.js)完成清晰的分离。 // part of JSConsole.qml import "jsconsole.js" as Util ... ApplicationWindow { id: root ... function jsCall(exp) { var data = Util.call(exp); // insert the result at the beginning of the list outputModel.insert(0, data) } } 为了安全我们不在JS中调⽤eval函数,这可能导致⽤户修改局部作⽤域。我 们使⽤constructor函数在运⾏时创建JS函数,并在我们的作⽤域中传⼊变量 值。由于函数可能随时都在创建,它不能扮演关闭和存储它私有作⽤域的⾓ ⾊,我们需要使⽤ this.a = 10 来存储值10在这个函数的作⽤域。这个作⽤域 由脚本设置到变量作⽤域。 // jsconsole.js .pragma library var scope = { // our custom scope injected into our function evaluation } function call(msg) { var exp = msg.toString(); console.log(exp) var data = { expression : msg 《QmlBook》In Chinese 388创建JS控制台(Creating a JS Console) } try { var fun = new Function('return (' + exp + ');'); data.result = JSON.stringify(fun.call(scope), null, 2) console.log('scope: ' + JSON.stringify(scope, null, 2) + 'result: ' + result) } catch(e) { console.log(e.toString()) data.error = e.toString(); } return data; } 调⽤函数返回的数据是⼀个JS对象,包含了⼀个结果,表达式和错误属性: data: { expression: {}, result: {}, error: {} }。我们可以直接在链表模型 (ListModel)中使⽤这个JS对象,并且可以通过代理(delegate)访问它, 例如model.expression会得到输⼊的表达式。为了让例⼦更加简单,我们忽 略了错误的结果。 《QmlBook》In Chinese 389创建JS控制台(Creating a JS Console) Qt是QML与JavaScript的C++扩展⼯具包。有许多语⾔与Qt绑定,但是由于 Qt是由C++开发的,C++的精神贯穿了整个Qt。在这⼀节中,我们着重从 C++的⾓度介绍Qt,使⽤C++开发的本地插件来了解如何更好的扩展QML。 通过C++,可以扩展和控制提供给QML的执⾏环境。 这章将讲解Qt,正如Qt所要求的⼀样,需要读者有⼀定的C++基础知识。Qt 不依赖于先进的C++特性,我认为Qt⻛格的C++代码可读性⾮常⾼,所以不 要担⼼你的C++⽅⾯⽐较差。 从C++的⾓度分析Qt,你会发现Qt通过内省数据的机制实现了许多现代语⾔ 的特性。这是通过使⽤基础类QObject实现的。内省数据,源数据,类运⾏ 时的数据维护。原⽣的C++是不会完成这些事情的。这使得动态查询对象信 息,例如它们的属性成为可能。 Qt使⽤源对象信息实现了信号与槽的回调绑定。每个信号能够连接任意数量 的槽函数或者其它的信号。当⼀个信号从⼀个对象实例从发送后,会调⽤连 接信号的槽函数。发送信号的对象不需要知道接收槽对象的任何信息,反之 亦然。这⼀机制可以创建复⽤性⾮常⾼的组件,并减少组件之间的依赖。 内省特性也⽤于创建动态语⾔的绑定,使得QML可以调⽤暴露的C++对象实 例,并且可以从JavaScript中调⽤C++函数。除了绑定Qt C++, 绑定标准的 JavaScript也是⼀种⾮常流⾏的⽅式,还有Python的绑定,叫做PyQt。 除了这些核⼼概念,Qt可以使⽤C++开发跨平台应⽤程序。Qt C++在不同的 操作系统上提供了⼀套平台抽象,允许开发者专注于⼿上的任务,不需要你 去了解如何在不同的操作系统上打开⼀个⽂件。这意味着你可以在 Windows,OS X和Linux重复编译相同的代码,由Qt去解决在不同平台上的 适配问题。最终保持本地构建的应⽤程序与⺫标平台的窗⼝⻛格上看起来⼀ 致。随着移动平台的桌⾯更新,Qt也提供相同的代码在不同的移动平台上编 译,例如IOS,Android,Jolla,BlackBerry,Ubuntu Phone,Tizen。 Qt and C++ 《QmlBook》In Chinese 390Qt and C++ 这样不仅仅是代码可以重⽤,开发者的技能也可以重⽤。了解Qt的团队⽐只 专注于单平台特定技能的团队可以接触更多的平台,由于Qt的灵活性,团队 可以使⽤相同的技术创建不同平台的组件。 对于所有平台,Qt提供了⼀套基本类,例如⽀持完整unicode编码的字符串, 链表容器,向量容器,缓冲容器。它也提供了⺫标平台的通⽤主循环抽象和 跨平台的线程⽀持,⺴络⽀持。Qt的主旨是为Qt的开发者提供所有必须的功 能。对于特定领域的任务,例如本地库接⼝,Qt也提供了⼀些帮助类来使得 这些操作更加简单。 《QmlBook》In Chinese 391Qt and C++ 理解Qt最好的⽅法是从⼀个⼩的应⽤程序开始。这个简单的例⼦叫做“Hello World!”,使⽤unicode编码将字符串写⼊到⼀个⽂件中。 #include #include #include #include #include #include int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // prepare the message QString message("Hello World!"); // prepare a file in the users home directory named out.txt QFile file(QDir::home().absoluteFilePath("out.txt")); // try to open the file in write mode if(!file.open(QIODevice::WriteOnly)) { qWarning() << "Can not open file with write access"; return -1; } // as we handle text we need to use proper text codecs QTextStream stream(&file); // write message to file via the text stream stream << message; // do not start the eventloop as this would wait for external IO // app.exec(); // no need to close file, closes automatically when scope ends return 0; } 演⽰程序(A Boilerplate Application) 《QmlBook》In Chinese 392演⽰程序(A Boilerplate Application) 这个简单的例⼦演⽰了⽂件访问的使⽤和通过⽂本流使⽤⽂本编码将⽂本正 确的写⼊到⽂件中。⼆进制数据的操作有⼀个跨平台的⼆进制流类叫做 QDataStream。我们使⽤的不同类需要使⽤它们的类名包含。另⼀种是使⽤ 模块名和类名 例如 #include 。对于⽐较懒的⼈,有⼀个 更加简单的⽅法是包含整个模块,使⽤ #include 。例如在QtCore 中你可以在应⽤程序中使⽤很多通⽤的类,这没有UI依赖。查看QtCore class list或者QtCore overview获取更多的信息。 使⽤qmake和make来构建程序。QMake读取项⺫⽂件(project file)并⽣成 ⼀个Makefile供make使⽤。项⺫⽂件(project file)独⽴于平台,qmake会 根据特定平台的设置应⽤⼀些规则来⽣成Makefile。在有特殊需求的项⺫ 中,项⺫⽂件也可以包含特定平台规则的平台作⽤域。下⾯是⼀个简单的项 ⺫⽂件(project file)例⼦。 # build an application TEMPLATE = app # use the core module and do not use the gui module QT += core QT -= gui # name of the executable TARGET = CoreApp # allow console output CONFIG += console # for mac remove the application bundling macx { CONFIG -= app_bundle } # sources to be build SOURCES += main.cpp 《QmlBook》In Chinese 393演⽰程序(A Boilerplate Application) 我们不会再继续深⼊这个话题,只需要记住Qt项⺫会使⽤特定的项⺫⽂件 (project file),qmake会根据这些项⺫⽂件和指定平台⽣成Makefile。 上⾯简单的例⼦只是在应⽤程序中写⼊⽂本。⼀个命令⾏⼯具这是不够的。 对于⼀个⽤户界⾯我们需要⼀个事件循环来等待⽤户的输⼊并安排刷新绘制 操作。下⾯这个相同的例⼦使⽤⼀个桌⾯按钮来触发写⼊。 令⼈惊奇的是我们的main.cpp依然很⼩。我们将代码移⼊到我们的类中,并 使⽤信号槽(signal/slots)来连接⽤⽤户的输⼊,例如按钮点击。信号槽 (signal/slot)机制通常需要⼀个对象,你很快就会看到。 #include #include #include #include "mainwindow.h" int main(int argc, char** argv) { QApplication app(argc, argv); MainWindow win; win.resize(320, 240); win.setVisible(true); return app.exec(); } 在main函数中我们简单的创建了⼀个应⽤程序对象,并使⽤exec()开始事件 循环。现在应⽤程序放在了事件循环中,并等待⽤户输⼊。 int main(int argc, char** argv) { QApplication app(argc, argv); // init application // create the ui 《QmlBook》In Chinese 394演⽰程序(A Boilerplate Application) return app.exec(); // execute event loop } Qt提供了⼏种UI技术。这个例⼦中我们使⽤纯Qt C++的桌⾯窗⼝⽤户界⾯ 库。我们需要创建⼀个主窗⼝来放置⼀个触发功能的按钮,同事由主窗⼝来 实现我们的核⼼功能,正如我们在上⾯例⼦上看到的。 主窗⼝本⾝也是⼀个窗⼝,它是⼀个没有⽗对象的窗⼝。这与Qt如何定义⽤ 户界⾯为⼀个界⾯元素树类似。在这个例⼦中,主窗⼝是我们的根元素,按 钮是主窗⼝上的⼀个⼦元素。 #ifndef MAINWINDOW_H #define MAINWINDOW_H #include class MainWindow : public QMainWindow { public: MainWindow(QWidget* parent=0); ~MainWindow(); public slots: 《QmlBook》In Chinese 395演⽰程序(A Boilerplate Application) void storeContent(); private: QPushButton *m_button; }; #endif // MAINWINDOW_H 此外我们定义了⼀个公有槽函数 storeContent() ,当点击按钮时会调⽤这 个函数。槽函数是⼀个C++⽅法,这个⽅法被注册到Qt的源对象系统中,可 以被动态调⽤。 #include "mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_button = new QPushButton("Store Content", this); setCentralWidget(m_button); connect(m_button, &QPushButton::clicked, this, &MainWindow::storeContent); } MainWindow::~MainWindow() { } void MainWindow::storeContent() { qDebug() << "... store content"; QString message("Hello World!"); QFile file(QDir::home().absoluteFilePath("out.txt")); if(!file.open(QIODevice::WriteOnly)) { qWarning() << "Can not open file with write access"; return; } QTextStream stream(&file); stream << message; } 《QmlBook》In Chinese 396演⽰程序(A Boilerplate Application) 在主窗⼝中我们⾸先创建了⼀个按钮,并将 clicked() 信号 与 storeContent() 槽连接起来。每点击信号发送时都会触发调⽤槽函 数 storeContent() 。就是这么简单,通过信号与槽的机制实现了松耦合的 对象通信。 《QmlBook》In Chinese 397演⽰程序(A Boilerplate Application) 正如介绍中描述的, QObject 是Qt的内省机制。在Qt中它⼏乎是所有类的基 类。值类型除外,例如 QColor , QString 和 QList 。 Qt对象是⼀个标准的C++对象,但是它具有更多的功能。可以从两个⽅向来 深⼊探讨:内省和内存管理。内省意味着Qt对象知道它的类名,它与其它类 的关系,以及它的⽅法和属性。内存管理意味着每个Qt对象都可以成为是其 它⼦对象的⽗对象。⽗对象拥有⼦对象,当⽗对象销毁时,它也会负责销毁 它的⼦对象。 理解 QObject 的能⼒如何影响⼀个类最好的⽅法是使⽤Qt的类来替换⼀个典 型的C++类。如下所⽰的代表⼀个普通的类。 类 Person 是⼀个数据类,包含了⼀个名字和性别属性。 Person 使⽤Qt的对 象系统来添加⼀个元信息到c++类中。它允许使⽤ Person 对象的⽤户连接槽 函数并且当属性变化时获得通知。 class Person : public QObject { Q_OBJECT // enabled meta object abilities // property declarations required for QML Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(Gender gender READ gender WRITE setGender NOTIFY genderChanged) // enables enum introspections Q_ENUMS(Gender) public: // standard Qt constructor with parent for memory management Person(QObject *parent = 0); enum Gender { Unknown, Male, Female, Other }; QString name() const; QObject对象(The QObject) 《QmlBook》In Chinese 398Qt对象(The QObject) Gender gender() const; public slots: // slots can be connected to signals void setName(const QString &); void setGender(Gender); signals: // signals can be emitted void nameChanged(const QString &name); void genderChanged(Gender gender); private: // data members QString m_name; Gender m_gender; }; 构造函数传⼊⽗对象到超类中并且初始化成员变量。Qt的值类型类会⾃动初 始化。在这个例⼦中 QString 将会初始化为⼀个空字符串 ( QString::isNull() )并且性别成员变量会明确的初始化为未知性别。 Person::Person(QObject *parent) : QObject(parent) , m_gender(Person::Unknown) { } 获取函数在命名在属性后并且是⼀个简单的 const 函数。使⽤设置属性函数 当属性被改变时会发送改变信号。为此我们插⼊⼀个保护⽤来⽐较当前值与 新值。只有在值不同时我们指定给成员变量的值才会⽣效,并且发送改变信 号。 QString Person::name() const { return m_name; } 《QmlBook》In Chinese 399Qt对象(The QObject) void Person::setName(const QString &name) { if (m_name != name) // guard { m_name = name; emit nameChanged(m_name); } } 类通过继承 QObject ,我们获得了元对象能⼒,我们可以尝试使 ⽤ metaObject() 的⽅法。例如从对象中检索类名。 Person* person = new Person(); person->metaObject()->className(); // "Person" Person::staticMetaObject.className(); // "Person" QObject基类和元对象还有其它很多功能。详情请查看 QMetaObject ⽂档获 取更多信息。 《QmlBook》In Chinese 400Qt对象(The QObject) 在不同的平台上稳定的编译软件是⼀个复杂的任务。你将会遇到不同环境下 的不同编译器,路径和库变量的问题。Qt的⺫的是防⽌应⽤开发者遭遇这些 跨平台问题。为了完成这个任务,Qt引进了 qmake 编译⽂件⽣成 器。 qmake 操作以 .pro 结尾的项⺫⽂件。这个项⺫⽂件包含了关于应⽤程 序的说明和需要读取的资源⽂件。⽤qmake执⾏这个项⺫⽂件会为你⽣成⼀ 个在unix和mac的 Makefile ,如果在windows下使⽤mingw编译⼯具链也会 ⽣成。否则可能会创建⼀个visual studio项⺫或者⼀个xcode项⺫。 在unix下使⽤Qt编译如下: $ edit myproject.pro $ qmake // generates Makefile $ make Qt也允许你使⽤影⼦编译。影⼦编译会在你的源码位置外的路径进⾏编译。 假设我们有⼀个myproject⽂件夹,⾥⾯有⼀个myproject.pro⽂件。如下输⼊ 命令: $ mkdir build $ cd build $ qmake ../myproject/myproject.pro 我们创建⼀个编译⽂件夹并且在这个编译⽂件中使⽤qmake指向我们项⺫⽂ 件夹中的项⺫⽂件。这将配置makefile使⽤编译⽂件夹替代我们的源代码⽂ 件夹来存放所有的编译中间件和结果。这允许我们同时为不同的qt版本和编 译配置创建不同的编译⽂件夹并且不会弄乱我们的源代码⽂件夹。 当你使⽤Qt Creator时,它会在后代为你做这些事情,通常你不在需要担⼼ 这些步骤。对于⽐较⼤的项⺫,建议使⽤命令⾏⽅式来编译你的Qt项⺫可以 编译系统(Build Systems) 《QmlBook》In Chinese 401编译系统(Build Systems) 更加深⼊的了解编译流。 《QmlBook》In Chinese 402编译系统(Build Systems) QMake是⽤来读取项⺫⽂件并⽣成编译⽂件的⼯具。项⺫⽂件记录了你的项 ⺫配置,扩展依赖库和源代码⽂件。最简单包含⼀个源代码⽂件的项⺫可能 像这样: // myproject.pro SOURCES += main.cpp 我们编译了⼀个基于项⺫⽂件名称 myproject 的可执⾏程序。这个编译将只 包含 main.cpp 源⽂件。默认情况下我们会为项⺫添加QtCore和QtGui模块。 如果我们的项⺫是⼀个QML应⽤程序,我们需要添加QtQuick和QtQml到这 个链表中: // myproject.pro QT += qml quick SOURCES += main.cpp 现在编译⽂件知道与Qt的QtQml和QtQuick模块链接。QMake使⽤ = , +=` and ``-= 来指定,添加和移除选项链表中的元素。例如⼀个只有控制台的编 译将不会依赖UI,你需要移除QtGui模块: // myproject.pro QT -= gui SOURCES += main.cpp QMake 《QmlBook》In Chinese 403QMake 当你期望编译⼀个库来替代⼀个应⽤程序时,你需要改变编译模板: // myproject.pro TEMPLATE = lib QT -= gui HEADERS += utils.h SOURCES += utils.cpp 现在项⺫将使⽤ utils.h 头⽂件和 utils.cpp ⽂件编译为⼀个没有UI依赖的 库。库的格式依赖于你当前编译项⺫所⽤的操作系统。 通常将会有更加复杂的配置并且需要编译个项⺫配置。qmake提供 了 subdirs 模板。假设我们有⼀个mylib和⼀个myapp项⺫。我们的配置可能 如下: my.pro mylib/mylib.pro mylib/utils.h mylib/utils.cpp myapp/myapp.pro myapp/main.cpp 我们已经知道了如何使⽤Mylib.pro和myapp.pro。my.pro作为⼀个包含项⺫ ⽂件配置如下: // my.pro TEMPLATE = subdirs subdirs = mylib \ myapp myapp.depends = mylib 《QmlBook》In Chinese 404QMake 在项⺫⽂件中声明包含两个⼦项⺫ mylib 和 myapp , myapp 依赖 于 mylib 。当你使⽤qmake为这个项⺫⽂件⽣成编译⽂件时,将会在每个项 ⺫⽂件对应的⽂件夹下⽣成⼀个编译⽂件。当你使⽤ my.pro ⽂件的makefile 编译时,所有的⼦项⺫也会编译。 有时你需要基于你的配置在不同的平台上做不同的事情。qmake推荐使⽤域 的概念来处理它。当⼀个配置选项设置为true时使⼀个域⽣效。 例如使⽤unix指定utils的实现可以像这样: unix { SOURCES += utils_unix.cpp } else { SOURCES += utils.cpp } 这表⽰如果CONFIG变量包含了unix配置将使⽤对应域下的⽂件路径,否则 使⽤其它的⽂件路径。⼀个典型的例⼦,移除mac下的应⽤绑定: macx { CONFIG -= app_bundle } 这将在mac下创建的应⽤程序不再是⼀个 .app ⽂件夹⽽是创建⼀个应⽤程序 替换它。 基于QMake的项⺫通常在你开始编写Qt应⽤程序最好的选择。但是也有⼀些 其它的选择。它们各有优势。我们将在下⼀⼩节中简短的讨论其它的选择。 引⽤ QMake Manual - qmake⼿册⺫录。 QMake Language - 赋值,域和相关语法 《QmlBook》In Chinese 405QMake QMake Variables - TEMPLATE,CONFIG,QT等变量解释 《QmlBook》In Chinese 406QMake CMake是由Kitware创造的⼯具。由于它们的3D可视化软件VTK使得Kitware 家喻户晓,当然这也有CMake这个跨平台makefile⽣成器的功劳。它使⽤⼀ 系列的 CMakeLists.txt ⽂件来⽣成平台指定的makefile。CMake被KDE项 ⺫所使⽤,它与Qt社区有⼀种特殊的关系。 CMakeLists.txt ⽂件存储了项⺫配置。⼀个简单的hello world使⽤QtCore 的项⺫如下: // ensure cmake version is at least 3.0 cmake_minimum_required(VERSION 3.0) // adds the source and build location to the include path set(CMAKE_INCLUDE_CURRENT_DIR ON) // Qt's MOC tool shall be automatically invoked set(CMAKE_AUTOMOC ON) // using the Qt5Core module find_package(Qt5Core) // create excutable helloworld using main.cpp add_executable(helloworld main.cpp) // helloworld links against Qt5Core target_link_libraries(helloworld Qt5::Core) 这将使⽤main.cpp编译⼀个可执⾏的helloworld应⽤程序,并与额外的 Qt5Core库链接。编译⽂件通常会被修改: // sets the PROJECT_NAME variable project(helloworld) cmake_minimum_required(VERSION 3.0) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) find_package(Qt5Core) // creates a SRC_LIST variable with main.cpp as single entry set(SRC_LIST main.cpp) CMake 《QmlBook》In Chinese 407CMake // add an executable based on the project name and source list add_executable(${PROJECT_NAME} ${SRC_LIST}) // links Qt5Core to the project executable target_link_libraries(${PROJECT_NAME} Qt5::Core) CMake⼗分强⼤。需要⼀些时间来适应语法。通常CMake更加适合⼤型和复 杂的项⺫。 引⽤ CMake Help - CMake在线帮助⽂档 Running CMake KDE CMake Tutorial CMake Book CMake and Qt 《QmlBook》In Chinese 408CMake 类 QObject 组成了Qt的基础,但是在这个框架⾥还有很多的类。在我们继续 探寻如何扩展QML之前,我们需要先了解⼀些有⽤的Qt基础类。 在这⼀节中的⽰例代码需要使⽤Qt Test库。它提供⼀种⾮常好的⽅法来测试 Qt的API并将其存储供以后参考使⽤。测试库提供 的 QVERIFY 与 QCOMPARE 函数断⾔⼀个正确条件。我们也将使⽤域来避免名 称校验冲突。所以不要对后⾯的代码有困惑。 Qt通⽤类(Common Qt Classes) 《QmlBook》In Chinese 409Qt通⽤类(Common Qt Classes) 通常在Qt中⽂本操作是基于unicode完成的。你需要使⽤ QString 类来完成 这个事情。它包含了很多好⽤的功能函数,这与其它流⾏的框架类似。对于8 位的数据你通常需要使⽤ QByteArray 类,对于ASCII校验最好使 ⽤ QLatin1String 来暂存。对于⼀个字符串链你可以使 ⽤ QList 或者 QStringList 类(派⽣⾃ QList )。 这⾥有⼀些例⼦介绍了如何使⽤ QString 类。QString可以在栈上创建,但 是它的数据存储在堆上。分配⼀个字符串数据到另⼀个上,不会产⽣拷⻉操 作,只是创建了数据的引⽤。这个操作⾮常廉价让开发者更专注于代码⽽不 是内存操作。 QString 使⽤引⽤计数的⽅式来确定何时可以安全的删除数 据。这个功能叫做隐式共享,在Qt的很多类中都⽤到了它。 QString data("A,B,C,D"); // create a simple string // split it into parts QStringList list = data.split(","); // create a new string out of the parts QString out = list.join(","); // verify both are the same QVERIFY(data == out); // change the first character to upper case QVERIFY(QString("A") == out[0].toUpper()); 这⾥我们将展⽰如何将⼀个字符串转换为数字,将⼀个数字转换为字符串。 也有⼀些⽅便的函数⽤于float或者double和其它类型的转换。只需要在Qt帮 助⽂档中就可以找到这些使⽤⽅法。 // create some variables int v = 10; int base = 10; // convert an int to a string QString a = QString::number(v, base); // and back using and sets ok to true on success QString 《QmlBook》In Chinese 410QString bool ok(false); int v2 = a.toInt(&ok, base); // verify our results QVERIFY(ok == true); QVERIFY(v = v2); 通常你需要参数化⽂本。例如使⽤ QString("Hello" + name) ,⼀个更加灵 活的⽅法是使⽤ arg 标记⺫标,这样即使在翻译时也可以保证⺫标的变化。 // create a name QString name("Joe"); // get the day of the week as string QString weekday = QDate::currentDate().toString("dddd"); // format a text using paramters (%1, %2) QString hello = QString("Hello %1. Today is %2.").arg(name).arg(weekday); // This worked on Monday. Promise! if(Qt::Monday == QDate::currentDate().dayOfWeek()) { QCOMPARE(QString("Hello Joe. Today is Monday."), hello); } else { QVERIFY(QString("Hello Joe. Today is Monday.") != hello); } 有时你需要在你的代码中直接使⽤unicode字符。你需要记住如何在使 ⽤ QChar 和 QString 类来标记它们。 // Create a unicode character using the unicode for smile :-) QChar smile(0x263A); // you should see a :-) on you console qDebug() << smile; // Use a unicode in a string QChar smile2 = QString("\u263A").at(0); QVERIFY(smile == smile2); // Create 12 smiles in a vector QVector smilies(12); smilies.fill(smile); // Can you see the smiles qDebug() << smilies; 《QmlBook》In Chinese 411QString 上⾯这些⽰例展⽰了在Qt中如何轻松的处理unicode⽂本。对于⾮unicode⽂ 本,QByteArray类同样有很多⽅便的函数可以使⽤。阅读Qt帮助⽂档中 QString部分,它有⼀些很好的⽰例。 《QmlBook》In Chinese 412QString 链表,队列,数组都是顺序容器。最常⽤的顺序容器是 QList 类。它是⼀个 模板类,需要⼀个类型才能被初始化。它也是隐式共享的,数据存放在堆 中。所有的容器类应该被创建在栈上。正常情况下你不需要使⽤ new QList() 这样的语句,千万不要使⽤new来初始化⼀个容器。 类 QList 与类 QString ⼀样强⼤,提供了⽅便的接⼝来查询数据。下⾯⼀个 简单的⽰例展⽰了如何使⽤和遍历链表,这⾥⾯也使⽤到了⼀些C++11的新 特性。 // Create a simple list of ints using the new C++11 initialization // for this you need to add "CONFIG += c++11" to your pro file. QList list{1,2}; // append another int list << 3; // We are using scopes to avoid variable name clashes { // iterate through list using Qt for each int sum(0); foreach (int v, list) { sum += v; } QVERIFY(sum == 6); } { // iterate through list using C++ 11 range based loop int sum = 0; for(int v : list) { sum+= v; } QVERIFY(sum == 6); } { // iterate through list using JAVA style iterators int sum = 0; 顺序容器(Sequential Containers) 《QmlBook》In Chinese 413顺序容器(Sequential Containers) QListIterator i(list); while (i.hasNext()) { sum += i.next(); } QVERIFY(sum == 6); } { // iterate through list using STL style iterator int sum = 0; QList::iterator i; for (i = list.begin(); i != list.end(); ++i) { sum += *i; } QVERIFY(sum == 6); } // using std::sort with mutable iterator using C++11 // list will be sorted in descending order std::sort(list.begin(), list.end(), [](int a, int b) { return a > b; }); QVERIFY(list == QList({3,2,1})); int value = 3; { // using std::find with const iterator QList::const_iterator result = std::find(list.constBegin(), list.constEnd(), value); QVERIFY(*result == value); } { // using std::find using C++ lambda and C++ 11 auto variable auto result = std::find_if(list.constBegin(), list.constBegin(), [value](int v) { return v == value; }); QVERIFY(*result == value); } 《QmlBook》In Chinese 414顺序容器(Sequential Containers) 映射,字典或者集合是组合容器的例⼦。它们使⽤⼀个键来保存⼀个值。它 们可以快速的查询它们的元素。我们将展⽰使⽤最多的组合容器 QHash ,同 时也会展⽰⼀些C++11新的特性。 QHash hash({{"b",2},{"c",3},{"a",1}}); qDebug() << hash.keys(); // a,b,c - unordered qDebug() << hash.values(); // 1,2,3 - unordered but same as order as keys QVERIFY(hash["a"] == 1); QVERIFY(hash.value("a") == 1); QVERIFY(hash.contains("c") == true); { // JAVA iterator int sum =0; QHashIterator i(hash); while (i.hasNext()) { i.next(); sum+= i.value(); qDebug() << i.key() << " = " << i.value(); } QVERIFY(sum == 6); } { // STL iterator int sum = 0; QHash::const_iterator i = hash.constBegin(); while (i != hash.constEnd()) { sum += i.value(); qDebug() << i.key() << " = " << i.value(); i++; } QVERIFY(sum == 6); } hash.insert("d", 4); QVERIFY(hash.contains("d") == true); 组合容器(Associative Containers) 《QmlBook》In Chinese 415组合容器(Associative Containers) hash.remove("d"); QVERIFY(hash.contains("d") == false); { // hash find not successfull QHash::const_iterator i = hash.find("e"); QVERIFY(i == hash.end()); } { // hash find successfull QHash::const_iterator i = hash.find("c"); while (i != hash.end()) { qDebug() << i.value() << " = " << i.key(); i++; } } // QMap QMap map({{"b",2},{"c",2},{"a",1}}); qDebug() << map.keys(); // a,b,c - ordered ascending QVERIFY(map["a"] == 1); QVERIFY(map.value("a") == 1); QVERIFY(map.contains("c") == true); // JAVA and STL iterator work same as QHash 《QmlBook》In Chinese 416组合容器(Associative Containers) 通常我们都需要从读写⽂件。 QFile 是⼀个 QObject 对象,但是⼤多数情况 下它被创建在栈上。 QFile 包含了通知⽤户数据可读取信号。它可以异步读 取⼤段的数据,直到整个⽂件读取完成。为了⽅便它允许使⽤阻塞的⽅式读 取数据。这种⽅法通常⽤于读取⼩段数据或者⼩型⽂件。幸运的是我们在这 些例⼦中都只使⽤了⼩型数据。 除了读取⽂件内容到内存中可以使⽤ QByteArray ,你也可以根据读取数据 类型使⽤ QDataStream 或者使⽤ QTextStream 读取unicode字符串。我们现 在来看看如何使⽤。 QStringList data({"a", "b", "c"}); { // write binary files QFile file("out.bin"); if(file.open(QIODevice::WriteOnly)) { QDataStream stream(&file); stream << data; } } { // read binary file QFile file("out.bin"); if(file.open(QIODevice::ReadOnly)) { QDataStream stream(&file); QStringList data2; stream >> data2; QCOMPARE(data, data2); } } { // write text file QFile file("out.txt"); if(file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); QString sdata = data.join(","); stream << sdata; } } ⽂件IO(File IO) 《QmlBook》In Chinese 417⽂件IO(File IO) { // read text file QFile file("out.txt"); if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); QStringList data2; QString sdata; stream >> sdata; data2 = sdata.split(","); QCOMPARE(data, data2); } } 《QmlBook》In Chinese 418⽂件IO(File IO) 在QML中的数据模型为链表视图,路径视图和其它需要为模型中的每个⼦项 创建⼀个代理引⽤的视图提供数据。视图只创建可是区域内或者缓冲范围内 的引⽤。这使得即使包含成千上万的⼦项模型仍然可以保持流畅的⽤户界 ⾯。代理扮演了⽤来渲染模型⼦项数据的模板。总之:视图使⽤代理作为模 板来渲染模型中的⼦项。模型为视图提供数据。 当你不想使⽤C++时,你可以在QML环境中定义模型,你有多重⽅法为⼀个 视图提供模型。使⽤C++操作数据或者使⽤包含了⼤型数据的C++模型⽐在 QML环境中达到相同⺫的更加稳定可靠。但是当你值需要少量数据时,QML 模型时⾮常适合的。 ListView { // using a integer as model model: 5 delegate: Text { text: 'index: ' + index } } ListView { // using a JS array as model model: ['A', 'B', 'C', 'D', 'E'] delegate: Text { 'Char['+ index +']: ' + modelData } } ListView { // using a dynamic QML ListModel as model model: ListModel { ListElement { char: 'A' } ListElement { char: 'B' } ListElement { char: 'C' } ListElement { char: 'D' } ListElement { char: 'E' } } delegate: Text { 'Char['+ index +']: ' + model.char } } C++数据模型(Models in C++) 《QmlBook》In Chinese 419C++数据模型(Models in C++) QML视图知道如何操作不同的模型。对于来⾃C++的模型需要遵循⼀个特定 的协议。这个协议与动态⾏为⼀起被定义在⼀个 API( QAbstractItemModel )中。这个API时为桌⾯窗⼝开发的,它可以灵 活的作为⼀个树的基础或者多列表格或者链表。在QML中我们通常值使⽤ API的链表版本( QAbstractListModel )。API包含了⼀些需要强制实现的 函数,另外⼀些函数时可选的。可选部分通常是⽤来动态添加或者删除数 据。 《QmlBook》In Chinese 420C++数据模型(Models in C++) ⼀个典型的QML C++模型继承⾃ QAbstractListModel ,并且最少需要实 现 data 和 rowCount 函数。在这个例⼦中我们将使⽤由 QColor 类提供的⼀ 系列SVG颜⾊名称并且使⽤我们的模型展⽰它们。数据被存储 在 QList 数据容器中。 我们的 DataEntryModel 基础⾃ QAbstractListModel 并且实现了需要强制 实现的函数。我们可以在 rowCount 中忽略⽗对象索引,这只在树模型中使 ⽤。 QModelIndex 类提供了视图检索数据需要的单元格⾏和列的信息,视图 基于⾏列和数据⾓⾊从模型中拉取数 据。 QAbstractListModel 在 QtCore 中定义,但是 QColor 被定义 在 QtGui 中。我们需要附加 QtGui 依赖。对于QML应⽤程序,它可以依 赖 QtGui ,但是它通常不依赖 QtWidgets 。 #ifndef DATAENTRYMODEL_H #define DATAENTRYMODEL_H #include #include class DataEntryModel : public QAbstractListModel { Q_OBJECT public: explicit DataEntryModel(QObject *parent = 0); ~DataEntryModel(); public: // QAbstractItemModel interface virtual int rowCount(const QModelIndex &parent) const; virtual QVariant data(const QModelIndex &index, int role) const; private: QList m_data; }; #endif // DATAENTRYMODEL_H ⼀个简单的模型(A simple model) 《QmlBook》In Chinese 421⼀个简单的模型(A simple model) 现在你可以使⽤QML导⼊命令 import org.example 1.0 来访 问 DataEntryModel ,和其它QML项使⽤的⽅法⼀样 DataEntryModel {} 。 我们在这个例⼦中使⽤它来显⽰⼀个简单的颜⾊条⺫列表。 import org.example 1.0 ListView { id: view anchors.fill: parent model: DataEntryModel {} delegate: ListDelegate { // use the defined model role "display" text: model.display } highlight: ListHighlight { } } ListDelegate是⾃定义⽤来显⽰⽂本的代理。ListHighlight是⼀个矩形框。保 持例⼦的整洁在代码提取时进⾏了保留。 视图现在可以使⽤C++模型来显⽰字符串列表,并且显⽰模型的属性。它仍 然⾮常简单,但是已经可以在QML中使⽤。通常数据由外部的模型提供,这 ⾥的模型只是扮演了视图的⼀个接⼝。 《QmlBook》In Chinese 422⼀个简单的模型(A simple model) 实际⼯作中使⽤的模型数据通常⽐较复杂。所以需要⾃定义⼀些⾓⾊枚举⽅ 便视图通过属性查找数据。例如模型提供颜⾊数据不仅只是16进制字符串, 在QML中也可以是来⾃HSV颜⾊模型的⾊调,饱和度和亮度, 以“model.hue”,“model.saturation”和“model.brightness”作为参数。 #ifndef ROLEENTRYMODEL_H #define ROLEENTRYMODEL_H #include #include class RoleEntryModel : public QAbstractListModel { Q_OBJECT public: // Define the role names to be used enum RoleNames { NameRole = Qt::UserRole, HueRole = Qt::UserRole+2, SaturationRole = Qt::UserRole+3, BrightnessRole = Qt::UserRole+4 }; explicit RoleEntryModel(QObject *parent = 0); ~RoleEntryModel(); // QAbstractItemModel interface public: virtual int rowCount(const QModelIndex &parent) const override; virtual QVariant data(const QModelIndex &index, int role) const override; protected: // return the roles mapping to be used by QML virtual QHash roleNames() const override; private: QList m_data; QHash m_roleNames; 更复杂的数据(More Complex Data) 《QmlBook》In Chinese 423更复杂的数据(More Complex Data) }; #endif // ROLEENTRYMODEL_H 在头⽂件中,我们为QML添加了数据⾓⾊枚举的映射。当QML尝试访问⼀个 模型中的属性时(例如“model.name”),链表视图将会在映射中查 询“name”然后向模型申请使⽤ NameRole ⾓⾊枚举的数据。⽤户在定义⾓⾊ 枚举时应该从 Qt::UserRole 开始,并且对于每个模型需要保证唯⼀。 #include "roleentrymodel.h" RoleEntryModel::RoleEntryModel(QObject *parent) : QAbstractListModel(parent) { // Set names to the role name hash container (QHash) // model.name, model.hue, model.saturation, model.brightness m_roleNames[NameRole] = "name"; m_roleNames[HueRole] = "hue"; m_roleNames[SaturationRole] = "saturation"; m_roleNames[BrightnessRole] = "brightness"; // Append the color names as QColor to the data list (QList) for(const QString& name : QColor::colorNames()) { m_data.append(QColor(name)); } } RoleEntryModel::~RoleEntryModel() { } int RoleEntryModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return m_data.count(); } QVariant RoleEntryModel::data(const QModelIndex &index, int role) const 《QmlBook》In Chinese 424更复杂的数据(More Complex Data) { int row = index.row(); if(row < 0 || row >= m_data.count()) { return QVariant(); } const QColor& color = m_data.at(row); qDebug() << row << role << color; switch(role) { case NameRole: // return the color name as hex string (model.name) return color.name(); case HueRole: // return the hue of the color (model.hue) return color.hueF(); case SaturationRole: // return the saturation of the color (model.saturation) return color.saturationF(); case BrightnessRole: // return the brightness of the color (model.brightness) return color.lightnessF(); } return QVariant(); } QHash RoleEntryModel::roleNames() const { return m_roleNames; } 现在实现只是改变了两个地⽅。⾸先是初始化。我们使⽤ QColor 数据类型 初始化数据链表。此外我们还定义了我们⾃⼰的⾓⾊名称映射实现QML的访 问。这个映射将在后⾯的 ::roleNames 函数中返回。 第⼆个变化是在 ::data 函数中。我们确保能够覆盖到其它的⾓⾊枚举(例 如⾊调,饱和度,亮度)。没有可以从颜⾊中获取SVG名称的⽅法,由于⼀ 个颜⾊可以替代任何颜⾊,但SVG名称是受限的。所以我们忽略掉这点。我 们需要创建⼀个结构体 { QColor, QString } 来存储名称,这样可以鉴别已 被命名的颜⾊。 《QmlBook》In Chinese 425更复杂的数据(More Complex Data) 在注册类型完成后,我们可以使⽤模型了,可以将它的条⺫显⽰在我们的⽤ 户界⾯中。 ListView { id: view anchors.fill: parent model: RoleEntryModel {} focus: true delegate: ListDelegate { text: 'hsv(' + Number(model.hue).toFixed(2) + ',' + Number(model.saturation).toFixed() + ',' + Number(model.brightness).toFixed() + ')' color: model.name } highlight: ListHighlight { } } 我们将返回的类型转换为JS数字类型,这样可以使⽤定点标记来格式化数 字。代码中应当避免直接调⽤数字(例 如 model.saturation.toFixed(2) )。选择哪种格式取决于你的输⼊数据。 《QmlBook》In Chinese 426更复杂的数据(More Complex Data) 动态数据包含了从模型中插⼊,移除,清除数据 等。 QAbstractListModel 期望当条⺫被移除或者插⼊时有⼀个明确的⾏ 为。这个⾏为使⽤⼀个信号来表⽰,在操作调⽤前和调⽤后调⽤这个⾏为。 例如向⼀个模型插⼊⼀⾏数据,你⾸先需要发送 beginInsertRows 信号,然 后操作数据,最后发送 endInsertRows 信号。 我们将在头⽂件中加⼊后续的函数。这些使⽤ Q_INVOKABLE 函数定义使得可 以在QML中调⽤它们。另⼀种⽅法是将它们定义为公共槽函数。 // inserts a color at the index (0 at begining, count-1 at end) Q_INVOKABLE void insert(int index, const QString& colorValue); // uses insert to insert a color at the end Q_INVOKABLE void append(const QString& colorValue); // removes a color from the index Q_INVOKABLE void remove(int index); // clear the whole model (e.g. reset) Q_INVOKABLE void clear(); 此外,我们定义了 count 属性来获取模型的⼤⼩和⼀个使⽤索引值的 get ⽅ 法来获取颜⾊。这些东⻄在QML中使⽤迭代器遍历模型时会⽤到。 // gives the size of the model Q_PROPERTY(int count READ count NOTIFY countChanged) // gets a color at the index Q_INVOKABLE QColor get(int index); 实现插⼊数据⾸先要检查边界和插⼊值是否有效。在这之后我们开始插⼊数 据。 动态数据(Dynamic Data) 《QmlBook》In Chinese 427动态数据(Dynamic Data) void DynamicEntryModel::insert(int index, const QString &colorValue) { if(index < 0 || index > m_data.count()) { return; } QColor color(colorValue); if(!color.isValid()) { return; } // view protocol (begin => manipulate => end] emit beginInsertRows(QModelIndex(), index, index); m_data.insert(index, color); emit endInsertRows(); // update our count property emit countChanged(m_data.count()); } 添加数据⾮常简单。我们使⽤模型⼤⼩并调⽤插⼊函数来实现。 void DynamicEntryModel::append(const QString &colorValue) { insert(count(), colorValue); } 移除数据与插⼊数据类似,但是需要调⽤移除操作协议。 void DynamicEntryModel::remove(int index) { if(index < 0 || index >= m_data.count()) { return; } emit beginRemoveRows(QModelIndex(), index, index); m_data.removeAt(index); emit endRemoveRows(); // do not forget to update our count property emit countChanged(m_data.count()); 《QmlBook》In Chinese 428动态数据(Dynamic Data) } 函数 count 不太重要,这⾥不再介绍,只需要知道它会返回数据总 数。 get 函数也⼗分简单。 QColor DynamicEntryModel::get(int index) { if(index < 0 || index >= m_data.count()) { return QColor(); } return m_data.at(index); } 你需要注意你只能返回⼀个QML可读取的值。如果它不是QML基础类型或者 QML所知类型,你需要使⽤qmlRegisterType或者 qmlRegisterUncreatableType注册类型。如果是⽤户不能在QML中实例化对 象的类型应该使⽤qmlRegisterUncreatableType注册。 现在你可以在QML中使⽤模型并且可以从模型中插⼊,添加,移除条⺫。这 ⾥有⼀个⼩例⼦,它允许⽤户输⼊⼀个颜⾊名称或者颜⾊16进制值,并将这 个颜⾊加⼊到模型中在链表视图中显⽰。代理上的红⾊圆圈允许⽤户从模型 中移除这个条⺫。在条⺫被移除后,模型通知链表视图更新它的内容。 《QmlBook》In Chinese 429动态数据(Dynamic Data) 这⾥是QML代码。你可以在这章的资源⾥找到完整的源代码。这个例⼦使⽤ 了QtQuick.Controls和QtQuick.Layout模块使得代码更加紧凑。控制模块提供 了QtQuick中⼀组与桌⾯相关的⽤户界⾯元素,布局模块提供了⾮常有⽤的布 局管理器。 import QtQuick 2.2 import QtQuick.Window 2.0 import QtQuick.Controls 1.2 import QtQuick.Layouts 1.1 // our module 《QmlBook》In Chinese 430动态数据(Dynamic Data) import org.example 1.0 Window { visible: true width: 480 height: 480 Background { // a dark background id: background } // our dyanmic model DynamicEntryModel { id: dynamic onCountChanged: { // we print out count and the last entry when count is changing print('new count: ' + count); print('last entry: ' + get(count-1)); } } ColumnLayout { anchors.fill: parent anchors.margins: 8 ScrollView { Layout.fillHeight: true Layout.fillWidth: true ListView { id: view // set our dynamic model to the views model property model: dynamic delegate: ListDelegate { width: ListView.view.width // construct a string based on the models proeprties text: 'hsv(' + Number(model.hue).toFixed(2) + ',' + Number(model.saturation).toFixed() + ',' + Number(model.brightness).toFixed() + ')' // sets the font color of our custom delegates color: model.name 《QmlBook》In Chinese 431动态数据(Dynamic Data) onClicked: { // make this delegate the current item view.currentIndex = index view.focus = true } onRemove: { // remove the current entry from the model dynamic.remove(index) } } highlight: ListHighlight { } // some fun with transitions :-) add: Transition { // applied when entry is added NumberAnimation { properties: "x"; from: -view.width; duration: 250; easing.type: Easing.InCirc } NumberAnimation { properties: "y"; from: view.height; duration: 250; easing.type: Easing.InCirc } } remove: Transition { // applied when entry is removed NumberAnimation { properties: "x"; to: view.width; duration: 250; easing.type: Easing.InBounce } } displaced: Transition { // applied when entry is moved // (e.g because another element was removed) SequentialAnimation { // wait until remove has finished PauseAnimation { duration: 250 } NumberAnimation { properties: "y"; duration: 75 } } } } } TextEntry { 《QmlBook》In Chinese 432动态数据(Dynamic Data) id: textEntry onAppend: { // called when the user presses return on the text field // or clicks the add button dynamic.append(color) } onUp: { // called when the user presses up while the text field is focused view.decrementCurrentIndex() } onDown: { // same for down view.incrementCurrentIndex() } } } } 模型-视图编程是Qt中最难的任务之⼀。对于正常的应⽤开发者,它是为数不 多的需要实现接⼝的类。其它类你只需要正常使⽤就可以额。模型的草图通 常在QML这边开始。你需要想象你的⽤户在QML中需要什么样的模型。通常 建议创建协议时⾸先使⽤ListModel看看如何在QML中更好的⼯作。这种⽅法 对于定义QML编程接⼝同样有效。使数据从C++到QML中可⽤不仅仅是技术 边界,也是从命令式编程到声明式编程的编程⽅法转变。所以准备好经历⼀ 些挫折并从中获取快乐吧。 《QmlBook》In Chinese 433动态数据(Dynamic Data) 进阶技巧(Advanced Techniques) 《QmlBook》In Chinese 434进阶技巧(Advanced Techniques) QML执⾏在受限的空间中,QML作为⼀种语⾔提供的功能有时是被限制的。 通过C++写的本地函数可以扩展QML运⾏时的功能。应⽤程序可以充分利⽤ 基础平台的性能和⾃由度。 C++扩展QML(Extending QML with C++) 《QmlBook》In Chinese 435C++扩展QML(Extending QML with C++) 当运⾏QML时,它在⼀个运⾏时环境下执⾏。这个运⾏时环境是由 QtQml 模 块下的C++代码实现的。它由⼀个负责执⾏QML的引擎,持有访问每个组件 属性的上下⽂和实例化的QML元素组件构成。 #include #include int main(int argc, char **argv) { QGuiApplication app(argc, argv); QUrl source(QStringLiteral("qrc:/main.qml")); QQmlApplicationEngine engine; engine.load(source); return app.exec(); } 在这个例⼦中, QGuiApplication 封装了所有与应⽤程序引⽤相关的属性 (例如应⽤程序名称,命令⾏参数,和事件循环管 理)。 QQmlApplicationEngine 分层管理上下⽂和组件的顺序。它需要加载 ⼀个典型的qml⽂件作为应⽤程序的开始点。在这个例⼦中, main.qml 包含 了以⼀个窗⼝和⼀个⽂本。 注意 通过 QmlApplicationEngine 加载⼀个使⽤简单项作为根类型 的 main.qml 不会在你的屏幕上显⽰任何东⻄,它需要⼀个窗⼝来管理⼀个 平⾯的渲染。引擎可以加载不包含任何⽤户界⾯的qml代码(例如⼀个纯粹 的对象)。由于它不会默认为你创建⼀个窗⼝。 qmlcene 或者新的qml运⾏ 环境将会在内部⾸先检查 main.qml ⽂件是否包含⼀个窗⼝作为根项,如果 没有包含将会为你创建⼀个并且设置根项作为新创建窗⼝的⼦项。 理解QML运⾏环境(Understanding the QML Run-time) 《QmlBook》In Chinese 436理解QML运⾏环境(Understanding the QML Run-time) import QtQuick 2.4 import QtQuick.Window 2.0 Window { visible: true width: 512 height: 300 Text { anchors.centerIn: parent text: "Hello World!" } } 在QML⽂件中我们定义我们的依赖是 QtQuick 和 QtQuick.Window 。这些定 义将会触发在导⼊的路径中查找这些模块,并在加载成功后由引擎加载需要 的插件。新加载的类型将会倍qmldir控制在qml⽂件中可⽤。 当然也可以使⽤快速创建插件直接向引擎添加我们的⾃定义类型。这⾥我们 假设我们有⼀个基于 QObject 的 CurrentTime 类。 QQmlApplicationEngine engine; qmlRegisterType("org.example", 1, 0, "CurrentTime"); engine.load(source); 现在我们也可可以在我们的qml⽂件中使⽤ CurrentTime 类型。 import org.example 1.0 CurrentTime { // access properties, functions, signals } 《QmlBook》In Chinese 437理解QML运⾏环境(Understanding the QML Run-time) ⼀种更偷懒的⽅式是通过上下⽂属性直接设置。 QScopedPointer current(new CurrentTime()); QQmlApplicationEngine engine; engine.rootContext().setContextProperty("current", current.value()) engine.load(source); 注意 不要混淆 setContextProperty() 和 setProperty()。 setContextProperty() 是设置⼀个qml上下⽂的属 性, setProperty() 是设置⼀个QObject的动态属性值,这对你没什么帮 助。 现在你可以在你的应⽤程序的任何地⽅使⽤这个属性了。感谢上下⽂继承这 ⼀特性。 import QtQuick 2.4 import QtQuick.Window 2.0 Window { visible: true width: 512 height: 300 Component.onCompleted: { console.log('current: ' + current) } } 通常有以下⼏种不同的⽅式扩展QML: 《QmlBook》In Chinese 438理解QML运⾏环境(Understanding the QML Run-time) 上下⽂属性 - setContextProperty() 引擎注册类型 - 在main.cpp中调⽤qmlRegisterType QML扩展插件 - 后⾯会讨论 上下⽂属性使⽤对于⼩型的应⽤程序使⽤⾮常⽅便。它们不需要你做太多的 事情就可以将系统编程接⼝暴露为友好的全局对象。它有助于确保不会出现 命名冲突(例如使⽤($)这种特殊符号,例如$.currentTime)。在JS变量 中$是⼀个有效的字符。 注册QML类型允许⽤户从QML中控制⼀个c++对象的⽣命周期。上下⽂属性 ⽆法完成这间事情。它也不会破坏全局命名空间。所有的类型都需要先注 册,并且在程序启动时会链接所有库,这在⼤多数情况下都不是⼀个问题。 QML扩展插件提供了最灵活的扩展系统。它允许你在插件中注册类型,在第 ⼀个QML⽂件调⽤导⼊鉴定时会加载这个插件。由于使⽤了⼀个QML单例这 也不会再破坏全局命名空间。插件允许你跨项⺫重⽤模块,这在你使⽤Qt包 含多个项⺫时⾮常⽅便。 这章的其余部分将会集中在qml扩展插件上讨论。它们提供了最好的灵活性和 可重⽤性。 《QmlBook》In Chinese 439理解QML运⾏环境(Understanding the QML Run-time) 插件是⼀个已定义接⼝的库,它只在需要时才被加载。这与⼀个库在程序启 动时被链接和加载不同。在QML场景下,这个接⼝叫 做 QQmlExtensionPlugin 。我们关⼼其中的两个⽅ 法 initializeEngine() 和 registerTypes() 。当插件被加载时,⾸先会调 ⽤ initializeEngine() ,它允许我们访问引擎将插件对象暴露给根上下 ⽂。⼤多数时候你只会使⽤到 registerTypes() ⽅法。它允许你注册你⾃定 义的QML类型到引擎提供的地址上。 我们稍微退⼀步考虑⼀个潜在的⽂件IO类型,它允许我们在QML中读取/写⼊ ⼀个⼩型⽂本⽂件。第⼀次的迭代可能看起来像在嘲笑QML的实现。 // FileIO.qml (good) QtObject { function write(path, text) {}; function read(path) { return "TEXT"} } 这是⼀个纯粹的qml可能的实现,C++基于QML编程接⼝来探索⼀些编程接 ⼝。我们看到我们有⼀个读取和写⼊函数。写⼊函数需要⼀个路径和⼀个⽂ 本,读取函数需要⼀个路径,返回⼀个⽂本。路径和⽂本看起来是公共参 数,或许我们可以将它们提取作为属性。 // FileIO.qml (better) QtObject { property url source property string text function write() { // open file and write text }; function read() { // read file and assign to text }; } 插件内容(Plugin Content) 《QmlBook》In Chinese 440插件内容(Plugin Content) 当然这看起来更像⼀个QML编程接⼝。我们使⽤属性让我们的环境能够绑定 我们的属性并且响应变化。 在C++中创建这个编程接⼝我们需要创建类似的⼀个接⼝。 class FileIO : public QObject { ... Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) ... public: Q_INVOKABLE void read(); Q_INVOKABLE void write(); ... } QML引擎需要注册 FileIO 类型。我们想要在 org.example.io 模块中使⽤ 它。 import org.example.io 1.0 FileIO { } ⼀个插件可以在相同的模块中暴露若干个类型。但是不能从⼀个插件中暴露 若干个模块。所以模块与插件之间的关系是⼀对⼀的。这个关系由模块标识 符表⽰。 《QmlBook》In Chinese 441插件内容(Plugin Content) Qt Creator包含了⼀个创建 QtQuick 2 QML Extension Plugin 向导,我们 使⽤它来创建⼀个叫做 fileio 的插件,这个插件包含了⼀个 从 org.example.io 中启动的 FileIO 对象。 插件类源于 QQmlExtensionPlugin ,并且实现了 registerTypes() 函 数。 Q_PLUGIN_METADATA 是强制标识这个插件作为⼀个qml扩展插件。除此 之外没有其它特殊的地⽅了。 #ifndef FILEIO_PLUGIN_H #define FILEIO_PLUGIN_H #include class FileioPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") public: void registerTypes(const char *uri); }; #endif // FILEIO_PLUGIN_H 在实现 registerTypes 中我们使⽤ qmlRegisterType 函数注册了我们的 FileIO类。 #include "fileio_plugin.h" #include "fileio.h" #include 创建插件(Creating the plugin) 《QmlBook》In Chinese 442创建插件(Creating the plugin) void FileioPlugin::registerTypes(const char *uri) { // @uri org.example.io qmlRegisterType(uri, 1, 0, "FileIO"); } 有趣的是我们不能在这⾥看到模块统⼀资源标识符(例 如 org.example.io )。这似乎是从外⾯设置的。 看你查找你的项⺫⽂件夹是,你会发现⼀个qmldir⽂件。这个⽂件指定了你 的qml插件内容或者最好是你插件中关于QML的部分。它看起来应该像这 样。 module org.example.io plugin fileio 模块是统⼀资源标识符,在统⼀标识符下插件能够被其它插件获取,并且插 件⾏必须与插件⽂件名完全相同(在mac下,它将 是 libfileio_debug.dylib 存在于⽂件系统上, fileio 在 qmldir 中)。 这些⽂件由Qt Creator基于给定的信息创建。模块的标识符在 .pro ⽂件中同 样可⽤。⽤来构建安装⽂件夹。 当你在构建⽂件夹中调⽤ make install 时,将会拷⻉库⽂件到Qt qml ⽂件 夹中(在Qt5.4之后mac上这将在 ~/Qt/5.4/clang_64/qml ⽂件夹中。这个 路径依赖Qt按住那个位置,并且使⽤系统上的编译器)。你将会 在 org/example/io ⽂件夹中发现库⽂件。⺫前包含两个⽂件。 libfileio_debug.dylib qmldir 当导⼊⼀个叫做 org.example.io 的模块时,qml引擎将会在导⼊路径下查找 ⼀个可⽤模块并且尝试使⽤qmldir⽂件定位 org/example/io 路径。qmldir会 告诉引擎使⽤哪个模块标识符加在哪个库作为qml扩展插件。两个模块使⽤相 《QmlBook》In Chinese 443创建插件(Creating the plugin) 同的统⼀标识符将会相互覆盖。 《QmlBook》In Chinese 444创建插件(Creating the plugin) 类 FileIO 实现很简单。记住编程接⼝我们想要创建的像这样。 class FileIO : public QObject { ... Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) ... public: Q_INVOKABLE void read(); Q_INVOKABLE void write(); ... } 我们将保留属性,因为它们是简单的设置者和获取者。 读取⽅法在读取模式下打开⼀个⽂件并且使⽤⼀个⽂本流读取数据。 void FileIO::read() { if(m_source.isEmpty()) { return; } QFile file(m_source.toLocalFile()); if(!file.exists()) { qWarning() << "Does not exits: " << m_source.toLocalFile(); return; } if(file.open(QIODevice::ReadOnly)) { QTextStream stream(&file); m_text = stream.readAll(); emit textChanged(m_text); } } FileIO实现(FileIO Implementation) 《QmlBook》In Chinese 445FileIO实现(FileIO Implementation) 当⽂本变化时,使⽤ emit textChanged(m_text) 需要通知其它对象这个变 化。否则属性绑定⽆法⼯作。 写⼊⽅法相同,但是在写⼊模式下打开⽂件,使⽤⽂本流写⼊内容。 void FileIO::write() { if(m_source.isEmpty()) { return; } QFile file(m_source.toLocalFile()); if(file.open(QIODevice::WriteOnly)) { QTextStream stream(&file); stream << m_text; } } 最后不要忘记调⽤make install。否则你的插件⽂件不会拷⻉到qml⽂件夹, qml引擎⽆法定位模块。 由于读取和写⼊会阻塞程序运⾏,你只能使⽤FileIO处理⼩型⽂本,否则会 阻塞Qt的UI线程运⾏。这⾥⼀定要注意! 《QmlBook》In Chinese 446FileIO实现(FileIO Implementation) 现在我们可以使⽤新创建的⽂件访问⼀些简单的数据。这个例⼦中我们想要 读取⼀个JSON格式下的城市数据并且在表格中显⽰。我们将使⽤两个项 ⺫,⼀个是扩展插件项⺫(叫做 fileio ),提供读取和写⼊⽂件的⽅法。 另外⼀个项⺫通过fileio读取/写⼊⽂件将数据显⽰在表格中( CityUI )。这 个例⼦中使⽤的数据在 cities.json ⽂件中。 JSON只是⽂本,它被格式化为可以转换为⼀个有效的JS对象/数组并返回⼀ 个⽂本。我们使⽤ FileIO 读取格式化的JSON数据并使⽤ JSON.parse() 将 它转换为⼀个JS对象。数据在后⾯被⽤作⼀个表格视图的数据模型。我们粗 略的阅读函数⽂档就可以获取这些内容。为了保存数据我们将转换回⽂本格 式并使⽤写⼊函数保存。 城市的JSON数据是⼀个格式化⽂本⽂件,包含了⼀组城市数据条⺫,每个 条⺫包含了关于城市数据。 使⽤FileIO(Using FileIO) 《QmlBook》In Chinese 447使⽤FileIO(Using FileIO) [ { "area": "1928", "city": "Shanghai", "country": "China", "flag": "22px-Flag_of_the_People's_Republic_of_China.svg.png", "population": "13831900" }, ... ] 《QmlBook》In Chinese 448使⽤FileIO(Using FileIO) 使⽤Qt Creator的QtQuick Application向导创建⼀个基于QtQuick controls的 应⽤程序。我们将不再使⽤新的QML格式,这在⼀本书⾥⾯将很难解释,即 使新格式使⽤ui.qml⽂件将⽐之前更加容易达到⺫的。所以你可以移除/删除 格式⽂件。 ⼀个应⽤程序窗⼝基础配置包含了⼀个⼯具栏,菜单栏和状态栏。我们只使 ⽤菜单栏创建⼀些典型的菜单条⺫来打开和保存⽂档。基础配置的窗⼝只会 显⽰⼀个空的窗⼝。 import QtQuick 2.4 import QtQuick.Controls 1.3 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.2 ApplicationWindow { id: root title: qsTr("City UI") width: 640 height: 480 visible: true } 应⽤程序窗⼝(The Application Window) 《QmlBook》In Chinese 449应⽤程序窗⼜(The Application Window) 为了更好的使⽤/复⽤我们的命令,我们使⽤QML Action 类型。这将允许我 们在后⾯可以使⽤相同的动作,也可以⽤于潜在的⼯具栏。打开,保存和退 出动作是标准动作。打开和保存动作不会包含任何逻辑,我们后⾯再来添 加。菜单栏由⼀个⽂件菜单和这三个动作条⺫组成。此外我们已经准备了⼀ 个⽂件对话框,它可以让我们选择我们的城市⽂档。对话框在定义时是不可 ⻅的,需要使⽤ open() ⽅法来显⽰它。 ... Action { id: save text: qsTr("&Save") shortcut: StandardKey.Save onTriggered: { } } Action { id: open text: qsTr("&Open") shortcut: StandardKey.Open onTriggered: {} } Action { id: exit text: qsTr("E&xit") onTriggered: Qt.quit(); } menuBar: MenuBar { Menu { title: qsTr("&File") MenuItem { action: open } MenuItem { action: save } MenuSeparator { } MenuItem { action: exit } 使⽤动作(Using Actions) 《QmlBook》In Chinese 450使⽤动作(Using Actions) } } ... FileDialog { id: openDialog onAccepted: { } } 《QmlBook》In Chinese 451使⽤动作(Using Actions) 城市数据的内容应该被现实在⼀个表格中。我们使⽤ TableView 控制并定义 4列:城市,国家,⾯积,⼈⼝。每⼀列都是典型的 TableViewColumn 。然 后我们添加列的标识并移除要求⾃定义列代理的操作。 TableView { id: view anchors.fill: parent TableViewColumn { role: 'city' title: "City" width: 120 } TableViewColumn { role: 'country' title: "Country" width: 120 } TableViewColumn { role: 'area' title: "Area" width: 80 } TableViewColumn { role: 'population' title: "Population" width: 80 } } 现在应⽤程序能够显⽰⼀个包含⽂件菜单的菜单栏和⼀个包含4个表头的空表 格。下⼀步是我们的 FileIO 扩展将有⽤的数据填充到表格中。 格式化表格(Formatting the Table) 《QmlBook》In Chinese 452格式化表格(Formatting the Table) ⽂档cities.json是⼀组城市条⺫。这⾥是⼀个例⼦。 [ { "area": "1928", "city": "Shanghai", "country": "China", "flag": "22px-Flag_of_the_People's_Republic_of_China.svg.png", "population": "13831900" }, ... ] 我们任务是允许⽤户选择⽂件,读取它,转换它,并将它设置到表格视图 中。 《QmlBook》In Chinese 453格式化表格(Formatting the Table) 我们让打开动作打开⼀个⽂件对话框。当⽤户已选择⼀个⽂件后,在⽂件对 话框上的 onAccepted ⽅法被调⽤。这⾥我们调⽤ readDocument() 函 数。 readDocument 函数将来⾃⽂件对话框的地址设置到我们的 FileIO 对 象,并调⽤ read() ⽅法。从 FileIO 中加载的⽂本使⽤ JSON.parse() ⽅法 解析,并将结果对象作为数据模型直接设置到表格视图上。这样⾮常⽅便。 Action { id: open ... onTriggered: { openDialog.open() } } ... FileDialog { id: openDialog onAccepted: { root.readDocument() } } function readDocument() { io.source = openDialog.fileUrl io.read() view.model = JSON.parse(io.text) } FileIO { id: io } 读取数据(Reading Data) 《QmlBook》In Chinese 454读取数据(Reading Data) 我们连接保存动作到 saveDocument() 函数来保存⽂档。保存⽂档函数从视 图中取出模型,模型是⼀个JS对象,并使⽤ JSON.stringify() 函数将它转 换为⼀个字符串。将结果字符串设置到 FileIO 对象的⽂本属性中,并调 ⽤ write() 来保存数据到磁盘中。在 stringify 函数上参数 null 和 4 将会 使⽤4个空格缩进格式化JSON数据结果。这只是为了保存⽂档更好阅读。 Action { id: save ... onTriggered: { saveDocument() } } function saveDocument() { var data = view.model io.text = JSON.stringify(data, null, 4) io.write() } FileIO { id: io } 从根本上说,这个应⽤程序就是读取,写⼊和现实⼀个JSON⽂档。考虑下 如果使⽤XML格式读取和写⼊,会花多少时间。使⽤JSON格式你只需要读 取/写⼊⼀个⽂本⽂件或者发送/接收⼀个⽂本缓存。 写⼊数据(Writing Data) 《QmlBook》In Chinese 455写⼊数据(Writing Data) 《QmlBook》In Chinese 456写⼊数据(Writing Data) 这个应⽤程序还没有真正的完成。我们想要显⽰旗帜,并允许⽤户通过从数 据模型中移除城市来修改⽂档。 这些旗帜被存放在 main.qml ⽂件夹下的 flags ⽂件夹中。为了在表格列中 显⽰它们,我们需要定义⼀个渲染旗帜图⽚的代理。 TableViewColumn { delegate: Item { Image { anchors.centerIn: parent source: 'flags/' + styleData.value } } role: 'flag' title: "Flag" width: 40 } 它将JS数据模型中暴露的flag属性作为 styleData.value 交给代理。代理调 整图⽚路径,并在路径前⾯加上 'flags/' 并显⽰它。 对于移除,我们使⽤相似的技巧来显⽰⼀个移除按钮。 TableViewColumn { delegate: Button { iconSource: "remove.png" onClicked: { var data = view.model data.splice(styleData.row, 1) view.model = data } } width: 40 收尾⼯作(Finishing Touch) 《QmlBook》In Chinese 457收尾⼯作(Finishing Touch) } 数据移除操作,我们坚持从视图模型上获取数据,然后使⽤JS的 splice 函 数移除⼀个条⺫。这个⽅法提供给我们的模型来⾃⼀个JS数组。 splice ⽅ 法通过移除已有元素,添加新的元素来改变数组内容。 ⼀个JS数组不如⼀个Qt模型智能,例如 QAbstractItemModel ,它⽆法通知 视图⾏更新或者数据更新。由于视图⽆法接收到任何更新的通知,它⽆法更 新数据显⽰。只有在我们将数据重新设置回视图时,视图才会知道有新的数 据需要刷新视图内容。使⽤ view.model = data 再次设置数据模型可以让视 图知道有数据更新。 《QmlBook》In Chinese 458收尾⼯作(Finishing Touch) 插件的创建⾮常简单,但是它可以复⽤,并且为不同的应⽤程序扩展类型。 使⽤创建的插件是⾮常灵活的解决⽅案。例如你可以只使⽤ qmlscene 开始 创建UI。打开CityUI项⺫⽂件夹,从 qmlscene 的 main.qml 开始。我真的⿎ 励⼤家使⽤ 与qmlscene ⼀起⼯作的⽅式写应⽤程序。对于UI开发者,这将 是⼀个巨⼤的改变,也是⼀个好的习惯来保证清晰的分离。 使⽤插件有⼀个缺点,对于简单的应⽤程序开发增加了难度。你需要为你的 应⽤程序开发插件。如果这是⼀个问题,你也可以使⽤与 FileIO 对象相同 的机制使⽤ qmlRegisterType 直接注册到你的 main.cpp 中。QML代码保持 ⼀样就可以了。 通常在⼤型项⺫中,你不会像这样使⽤应⽤程序。你有⼀个与 qmlscene 类 似的简单的qml运⾏环境,并且需要所有本地的功能插件。你的项⺫使⽤这些 qml扩展插件,也是简单纯粹的qml项⺫。这为UI的变换提供了最⼤的灵活性 并移除了编译步骤。在编辑完成⼀个QML⽂件后,你只需要运⾏UI。这允许 ⽤户界⾯开发者保持灵活性并迅速的使所有的⼩修改⽴刻得到响应。 插件提供了健壮和清晰的C++后台开发与QML前端开发的分离。当开发QML 插件时,通常在QML端有⼀个想法,并在使⽤C+=实现前,可以使⽤QML的 样本模型进⾏API验证。如果API是C++⼈员写的,通常会犹豫去改变它或者 重写它。复制⼀个QML提供的API通常更加灵活并且初始投资更少。当使⽤ 插件切换⼀个样本模型API和⼀个真是API时,仅仅只需要改变qml运⾏环境 的导⼊路径。 总结(Summary) 《QmlBook》In Chinese 459总结(Summary) 该章节介绍了⼀些其它相关的内容,原⽂内不存在本章节。 其它(Other) 《QmlBook》In Chinese 460其它(Other) Chapter 01 examples (ch01-assets.tgz) Chapter 04 examples (ch04-assets.tgz) Chapter 05 examples (ch05-assets.tgz) Chapter 06 examples (ch06-assets.tgz) Chapter 07 examples (ch07-assets.tgz) Chapter 08 examples (ch08-assets.tgz) Chapter 09 examples (ch09-assets.tgz) Chapter 10 examples (ch10-assets.tgz) Chapter 11 examples (ch11-assets.tgz) Chapter 12 examples (ch12-assets.tgz) Chapter 13 examples (ch13-assets.tgz) Chapter 14 examples (ch14-assets.tgz) Chapter 15 examples (ch15-assets.tgz) Chapter 16 examples (ch16-assets.tgz) ⽰例源码 《QmlBook》In Chinese 461⽰例源码 术语英汉对照表 《QmlBook》In Chinese 462术语英汉对照表 格式定义 《QmlBook》In Chinese 463格式定义 很多热⼼的爱好者想要知道如何帮忙校对,这⾥我再增加⼀个详细的教程帮 助⼤家。 注册github账号,下载markdown编辑⼯具 ⾸先在github上注册⼀个账号,然后下载markdown编辑⼯具,我使⽤的是 GitBook Editor,点这⾥下。 当然你也可以使⽤其它的markdown编辑⼯具。 创建⾃⼰的⼯作分⽀ 协作校正 《QmlBook》In Chinese 464协作校正 登录你的github账号后,进⼊我们项⺫的⻚⾯,点击上图右上⾓的fork,创建 ⾃⼰的⼯作分⽀。 下图是我测试号的⼯作分⽀。 《QmlBook》In Chinese 465协作校正 下载TortoiseGit,克隆你的⼯作分⽀到本地 下载TortoiseGit⼯具,点我下载,你也可以使⽤其它的⼯具来克隆你的⼯作 分⽀。 下图是使⽤TortoiseGit⼯具克隆⼯作分⽀的界⾯截图。 《QmlBook》In Chinese 466协作校正 使⽤gitbook客户端打开项⺫⽂件夹,开始校对 使⽤gitbook客户端打开项⺫⽂件夹,就能开始编译校对,下图是打开项⺫⽂ 件夹的截图。 《QmlBook》In Chinese 467协作校正 校对完成后上传到你在github上的⼯作分⽀ 校对完成后,⾸先使⽤Git Commit->master上传到本地库,然后使⽤pull上传 到github上。 下图是上传信息的名字与联系⽅式的补充。 《QmlBook》In Chinese 468协作校正 下图是上传到本地库的界⾯截图。 《QmlBook》In Chinese 469协作校正 上传本地库完成后,点击pull上传到你在github上的⼯作分⽀。 《QmlBook》In Chinese 470协作校正 确定上传⼯作分⽀地址,就是你在github上的⼯作分⽀地址。 《QmlBook》In Chinese 471协作校正 提交pull rqeuset到我的项⺫ 《QmlBook》In Chinese 472协作校正 再次进⼊你的github⼯作分⽀⻚,点击右边的pull request进⼊。 《QmlBook》In Chinese 473协作校正 点击上图的New pull request绿⾊按键,进⼊修改提交。 系统会检测你的⼯作分⽀与我们项⺫的差别,确认提交内容,点击Create pull rqeust绿⾊按键添加修改内容描述。 《QmlBook》In Chinese 474协作校正 完成描述后点击上图的Create pull request绿⾊按键确认提交。 下图为我的项⺫收到新的pull request的请求,我会确认提交内容后合并。 《QmlBook》In Chinese 475协作校正 《QmlBook》In Chinese 476协作校正
还剩475页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 8 人已下载

下载pdf

pdf贡献者

wisdom_hy

贡献于2017-09-29

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf