# 高级技术
# 路径视图(PathView)
PathView
元素是Qt Quick中提供的最灵活的视图,但也是最复杂的视图。它可以创建一个沿着任意路径布置项目的视图。沿着相同的路径,可以详细控制缩放、不透明度等属性。
使用PathView
时,必须定义一个代理和一个路径。除此之外,PathView
可以通过一系列属性定义自身,最常见的是:pathItemCount
,控制同时可见项目的数量;以及高亮范围控制属性preferredHighlightBegin
、preferredHighlightEnd
和highlightRangeMode
,控制当前项沿路径显示的位置。
在深入了解高亮范围控件属性之前,必须了解path
属性。 path
属性需要一个Path
元素来定义代理在PathView
上滚动所遵循的路径。路径使用startX
和startY
属性、路径元素(例如 PathLine
、PathQuad
和 PathCubic
)结合定义,这些元素连接在一起形成了二维路径。
在定义路径后,可以使用PathPercent
和PathAttribute
元素进一步调整它,这些元素放在路径元素之间,并提供对路径及其上的代表项的更细粒度的控制。 PathPercent
控制每个元素之间覆盖的路径部分有多大,这反过来又控制了代表项在路径上的分布,因为它们的分布与进度百分比成正比。
PathView
的preferredHighlightBegin
和preferredHighlightEnd
属性是进入高亮的位置,它们都被期望是在0到1范围内的实数值,终点需要大于等于起点。设置这两个属性,例如都设置成0.5,当前项将显示在沿路径百分之五十的位置。译者笔记:这是设置当前项默认的位置,如果都设置成0.3,则最开始当前项将会在30%这个位置,当前项跟显示的大小无关,可以通过判断当前项变化颜色找到当前项的位置。如果将highlightRangeMode
设置为PathView.StrictlyEnforceRange
,会发现切换当前项目时候,总会显示在30%。对于起点和终点的设置,应该是一个百分比的范围,设置的较宽时候,可能存在多个进入这个范围的情况,通过测试是起点位置优先。
在Path
中,如同PathPercent
元素一样,PathAttribute
元素被放置在元素之间,它们允许指定沿路径插入的属性值。这些属性附加到代理,可用于控制任何可能的属性。
下面的示例演示了如何使用PathView
元素来创建用户可以翻阅的卡片的视图,它采用了许多技巧来做到这一点。 路径由三个PathLine
元素组成;使用PathPercent
元素,可以使中心元素正确居中并提供足够的空间不会被其他元素弄乱;使用PathAttribute
元素,可以控制旋转、大小和z
值。
除path
外,还设置了PathView
的pathItemCount
属性,这控制了路径上的呈现密度。 preferredHighlightBegin
和preferredHighlightEnd
、PathView.onPath
用于控制代理的可见性。译者笔记:PathView.onPath
是布尔类型的值,提现的是这个元素是否在路径上,可以使用visible: PathView.onPath
控制可见性。
PathView {
anchors.fill: parent
model: 100
delegate: flipCardDelegate
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
}
如下所示,代理利用了来自PathAttribute
元素的itemZ
、itemAngle
和itemScale
附加属性。 值得注意的是,只能在wrapper
中获得代理的附加属性, 因此,如果想在Rotation
元素中要访问相关附加属性值的话,需要定义wrapper
的rotX
属性做一个中转桥梁。
PathView
的另一个特定值得注意的细节是:PathView.onPath
附加属性的使用。 将可见性绑定到this
是常见的做法,这允许PathView
为缓存的目的保留不可见的元素。 这种目的通常不能通过剪切来处理,因为PathView
的代理项目的摆放比ListView
或GridView
视图更自由。
Component {
id: flipCardDelegate
BlueBox {
id: wrapper
required property int index
property real rotX: PathView.itemAngle
visible: PathView.onPath
width: 64
height: 64
scale: PathView.itemScale
z: PathView.itemZ
antialiasing: true
gradient: Gradient {
GradientStop { position: 0.0; color: "#2ed5fa" }
GradientStop { position: 1.0; color: "#2467ec" }
}
transform: Rotation {
axis { x: 1; y: 0; z: 0 }
angle: wrapper.rotX
origin { x: 32; y: 32; }
}
text: wrapper.index
}
}
在PathView
中,转换图像或其他复杂元素时,常用的性能优化技巧是将Image
元素的smooth
属性绑定到附加属性PathView.view.moving
上。这意味着图像在移动时忽略一些细节,但在静止时会平滑转换。当视图处于运动状态时,在平滑缩放上花费处理能力是没有意义的,因为用户无论如何都看不到这些效果。
提示
鉴于PathAttribute
的动态特性,qml工具(在本例中是qmlint
)不了解itemZ
、itemAngle
或itemScale
属性。译者笔记:qmlint是Qt附带的一个工具,用于验证QML文件的语法有效性。更多信息请参考Qt Quick tool qmllint (opens new window)
当使用PathView
并以编程方式更改其currentIndex
时,可能希望控制路径移动的方向,可以使用movementDirection
属性来完成操作。它可以设置为PathView.Shortest
,这是默认值,这意味着可以在任一方向移动,它具体取决于哪种方式最接近目标值;也可以通过将movementDirection
设置为PathView.Negative
或PathView.Positive
来限制方向。
# 表模型
到目前为止,讨论过的所有视图都以一种或另一种方式呈现一个系列的项目,甚至就连GridView
也是期望模型里面提供的是一个一维的项目列表。 对于二维的数据表,需要使用TableView
元素。
与其他视图类似,TableView
组合了一个model
和一个delegate
来形成一个网格。 如果给定一个面向列的模型,它会显示一列,变得与ListView
元素非常相似。 但是,它还可以显示明确定义了列和行的二维模型。
在下面的示例中,使用从C++暴露的自定义模型来设置了一个简单的TableView
。 目前,无法直接从QML里面创建面向表的模型,在“Qt与C++”一章中解释了这个概念。 运行示例的结果如下图所示:
在下面的示例中,创建了一个TableView
,并设置了rowSpacing
和columnSpacing
来控制代理之间的水平间隙和垂直间隙。 其余属性的设置与其他类型的视图一样。
TableView {
id: view
anchors.fill: parent
anchors.margins: 20
rowSpacing: 5
columnSpacing: 5
clip: true
model: tableModel
delegate: cellDelegate
}
代理本身可以通过implicitWidth
和implicitHeight
携带隐式大小。 在下面的示例中就将这样做的,这个例子中,实际展示数据内容从模型的display
角色中获取的。
Component {
id: cellDelegate
GreenBox {
id: wrapper
required property string display
implicitHeight: 40
implicitWidth: 40
Text {
anchors.centerIn: parent
text: wrapper.display
}
}
}
可以根据模型内容提供不同大小的代理,例如:
GreenBox {
implicitHeight: (1 + row) * 10
// ...
}
请注意,宽度和高度都必须大于零。
当代理提供隐式大小时,每行最高的代理和每列最宽的代理控制大小。 如果项目的宽度取决于行,或者高度取决于列,这会产生有趣的行为。 这是因为并非所有代理都始终被实例化,因此当用户滚动表格时,列的宽度可能会发生变化。译者笔记:这是由于在下面的代理还没有显示出来过,当显示出来后,如果比原来的宽,则所有的根据最宽的确定当前表格的列宽,那么总体表格的列宽会越往下滑动越宽(如果越往下隐式宽度越宽的话)。
为了避免使用隐式代理尺寸指定列宽和行高的问题,可以提供计算这些尺寸的函数,这通过使用columnWidthProvider
和rowHeightProvider
完成控制的。如下所示,这些函数分别返回列宽和行高:
TableView {
columnWidthProvider: function (column) { return 10 * (column + 1) }
// ...
}
如果需要动态更改列宽或行高,必须通过调用forceLayout
方法通知视图。 这将使视图重新计算所有单元格的大小和位置。
# 来自XML的模型
由于XML是一种普遍存在的数据格式,QML提供了XmlListModel
元素,该元素将XML数据暴露成为模型。 该元素可以在本地或远程获取XML数据,然后使用XPath表达式处理数据。
下面的示例演示了从RSS流中获取图像。 source
属性通过HTTP指定远程的位置,数据会自动下载。
数据被下载后,将其处理为模型项和角色。 XmlListModel
的query
属性是一个XPath,表示用于创建模型项的基本查询。在这个例子中,路径是/rss/channel/item
,所以对于每个rss标签、每个channel标签、每个item标签,都会创建一个模型项目。
对于每个模型项,提取了许多角色,由XmlListModelRole
元素表示。每个角色都有一个名称,代理可以通过附加属性访问该名称。每个这种属性的实际值是通过每个角色的elementName
和可选的attributeName
属性确定的。例如,title
属性对应于XML元素title
,返回<title>
和</title>
标签之间的内容。
imageSource
属性提取标签的属性值而非标签的内容。在这种情况下,enclosure
标签的url
属性被提取成字符串。然后可以将imageSource
属性直接用作Image
元素的source
,该元素从给定的URL加载图像。
import QtQuick
import QtQml.XmlListModel
import "../common"
Background {
width: 300
height: 480
Component {
id: imageDelegate
Box {
id: wrapper
required property string title
required property string imageSource
width: listView.width
height: 220
color: '#333'
Column {
Text {
text: wrapper.title
color: '#e0e0e0'
}
Image {
width: listView.width
height: 200
fillMode: Image.PreserveAspectCrop
source: wrapper.imageSource
}
}
}
}
XmlListModel {
id: imageModel
source: "https://www.nasa.gov/rss/dyn/image_of_the_day.rss"
query: "/rss/channel/item"
XmlListModelRole { name: "title"; elementName: "title" }
XmlListModelRole { name: "imageSource"; elementName: "enclosure"; attributeName: "url"; }
}
ListView {
id: listView
anchors.fill: parent
model: imageModel
delegate: imageDelegate
}
}
# 分段列表
有时,列表中的数据可以分为多段, 它如同按字母划分联系人列表、按专辑划分音乐曲目一样简单。 使用ListView
可以将平铺的列表划分成多个类别,从而提供更深入的体验。
为了使用分段,必须设置section.property
和section.criteria
。 section.property
定义了使用哪个属性将内容划分段的依据。在这里,重要的是要知道必须对模型进行排序,以便每个段都由连续元素组成,否则,相同的属性名称可能会出现在多个位置。
section.criteria
可以设置为ViewSection.FullString
或ViewSection.FirstCharacter
。ViewSection.FullString
是默认值,可用于具有清晰分段的模型,例如音乐专辑曲目。ViewSection.FirstCharacter
依据属性的第一个字符分段,意味着任何属性都可以用于分段,最常见的例子是电话簿中联系人的姓氏。
定义了分段后,可以使用附加属性ListView.section
、ListView.previousSection
和ListView.nextSection
从每个项上访问它们。使用这些属性,可以检测一段的第一个和最后一个项目,并采取相应的行动。
也可以将段代理组件分配给ListView
的section.delegate
属性。这将创建一个段标题代理,该代理在本段所有项目之前插入。代理组件可以使用附加属性section
访问当前段的名称。
下面的示例通过显示按国籍划分的太空人列表来演示分段概念。 将nation
用作section.property
;section.delegate
组件sectionDelegate
显示每个国家的标题,显示国家名称。在每段中,太空人的名字都使用spaceManDelegate
组件显示。
import QtQuick
import "../common"
Background {
width: 300
height: 290
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: spaceMen
delegate: spaceManDelegate
section.property: "nation"
section.delegate: sectionDelegate
}
Component {
id: spaceManDelegate
Item {
id: spaceManWrapper
required property string name
width: ListView.view.width
height: 20
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
font.pixelSize: 12
text: spaceManWrapper.name
color: '#1f1f1f'
}
}
}
Component {
id: sectionDelegate
BlueBox {
id: sectionWrapper
required property string section
width: ListView.view ? ListView.view.width : 0
height: 20
text: sectionWrapper.section
fontColor: '#e0e0e0'
}
}
ListModel {
id: spaceMen
ListElement { name: "Abdul Ahad Mohmand"; nation: "Afganistan"; }
ListElement { name: "Marcos Pontes"; nation: "Brazil"; }
ListElement { name: "Alexandar Panayotov Alexandrov"; nation: "Bulgaria"; }
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"; }
}
}
# 对象模型(ObjectModel)
在某些情况下,可能希望将列表视图用于大量不同的项目。可以使用动态QML和Loader
来解决这个问题,但还有另一种选择是使用来自QtQml.Models
模块的ObjectModel
。对象模型与其他模型不同,因为它允许将实际的视觉元素放在模型里面。这样,视图就不需要任何delegate
。
在下面的示例中,将三个Rectangle
元素放入到ObjectModel
中。然而,一个矩形包含一个Text
子元素,同时最后一个矩形带有圆角。这将导致使用类似ListModel
的表格样式模型,这也会导致模型中出现空的Text
元素。
import QtQuick
import QtQml.Models
Rectangle {
width: 320
height: 320
gradient: Gradient {
GradientStop { position: 0.0; color: "#f6f6f6" }
GradientStop { position: 1.0; color: "#d7d7d7" }
}
ObjectModel {
id: itemModel
Rectangle { height: 60; width: 80; color: "#157efb" }
Rectangle { height: 20; width: 300; color: "#53d769"
Text { anchors.centerIn: parent; color: "black"; text: "Hello QML" }
}
Rectangle { height: 40; width: 40; radius: 10; color: "#fc1a1c" }
}
ListView {
anchors.fill: parent
anchors.margins: 10
spacing: 5
model: itemModel
}
}
ObjectModel
的另一个方面是,可以使用get
、insert
、move
、remove
和clear
方法动态填充。通过这种方式,模型的内容可以从各种源动态生成,并且仍然可以轻松地显示在单个视图中。
# 带有动作的模型
ListElement
类型支持将Javascript函数绑定到属性,这意味着可以将函数放入模型中。 这在使用动作和类似结构构建菜单时非常有用。
下面的示例通过使用城市模型来演示这一点,这些城市使用不同的欢迎语。 actionModel
是四个城市的模型,将hello
属性绑定到函数,每个函数都有一个参数value
,也有任意数量的参数。
在代理actionDelegate
中,MouseArea
将函数hello
当成普通函数调用,将导致调用到模型中相应的hello
属性。
import QtQuick
Rectangle {
width: 120
height: 300
gradient: Gradient {
GradientStop { position: 0.0; color: "#f6f6f6" }
GradientStop { position: 1.0; color: "#d7d7d7" }
}
ListModel {
id: actionModel
ListElement {
name: "Copenhagen"
hello: function(value) { console.log(value + ": You clicked Copenhagen!"); }
}
ListElement {
name: "Helsinki"
hello: function(value) { console.log(value + ": Helsinki here!"); }
}
ListElement {
name: "Oslo"
hello: function(value) { console.log(value + ": Hei Hei fra Oslo!"); }
}
ListElement {
name: "Stockholm"
hello: function(value) { console.log(value + ": Stockholm calling!"); }
}
}
ListView {
anchors.fill: parent
anchors.margins: 20
focus: true
model: actionModel
delegate: Rectangle {
id: delegate
required property int index
required property string name
required property var hello
width: ListView.view.width
height: 40
color: "#157efb"
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: delegate.name
}
MouseArea {
anchors.fill: parent
onClicked: delegate.hello(delegate.index)
}
}
spacing: 5
clip: true
}
}
# 调优性能
模型视图的可感知性能在很大程度上取决于准备新代理所需的时间。例如,当向下滚动ListView时,代理将从底部添加到视图的外部,并在离开视图顶部时移除。如果将clip
属性设置为false
,这一点就会变得很明显。如果代理初始化花费的时间太长,一旦视图滚动过快时,用户就会发现添加和移除代理这一点。
要解决此问题,可以调整滚动视图的边距(以像素为单位)。这是使用cacheBuffer
属性完成的。在上述情况下,垂直滚动,它将控制在ListView的上方和下方能包含准备好的代理的像素数。例如,将此与异步加载的Image
元素相结合,可以在图像出现之前让它们有时间加载。
拥有更多的代理会牺牲内存为代价以获得更流畅的体验,并会用更多的时间初始化每个代理。这并不能解决复杂代理的问题。每次实例化代理时,都会对其内容进行计算和编译,这需要时间,如果需要的时间太多,将导致较差的滚动体验。代理中有许多元素也会降低滚动性能,它会简单地花费一个周期来移动许多元素。
为了解决后两个问题,建议使用Loader
元素,它们可以在需要时实例化其他元素。例如,展开代理可以使用Loader
将其详细视图的实例化推迟到需要展开时。出于同样的原因,最好将每个代理中的JavaScript数量保持在最低限度。最好让它们调用驻留在每个代理之外的复杂JavaScript片段,这减少了每次创建代理时编译JavaScript所花费的时间。
提示
请注意,使用Loader
来推迟初始化正是这样做的——它会推迟性能问题。这意味着滚动性能将得到改善,但实际内容仍需要时间才能显示。