首页 > 代码库 > Qt Quick应用开发介绍 6-8
Qt Quick应用开发介绍 6-8
Chapter6 Using JavaScript 使用JavaScript
在QtQuick中JavaScript可以有很多复杂和强大的用法; 实际上, QtQuick是被实现成一个JavaScript的扩展; JS基本可以在任何地方使用, 只要代码返回的值的类型和预期的一致; 此外, 使用JS是一部分处理应用逻辑和计算的代码的标准形式;
6.1 JavaScript is not JavaScript
JS是从web开发产生的; 在那段时间内, JS快速成长为许多受欢迎和优秀的扩展, add-ons的开发工具; 为了有更加广泛的支持, JS被标准化, 成为ECMAScript-262标准的开发语言; 要强调ECMAScript-262只覆盖了语言部分, 像获取web页面的对象和库的API这样的附加方法没有涉及; 不管标准化花了多大力气, 许多JS的web开发细节仍旧是浏览器特定的, 甚至近年来更严重; http://en.wikipedia.org/wiki/Javascript
JS也在web之外被使用, 裁剪成支持某种用例的版本; 用户端的JS用法在web开发中还是占有主导地位的; 所有的书本和多数的web资源实际上都是为web开发存在的; QtQuick属于一个在web之外使用JS的平台; 如果你以后对JS了解更多, 注意其中的区别;
Qt开发团队在尽力提供更多QtQuick中JS的细节, 本文会涉及其中一部分;
6.2 More about JavaScript
本文包含了一份JavaScript基础的附录, 如果你不熟悉JS, 建议先阅读一下;
除了附录, 也可以看看Mozilla的开发者网络资源:
http://developer.mozilla.org/en/JavaScript/About_JavaScript http://developer.mozilla.org/en/A_re-introduction_to_JavaScript http://developer.mozilla.org/en/JavaScript/Guide
下面三篇文章解释了JS在QtQuick中的基本要素:
- 整合JS: QtQuick中使用JS的关键点 http://qt-project.org/doc/qt-4.8/qdeclarativejavascript.html [stateless helper functions, .pragma library, Qt.include("factorial.js"), Component.onCompleted, Global对象是constant的, 目前大多上下文中的this未定义]
Note [There is no way to create a property binding directly from imperative JavaScript code, although it is possible to use the Binding element.]
- ECMAScript参考: 在QtScript/QtQuick中支持的内建类型, 方法和属性的列表; http://qt-project.org/doc/qt-4.8/ecmascript.html
- QML Scope 解释了JS对象和QtQuick item的可见性 http://qt-project.org/doc/qt-4.8/qdeclarativescope.html
注意不久以后Qt Doc对JS的使用可能会有重大更新, 随着新的Qt发布更全面的使用覆盖;
6.3 Adding Logic to Make the Clock Tick 添加逻辑让时钟走动
之前我们已经用了一些JS, 错误处理之类; 这节要使用JS显示时间日期;
我们将从全局对象获取现在的时间日期; 返回值将被格式化, 留下日期和时间信息; http://qt-project.org/doc/qt-4.8/qml-qt.html#formatDateTime-method
1 2 3 4 | function getFormattedDateTime(format) { var date = new Date return Qt.formatDateTime(date, format) } |
Qt.formatDateTime属于QML全局对象, 除了ECMAScript Reference 中定义的标准之外, 其中还提供了很多其他有用的功能;
getFormattedDateTime()被另一个方法使用, 在Text元素中创建真实的值:
1 2 3 4 5 | function updateTime() { root.currentTime = "<big>" + getFormattedDateTime(Style.timeFormat) + "</big>" + (showSeconds ? "<sup><small> " + getFormattedDateTime( "ss" ) + "</small></sup>" : "" ); root.currentDate = getFormattedDateTime(Style.dateFormat); } |
Note 我们使用多格式文本rich-text格式化text的时间值;
在showSeconds上使用三元运算符conditional operator/ternary operator, 它是一个自定义属性, 表明时间是否要显示秒数; 在QtQuick中使用conditional operator来将属性(或者变量)绑定到一个依赖条件决定的值上面, 是非常方便的;
updateTime()会触发currentTime和currentDate不断更新; 使用Timer元素:
1 2 3 4 5 6 7 8 9 10 11 12 | Timer { id: updateTimer running: Qt.application.active && visible == true repeat: true triggeredOnStart: true onTriggered: { updateTime() // refresh the interval to update the time each second or minute. // consider the delta in order to update on a full minute interval = 1000*(showSeconds? 1 : (60 - getFormattedDateTime( "ss" ))) } } |
实现中有些有趣部分: 为了优化耗电, 把timer的running属性绑定到2个其他属性上, 从而减轻CPU负荷; 当clock元素不可见(在使用其他应用时)或者程序不再处于活动状态(在后台运行或最小化iconified)
我们在没有启动但是timer触发的时候也给interval属性分配了值; 这是为了在秒数没有被使用的时候来抓取增量时间的, 以保证时间的更新对应分钟;
代码: NightClock/NightClock.qml
6.4 Importing导入JavaScript文件
如果你的程序有很多JS代码, 考虑将它们移到一个单独的文件中; 你可以import这些文件就像importQtQuick模块; 由于JS在QtQuick中的特殊角色, 你必须为这些文件的内容定义namespace; e.g. 例子中的Logic; 你的代码会像这样使用: Logic.Foo(), 而不是直接 Foo(); 导入语句看起来是这样的:
1 2 | import QtQuick 1.1 import "logic.js" as Logic |
Note 如果应用逻辑很复杂, 考虑将它们在C++实现, 然后导入QtQuick: http://qt-project.org/doc/qt-4.8/qml-extending.html
Note that signals with the same name but different parameters cannot be distinguished.
当你导入一个JS文件, 用起来就像库一样, 范围限于导入它的QML文件; 一些情况下, 你需要一个stateless的库或者一组由多个QML文件共享的全局变量; [就像static的, 普通的JS对于每个QML都有一份对象] 你可以使用 .pragma library 声明; http://qt-project.org/doc/qt-4.8/qdeclarativejavascript.html
这里将clock的JS方法搬到logic.js, 导入名为Logic; 还把所有style属性搬到style.js, 导入名Style; 这样相当程度上简化了代码, 而且其他组件也可以共享样式style;
代码: NightClock.qml
更多JS的高级用法
Qt Quick Application Developer Guide for Desktop http://qt-project.org/wiki/Developer-Guides/
---6---
Chapter7 Acquire and Visualize Data 获取以及视觉化数据
这章开始天气预报应用, 主要关注数据处理; 前一个代码中数据都在属性和JS变量中; 对于简小的程序或许足够, 但很快你会需要处理大量的数据;
QtQuick应用了现有的model-view结构, 提供了一组方便的APIs; model用来保存或者获取数据; View元素读取model项. 把每个model根据delegate用特定的方式渲染出来; e.g. 一个grid或一个list;
7.1 Models 模型
QtQuick模型model非常简单, 基于列表list的概念; 用的最多的三种model:
- 一个int值(用来显示多次)
- 一个JavaScript对象的数组array
- 列表list model, e.g. ListModel, XmlListModel元素
看一下Models and Data Handling部分: http://qt-project.org/doc/qt-4.8/qdeclarativeelements.html#models-and-data-handling [Binding],了解model相关列表; 还有一些高级方法在QML Data Models中提到; http://qt-project.org/doc/qt-4.8/qdeclarativemodels.html [in delegate: ListView.view.model]
我们将使用XmlListModel, 还要看几个使用int和array作为model的例子;
我们的天气预报程序使用Google weather API来获取数据; 注意, Google weather API还没有作为常规的互联网服务;
通过这些API, 你可以在网上创建一个请求query, 然后接收XML格式的天气数据的response; 作为一个常规的数据储备, QtQuick提供了一个专门的model: XmlListModel;
XmlListModel使用XPath和XQuery(http://en.wikipedia.org/wiki/XPath)来读取XML数据; 使用XmlRole来创造model items对应被选中的XML树节点;
请求URL类似这样 http://www.google.com/ig/api?weather=[LOCATION]&hl=[LANGUAGE], 返回最新天气情况和后几天的预报; e.g. http://www.google.com/ig/api?weather=Munich&hl=en
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <? xml version = "1.0" ?> < xml_api_reply version = "1" > < weather module_id = "0" tab_id = "0" mobile_row = "0" mobile_zipped = "1" row = "0" section = "0" > < forecast_information > < city data = "Munich, Bavaria" /> < postal_code data = "Munich" /> < latitude_e6 data = "" /> < longitude_e6 data = "" /> < forecast_date data = "2012-02-22" /> < current_date_time data = "1970-01-01 00:00:00 +0000" /> < unit_system data = "US" /> </ forecast_information > < current_conditions > < condition data = "Clear" /> < temp_f data = "39" /> < temp_c data = "4" /> < humidity data = "Humidity: 56%" /> < icon data = "/ig/images/weather/sunny.gif" /> < wind_condition data = "Wind: E at 8 mph" /> </ current_conditions > < forecast_conditions > < day_of_week data = "Fri" /> < low data = "36" /> < high data = "54" /> < icon data = "/ig/images/weather/sunny.gif" /> < condition data = "Clear" /> </ forecast_conditions > < forecast_conditions > < day_of_week data = "Sat" /> < low data = "34" /> < high data = "48" /> < icon data = "/ig/images/weather/chance_of_rain.gif" /> < condition data = "Chance of Rain" /> </ forecast_conditions > </ weather > </ xml_api_reply > |
进行请求和处理的数据的model:
1 2 3 4 5 6 7 8 | XmlListModel { id: weatherModelCurrent source: baseURL + dataURL + location + "&hl=" + language query: "/xml_api_reply/weather/current_conditions" XmlRole { name: "condition" ; query: "condition/@data/string()" } XmlRole { name: "temp_f" ; query: "temp_f/@data/string()" } //... } |
仔细看XmlRole元素, 你会发现它基本上是按照属性-值组合对来创建model的, 通过query开始处定义的节点, 把它们map成特定的XML树的节点; 比如Image, Font, XmlListModel都提供了status和progress属性, 用来跟踪读取的过程, 捕获错误; 另外, 还有一个reload()方法会强行让model请求一次URL和加载数据; 我们会用它来让天气预报保持更新;
7.2 Repeater和View
现在将model中收集的天气数据可视化; QtQuick有很多方法, 可视化的大多数元素是继承自Flickable的: ListView, GridView, PathView;
这些元素作为view port, 使用delegate元素来画出每个model item; View通过height和width设定一个固定的大小; 里面的内容在特定区域画出来, 而且是flicked的(默认可以up/down):
1 2 3 4 5 | ListView { width: 150; height: 50 model: [ "one" , "two" , "three" , "four" , "five" ] // or just a number, e.g 10 delegate: Text { text: "Index: " + model.index + ", Data: " + model.modelData } } |
[model.index/modelData, delegate中使用model的内置属性 - QAbstractItemDelegate]
view的最佳使用是对于一个有巨大数目的model items要被显示出来的时候; view提供内建的scroll或flick功能, 对于大数据集合支持人体工程学表现; 由于performance的原因, view只是部分地加载可见的的item, 而不是整个集合;
使用视图view的好处
view提供了多样的功能, 可以创建漂亮又精巧的UI; http://qt-project.org/wiki/Qt_Quick_Carousel
如果你有小量的model item要被一个接一个地按次序放置, 那么使用Repeater会比较合适; Repeat按model中的item来创建特定元素; 这些元素必须用positioner来放置在屏幕上, e.g. Column, Grid之类; 上面的例子可以改成Repeater:
1 2 3 4 5 6 | Column { Repeater { model: [ "one" , "two" , "three" , "four" , "five" ] // or just a number, e.g 10 Text { text: "Index: " + model.index + ", Data: " + model.modelData } } } |
注意所有的item现在都是可见的, 即便Column的size没有设定; Repeater会计算元素的size, 将Column调整到合适大小; http://qt-project.org/doc/qt-4.8/qml-views.html [ListView-section]
再添加两个可视化的element来完善程序, 每个都有自己的delegate; 我们要把delegate分成最近的天气情况和天气预报, 它们有不同的结构, 用不同方式来展示;
该使用哪种element呢? view和Repeater都可以么? weatherModelForecast可以显示成一个GridView, 可以是多列的; 如果用Repeater看起来就会像一列;
weatherModelCurrent只有一个item, 因此Repeater足够显示;
Weather/weather.qml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | Column { id: allWeather anchors.centerIn: parent anchors.margins: 10 spacing: 10 Repeater { id: currentReportList model: weatherModelCurrent delegate: currentConditionDelegate } /* we can use a GridView...*/ GridView { id: forecastReportList width: 220 height: 220 cellWidth: 110; cellHeight: 110 model: weatherModelForecast delegate: forecastConditionDelegate } /**/ /* ..a Repeater Repeater { id: forecastReportList model: weatherModelForecast delegate: forecastConditionDelegate } */ } |
FolderListModel - Qt4.8可以使用plug-in来实现, Qt5已经加入QML element;
http://qt-project.org/doc/qt-4.8/src-imports-folderlistmodel.html
下一步
下一章把clock和weather forecast放入一个程序中;
---7---
Chapter8 Comoments and Modules 组件和模块
下一步的版本: 将开发的天气预报和时钟应用组合起来; 我们不用再次实现这些特性或者拷贝代码; 只要稍稍改动一下程序, 重用组件即可;
下一章我们会学习添加更多组件来强化程序的功能;
8.1 Creating Components and Collecting Modules 创建组件以及搜集模块
component在QtQuick中的概念很简单: 任何由其他elements组合的item或由自己组合起来的component. component是一些用来创建更大应用程序的建筑块; 使用module的时候, 可以创建component集合(libraries)
为了创建component, 你要先创建一个文件: <NameOfComponent>.qml; 文件中有一个root元素(和普通的QtQuick程序一样); Note 文件名必须首字母大写;
现在开始, 新的名为<NameOfComponent>的component可以在任何其他同一文件夹下的QtQuick程序中使用; 一般来说, 文件有qml后缀的就是QML文件 http://qt-project.org/doc/qt-4.8/qdeclarativedocuments.html
工作中, 你可能会在程序文件夹有许多文件, 甚至要管理不同版本的component; QtQuick module这时可以帮到你; 1) 把所有属于一种功能/模块组的component(基本上就是文件)搬到新的文件夹中; 2) 然后你需要创建一个qmldir 文件包含文件夹中这些component的meta-information; 3) 这样这个文件夹就变成一个模块, 可以import到你的应用中, 和标准QtQuick元素一样:
1 2 | import QtQuick 1.1 import "components" 1.0 |
Define New Components http://qt-project.org/doc/qt-4.8/qmlreusablecomponents.html
如果你把文件夹和模块移动了位置, 必须在QML文件中更新路径; 你可以在全局中的任何程序中使用这些预设的模块; http://qt-project.org/doc/qt-4.8/qdeclarativemodules.html
Note Component也可以作为C++plug-ins, http://qt-project.org/doc/qt-4.8/qml-extending.html
有些情况下, 你必须定义in-line的component, e.g. 在同一个QML文件中, 当你把一个component的引用传递给了一个元素; 这种情况常见在view的delegate;
在import模块或component的时候如果你遇到问题, 设置环境变量: QML_IMPORT_TRACE: http://qt-project.org/doc/qt-4.8/qdeclarativedebugging.html
实践一下:
把NightClock.qml移到一个叫components的文件夹下面, 包含两个component: Weather和WeatherModelItem; 就像前面提到的, 添加一个qmldir文件, 用来描述新的moudle:
components/qmldir
1 2 3 4 | Configure 1.0 Configure.qml NightClock 1.0 NightClock.qml WeatherModelItem 1.0 WeatherModelItem.qml Weather 1.0 Weather.qml |
Weather和WeatherModelItem包含了前面章节的代码;
8.2 Defining Interfaces and Default Behavior 定义接口和默认行为
把代码移到单独的文件中只是创建component的第一步; 你必须定义一个新的component将怎样使用, i.e. 哪些接口用来改变行为和外观; 如果你使用Qt/C++, 应当记住在QtQuick中使用component和在C++中使用class和library不同;
QtQuick和JavaScript差不多; 如果在你的Item中使用一个外部component, 它加载后就好像是被内联定义的一样, 有property, handler, signal等; 你可以把现存的property绑定到另一个值上, 使用已有的signal和handler; 你也可以扩展component, 声明其他的property, 新的signal, handler, JavaScript function; 虽然这些步骤都是可选的, 一个component加载的时候必须有一个默认的外观和行为; e.g.
1 2 3 4 5 6 7 8 | import QtQuick 1.1 import "components" 1.0 Item { id: root NightClock { id: clock } } |
这个和独立执行NightClock的QtQuick的程序一样;
我们尝试再创建个新程序clock-n-weather, 使用3个component: NightClock--数字钟, WeatherModelItem--结合了天气预报与当前天气的model, Weather--绘制天气数据的delegate;
代码稍有改动:
1 2 3 4 5 6 7 8 | NightClock { id: clock height: 80 width: 160 showDate: root.showDate showSeconds: root.showSeconds textColor: Style.onlineClockTextColor } |
showDate和showSeconds属性是配置参数, 在root元素中作为属性的值;
8.3 Handling Scope 处理作用域
WeatherModelItem的作用和之前的component有些不同, 合理的做法是将forecast model和current condition model组合成一个component, 这样我们就可以把它们作为一个天气model使用:
components/WeatherModelItem.qml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | Item { id: root property alias forecastModel: forecast property alias currentModel: current //... XmlListModel { id: forecast source: root.source query: "/xml_api_reply/weather/forecast_conditions" XmlRole { name: "day_of_week" ; query: "day_of_week/@data/string()" } XmlRole { name: "low" ; query: "low/@data/string()" } //... } XmlListModel { id: current source: root.source query: "/xml_api_reply/weather/current_conditions" XmlRole { name: "condition" ; query: "condition/@data/string()" } XmlRole { name: "temp_c" ; query: "temp_c/@data/string()" } onStatusChanged: { // ... } Timer { // note that this interval is not accurate to a second on a full minute // since we omit adjustment on seconds like in the clock interval // to simplify the code interval: root.interval*60000 running: Qt.application.active && !root.forceOffline repeat: true onTriggered: { current.reload() forecast.reload() } } } |
上述代码中, 一个Item中有2个model; 之后我们可以单独访问其中的一个, 在view中显示; WeatherModelItem的id是weatherModelItem, 你可能觉得可以使用weatherModelItem.forecast 和 weatherModelItem.current来分别访问它们; 但是不行;
Note 问题在于一个imported的component的child item默认是不可见的; 一种解决方案是使用alias property来导出id;
1 2 | property alias forecastModel: forecast property alias currentModel: current |
item的Scope和visibility, property和JavaScript object是QtQuick中的重要部分; --QML Scope http://qt-project.org/doc/qt-4.8/qdeclarativescope.html
上面的文章解释了QtQuick的scope机制对名字冲突的解析; 记住这些规则很重要; 日常工作中应当注意对绑定的property的适当描述, 防止side effect, 使得代码容易理解; e.g. 使用这样的代码
1 2 3 4 5 | Item { id: myItem ... enable: otherItem.visible } |
而不要:
1 2 3 4 5 | Item { id: myItem ... enable: visible } |
8.4 Integrated Application 集成整个程序
之前代码中有些改进部分值得注意; 最重要的一个是timer, 触发了两个model的加载:
1 2 3 4 5 6 7 8 9 10 11 12 | Timer { // note that this interval is not accurate to a second on a full minute // since we omit adjustment on seconds like in the clock interval // to simplify the code interval: root.interval*60000 running: Qt.application.active && !root.forceOffline repeat: true onTriggered: { current.reload() forecast.reload() } } |
这个timer和前面更新天气数据的时钟应用类似; root.interval是一个可配置的参数, 定义为一个属性并且绑定了相应的值;
我们也更新了delegate来绘制天气情况; 使用本地的icon来代替网络上的; 这样做有很多好处, 比如节省带宽(如果是移动设备), 或者这个外观更符合预期而且也不用依赖于外部网络情况; KED http://www.kde.org/ 上有不错的icon; 将它们重命名以符合天气情况的描述; 写几句JavaScript就能加载它们:
1 2 3 4 5 6 7 8 9 10 11 12 13 | Image { id: icon anchors.fill: parent smooth: true fillMode: Image.PreserveAspectCrop source: "../content/resources/weather_icons/" + conditionText.toLowerCase().split( ‘ ‘ ).join( ‘_‘ ) + ".png" onStatusChanged: if (status == Image.Error) { // we set the icon to an empty image if we failed to find one source = "" console.log( "no icon found for the weather condition: \"" + conditionText + "\"" ) } } |
注意Weather组件可以独立运行; 它使用了默认属性值; 这样做有利于在各种情况下进行测试;
主要的Item使用了各种component: clock-n-weather/ClockAndWeather.qml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | import "../components" 1.0 import "../js/logic.js" as Logic import "../js/style.js" as Style Rectangle { id: root //... Image { id: background source: "../content/resources/background.png" fillMode: "Tile" anchors.fill: parent onStatusChanged: if (background.status == Image.Error) console.log( "Background image \"" + source + "\" cannot be loaded" ) } WeatherModelItem { id: weatherModelItem location: root.defaultLocation interval: root.defaultInterval } Component { id: weatherCurrentDelegate Weather { id: currentWeatherItem labelText: root.defaultLocation conditionText: model.condition tempText: model.temp_c + "C°" } } Component { id: weatherForecastDelegate Weather { id: forecastWeatherItem labelText: model.day_of_week conditionText: model.condition tempText: Logic.f2C (model.high) + "C° / " + Logic.f2C (model.low) + "C°" } } Column { id: clockAndWeatherScreen anchors.centerIn: root NightClock { //... } Repeater { id: currentWeatherView model: weatherModelItem.currentModel delegate: weatherCurrentDelegate } GridView { id: forecastWeatherView width: 300 height: 300 cellWidth: 150; cellHeight: 150 model: weatherModelItem.forecastModel delegate: weatherForecastDelegate } } //... } |
WeatherModelItem作为weatherModelItem加载进来, 随后定义了了2个基于Weather的delegate; Column放置NightClock, Repeater放置当前天气, GridView放置了预报;
8.5 Further Readings 扩展阅读
UI elements on Symbian http://doc.qt.digia.com/qtquick-components-symbian-1.1/index.html
开发的UI Component http://qt-project.org/wiki/Qt_Quick_Components
桌面: https://qt.gitorious.org/qt-components/desktop
下一步 集中在用户交互上;
---8---
--YCR---