首页 > 代码库 > 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都提供了statusprogress属性, 用来跟踪读取的过程, 捕获错误; 另外, 还有一个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---