模型视图是一种数据和显示相分离的技术。QtQuick提供了一系列预定义的模型和视图。
QtQuick中将数据从表现层分离的最基本方法是使用Repeater元素。Repeater元素可以用于显示一个数组的数据,并且可以很方便地在用户界面进行定位。Repeater的模型范围从一个整型到网络数据,均可作为其数据模型。
Repeater最简单的用法是将一个整数作为其model属性的值,整数代表Repeater所使用的模型中的数据个数。例如下面的代码中,model: 10代表Repeater的模型有10个数据项。
import QtQuick 2.6
Column
{
spacing: 2
Repeater
{
model: 10
Rectangle
{
width: 100
height: 20
radius: 3
color: "blue"
Text
{
anchors.centerIn: parent
text: index
}
}
}
}
设置了 10 个数据项,然后定义一个Rectangle进行显示。每一个Rectangle的宽度和高度分别为 100px 和 20px,并且有圆角和浅蓝色背景。Rectangle中有一个Text元素为其子元素,Text文本值为当前项的索引。
import QtQuick 2.6
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
color: surfaceColor
}
}
}
}
ListElement的每个属性都被Repeater绑定到实例化的显示项。每一个用于显示数据的Rectangle作用域内都可以访问到ListElement的name和surfaceColor属性。
Repeater适用于少量的静态数据集。
Repeater适用于少量的静态数据集,但实际应用中,数据模型是非常复杂的,并且数量巨大。因此,QtQuick提供了两个专门的视图元素:ListView和GridView。ListView和GridView都继承自Flickable,允许用户在一个很大的数据集中进行移动。同时,ListView和GridView能够复用创建的代理,ListView和GridView不需要为每一个数据创建一个单独的代理,减少了大量代理的创建造成的内存问题。
ListView使用模型提供数据,创建代理渲染数据。
如果数据模型包含的数据不能在一屏显示完全,ListView只会显示整个列表的一部分。但作为 QtQuick 的一种默认行为,ListView并不能限制显示范围就在代理显示的区域内,代理可能会在ListView的外部显示出来。设置clip属性为true,可以使超出ListView边界的代理能够被裁减掉。
import QtQuick 2.6
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
}
}
}
}
ListView是一个可滚动的区域。ListView支持平滑滚动,能够快速流畅地进行滚动。默认情况下,滚动具有在向下到达底部时会有一个反弹的特效,由boundsBehavior属性控制。boundsBehavior属性有三个可选值:Flickable.StopAtBounds完全消除反弹效果;Flickable.DragOverBounds在自由滑动时没有反弹效果,允许用户拖动越界;Flickable.DragAndOvershootBounds则是默认值,不仅用户可以拖动越界,还可以通过自由滑动越界。
当列表滑动结束时,列表可能停在任意位置:一个代理可能只显示一部分,另外部分被裁减掉,由snapMode属性控制的。snapMode属性的默认值是ListView.NoSnap,可以停在任意位置;ListView.SnapToItem会在某一代理的顶部停止滑动;ListView.SnapOneItem则规定每次滑动时不得超过一个代理,每次只滑动一个代理,在分页滚动时尤其有效。
默认情况下,列表视图是纵向的。通过orientation属性可以将其改为横向。属性可接受值为ListView.Vertical或ListView.Horizontal。当列表视图横向排列时,默认元素按照从左向右的顺序布局,使用layoutDirection属性可以修改,属性的可选值为Qt.LeftToRight或Qt.RightToLeft。
在触摸屏环境下使用ListView,默认设置已经足够。但如果在带有键盘的环境下,使用方向键一般应该突出显示当前项。视图也支持使用一个专门用于高亮的代理,只会被实例化一次,并且只会移动到当前项目的位置。
import QtQuick 2.6
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
{
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
}
}
}
}
focus属性设置为true,允许ListView接收键盘焦点。highlight属性设置为一个被使用的高亮代理。当键盘上下键按下时,焦点将会移动,高亮的项也会移动。默认情况下,高亮的移动是由视图负责的。移动速度和大小都是可控的,属性有highlightMoveSpeed,highlightMoveDuration,highlightResizeSpeed以及highlightResizeDuration。速度默认为每秒 400 像素;持续时间被设置为 -1,意味着持续时间由速度和距离控制。同时设置速度和持续时间则由系统选择二者中较快的那个值。有关高亮更详细的设置则可以通过将highlightFollowCurrentItem属性设置为false达到。这表示视图将不再负责高亮的移动,完全交给开发者处理。下面的例子中,高亮代理的y属性被绑定到ListView.view.currentItem.y附加属性。这保证了高亮能够跟随当前项目。但是,我们不希望视图移动高亮,而是由自己完全控制,因此在y属性上面应用了一个Behavior。下面的代码将这个移动的过程分成三步:淡出、移动、淡入。注意,SequentialAnimation和PropertyAnimation可以结合NumberAnimation实现更复杂的移动。有关动画部分,将在后面的章节详细介绍,这里只是先演示这一效果。
ListView的header和footer实际会添加在第一个元素之前和最后一个元素之后。header和footer通常用于显示额外的元素,例如在最底部显示“加载更多”的按钮。
import QtQuick 2.6
Rectangle
{
width: 240
height: 300
color: "white"
ListView
{
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
delegate: numberDelegate
spacing: 5
header:headerComponent
footer:footerComponent
}
Component
{
id: headerComponent
Rectangle
{
width: 40
height: 20
color: "yellow"
Text{text:"up"}
}
}
Component
{
id: footerComponent
Rectangle
{
width: 40
height: 20
color: "red"
Text{text:"down"}
}
}
Component
{
id: numberDelegate
Item
{
width: 40
height: 40
Text
{
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
GridView用于显示二维表格。表格的元素不依赖于代理的大小和代理之间的间隔,而是由cellWidth和cellHeight属性控制一个单元格。每一个代理都会被放置在单元格的左上角。GridView支持不同的显示方向,使用flow属性控制,可选值为GridView.LeftToRight和GridView.TopToBottom。前者按照先从左向右、再从上到下的顺序填充,滚动条出现在竖直方向;后者按照先从上到下、再从左到右的顺序填充,滚动条出现在水平方向。
在自定义用户界面中,代理扮演着重要的角色。模型中的每一个数据项都要通过一个代理向用户展示,用户看到的可视部分就是代理。
每一个代理都可以访问一系列属性和附加属性。这些属性及附加属性中,有些来自于数据模型,有些则来自于视图。前者为代理提供了每一个数据项的数据信息;后者则是有关视图的状态信息。
代理中最常用到的是来自于视图的附加属性ListView.isCurrentItem和ListView.view。 前者是一个布尔值,用于表示代理所代表的数据项是不是视图所展示的当前数据项;后者则是一个只读属性,表示代理所属于的视图。通过访问视图的相关数据,可以创建通用的可复用的代理,用于适配视图的大小和展示特性。
import QtQuick 2.0
Rectangle
{
width: 120
height: 300
gradient: Gradient
{
GradientStop { position: 0.0; color: "#f6f6f6" }
GradientStop { position: 1.0; color: "#d7d7d7" }
}
ListView
{
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
delegate: numberDelegate
spacing: 5
focus: true
}
Component
{
id: numberDelegate
Rectangle
{
width: ListView.view.width
height: 40
color: ListView.isCurrentItem?"#157efb":"#53d769"
border.color: Qt.lighter(color, 1.1)
Text
{
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
本例展示了每一个代理的宽度都绑定到视图的宽度,而代理的背景色则根据附加属性ListView.isCurrentItem的不同而有所不同。
如果模型的每一个数据项都关联一个动作,如响应对数据项的点击操作,模型中数据项关联动作应该是每一个代理的一部分。这会将事件管理从视图分离出来。视图主要处理的是各个子视图之间的导航、切换,而代理则是对一个特定的数据项的事件进行处理。完成这一功能最常用的方法是,为每一个视图创建一个MouseArea,然后响应其onClicked信号。
一个视图中的数据项并不是固定不变的,需要动态地增加、移除。视图中数据项的增加、移除其实是对底层模型的修改的反应。添加动画效果可以让用户清晰地明白哪些数据发生了改变。
QML为每个代理提供了两个信号,onAdd和onRemove,用于增加和删除数据项。
import QtQuick 2.0
Rectangle
{
width: 480
height: 300
gradient: Gradient
{
GradientStop { position: 0.0; color: "#dbddde" }
GradientStop { position: 1.0; color: "#5fc9f8" }
}
ListModel
{
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: "#53d769"
border.color: Qt.lighter(color, 1.1)
Text
{
anchors.centerIn: parent
text: "Add item!"
}
MouseArea
{
anchors.fill: parent
onClicked:
{
theModel.append({"number": ++parent.count});
}
}
property int count: 9
}
GridView
{
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
gradient: Gradient
{
GradientStop { position: 0.0; color: "#f8306a" }
GradientStop { position: 1.0; color: "#fb5b40" }
}
Text
{
anchors.centerIn: parent
font.pixelSize: 10
text: number
}
MouseArea
{
anchors.fill: parent
onClicked:
{
if (!wrapper.GridView.delayRemove)
theModel.remove(index);
}
}
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 }
}
}
}
}
本例演示了为动态修改ListModel增加动画效果。有一个用于新增数据项的按钮,点击按钮,会通过调用append函数向模型增加一个数据项,会将触发视图创建一个新的代理,并且发出GridView.onAdd信号。GridView.onAdd信号关联了一个SequentialAnimation类型的动画,利用scale属性的变化,将代理缩放到视图。当视图中的一个数据项被点击时,会通过调用视图的remove函数被移除,同时会发出GridView.onRemove信号,触发另一个SequentialAnimation类型的动画。在添加代理时,代理必须在动画开始之前就被创建,删除代理时,代理需要在动画结束后才能被销毁。使用PropertyAction元素,在动画开始之前将GridView.delayRemove属性设置为true,动画完成之后,再将其设置为false,保证在代理被销毁前动画能够顺利完成。
在表现列表时,常常会有一种机制:当数据项被选中时,数选中据项项会变大以充满屏幕。这种行为可以将被激活的数据项放置在屏幕中央,或者为用户显示更详细的信息。
本例中,ListView的每一个数据项在点击时都会充满整个列表视图,多出来的额外空间用于显示更多信息。可以使用状态实现这种机制。在这个过程中,列表的很多属性都会发生改变。
首先,wrapper的高度会被设置为ListView的高度;缩略图会变大,从先前的位置移动到一个更大的位置。除此以外,两个隐藏的组件,factsView和closeButton会显示在恰当的位置。最后,ListView的contentsY属性会被重新设置为代理的y值。contentsY属性其实就是视图的可见部分的顶部距离。视图的interactive属性会被设置为false,这可以避免用户通过拖动滚动条使视图移动。当数据项第一次被点击时,它会进入expanded状态,是其代理充满整个ListView,并且重新布局内容。当点击关闭按钮时,expanded状态被清除,代理重新回到原始的状态,ListView的交互也重新被允许。
PathView是QtQuick中最强大、最复杂的视图。PathView允许创建一种更灵活的视图,视图中的数据项并不是方方正正,而是可以沿着任意路径布局,沿着同一布局路径,数据项的属性可以被更详细的设置,例如缩放、透明度等。
使用PathView首先需要定义一个代理和一个路径。此外,PathView还可以设置很多其它属性,其中最普遍的是pathItemCount,用于设置可视数据项的数目;preferredHighlightBegin、preferredHighlightEnd和highlightRangeMode可以设置高亮的范围,也就是沿着路径上的当前可以被显示的数据项。
在深入了解高亮范围前,必须首先了解path属性。path接受一个Path元素,用于定义PathView中的代理所需要的路径。路径使用startX和startY属性,结合PathLine、PathQuad、PathCubic等路径元素进行定义。
一旦路径定义完成,可以使用PathPercent和PathAttribute元素进行调整。这些元素用于两个路径元素之间,更好的控制路径和路径上面的代理。PathPercent控制两个元素之间的路径部分有多大。它控制了路径上面代理的分布,这些代理按照其定义的百分比进行分布。
PathAttribute元素同PathPercent同样放置在元素之间。该元素允许沿路径插入一些属性值。这些属性值附加到代理上面,可用于任何能够使用的属性。
下面的例子演示了如何利用PathView实现卡片的弹入。这里使用了一些技巧来达到这一目的。它的路径包含三个PathLine元素。通过PathPercent元素,中间的元素可以正好位于中央位置,并且能够留有充足的空间,以避免被别的元素遮挡。元素的旋转、大小缩放和 Z 轴都是由PathAttribute进行控制。除了定义路径,我们还设置了PathView的pathItemCount属性。该属性用于指定路径所期望的元素个数。最后,代理中的PathView.onPath使用preferredHighlightBegin和preferredHighlightEnd属性控制代理的可见性。
XML是常见的数据格式,QML提供了XmlListModel元素支持将XML数据转换为模型。XmlListModel可以加载本地或远程的XML文档,使用XPath表达式处理数据。
Background.qml文件:
import QtQuick 2.0
Rectangle
{
width: 320
height: 320
gradient: Gradient
{
GradientStop { position: 0.0; color: "#f6f6f6" }
GradientStop { position: 1.0; color: "#d7d7d7" }
}
}
Box.qml文件:
import QtQuick 2.0
Rectangle
{
id: root
width: 64
height: 64
color: "#ffffff"
border.color: Qt.darker(color, 1.2)
property alias text: label.text
property color fontColor: '#1f1f1f'
Text
{
id: label
anchors.centerIn: parent
font.pixelSize: 14
color: root.fontColor
}
}
Main.qml文件:
当XML数据被下载下来就被处理成模型的数据项和角色。query属性是 XPath表达式语言,用于创建模型数据项,本例中属性值为/rss/channel/item,rss 标签下的每一个channel标签中的每一个item标签,都会被生成一个数据项。每一个数据项都可以定义一系列角色,角色使用XmlRole表示。每一个角色都有一个名字,代理可以使用附件属性访问到其值。角色的值使用XPath表达式获取的。例如,title属性的值由title/string()表达式决定,返回的是<title>和</title>标签之间的文本。imageSource属性值并不是直接由XML获取的字符串,而是一系列函数的运算结果。在返回的XML中,有些item中包含图片,使用<img src=标签表示。使用substring-after和substring-beforeXPath 函数,可以找到每张图片的地址并返回。因此,imageSource属性可以直接作为Image元素的source属性值。
为了使用分组,需要设置section.property和section.criteria两个属性。section.property定义了使用哪个属性进行分组。在分组前,需要确保模型已经排好序,以便每一部分能够包含连续的元素,否则,同一属性的名字可能出现在多个位置。section.criteria的可选值为ViewSection.FullString或ViewSection.FirstCharacter。前者为默认值,适用于具有明显分组的模型,例如,音乐集等;后者按照属性首字母分组,并且意味着所有属性都适用,常见例子是电话本的通讯录名单。
一旦分组定义完毕,在每一个数据项就可以使用附加属性ListView.section、ListView.previousSection和ListView.nextSection访问到分组。使用这个属性,我们就可以找到一个分组的第一个和最后一个元素,从而实现某些特殊功能。
我们也可以给ListView的section.delegate属性赋值,以便自定义分组显示的代理。这会在一个组的数据项之前插入一个用于显示分组的代理。这个代理可以使用附加属性访问当前分组的名字。
按照国别对一组人进行分组。国别被设置为section.property属性的值。section.delegate组件,也就是sectionDelegate,用于显示每组的名字,也就是国家名。每组中的人名则使用spaceManDelegate显示。
import QtQuick 2.0
import QtQuick.XmlListModel 2.0
Background
{
width: 300
height: 480
Component
{
id: imageDelegate
Box
{
width: listView.width
height: 220
color: '#333'
Column
{
Text {text: title;color: '#e0e0e0'}
Image
{
width: listView.width
height: 200
fillMode: Image.PreserveAspectCrop
source: imageSource
}
}
}
}
XmlListModel
{
id: imageModel
source: "http://www.padmag.cn/feed"
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
}
}
模型视图的性能很大程度上取决于创建新的代理所造成的消耗。例如,如果clip属性设置为false,当向下滚动ListView时,系统会在列表末尾创建新的代理,并且将列表上部已经不可显示的代理移除。初始化代理需要消耗大量时间时,用户在快速拖动滚动条时,初始化代理和删除不可显示代理会造成一定程度的影响。
为了避免创建代理造成的消耗,可以调整被滚动视图的外边框的值。通过修改cacheBuffer属性即可达到这一目的。在上面所述的有关竖直滚动的例子中,这个属性会影响到列表上方和下方会有多少像素。这些像素则影响到是否能够容纳这些代理。例如,将异步加载图片与此结合,就可以实现在图片真正加载完毕之后才显示出来。
更多的代理意味着更多的内存消耗,从而影响到用户的操作流畅度,同时也有关代理初始化的时间。对于复杂的代理,通过修改cacheBuffer属性并不能从根本上解决问题。代理初始化一次,代理内容就会被重新计算,会消耗时间,如果消耗时间很长,会显著降低用户体验。代理中子元素的个数同样也有影响,因为移动更多的元素当然要更多的时间。为了解决代理的消耗问题,推荐使用Loader元素。Loader元素允许延时加载额外的元素。例如,一个可展开的代理,只有当用户点击时,才会显示详细信息,包含一个很大的图片。利用Loader元素,可以做到只有其被显示时才进行加载,否则不加载。同样,每个代理中包含的JavaScript代码应该尽可能少,最好能做到在代理之外调用复杂的JavaScript代码。这会减少代理创建时编译JavaScript所消耗的时间。