# 高级技术

# 路径视图(PathView)

PathView元素是Qt Quick中提供的最灵活的视图,但也是最复杂的视图。它可以创建一个沿着任意路径布置项目的视图。沿着相同的路径,可以详细控制缩放、不透明度等属性。

使用PathView时,必须定义一个代理和一个路径。除此之外,PathView可以通过一系列属性定义自身,最常见的是:pathItemCount,控制同时可见项目的数量;以及高亮范围控制属性preferredHighlightBeginpreferredHighlightEndhighlightRangeMode,控制当前项沿路径显示的位置。

在深入了解高亮范围控件属性之前,必须了解path 属性。 path属性需要一个Path元素来定义代理在PathView上滚动所遵循的路径。路径使用startXstartY属性、路径元素(例如 PathLinePathQuadPathCubic)结合定义,这些元素连接在一起形成了二维路径。

在定义路径后,可以使用PathPercentPathAttribute元素进一步调整它,这些元素放在路径元素之间,并提供对路径及其上的代表项的更细粒度的控制。 PathPercent控制每个元素之间覆盖的路径部分有多大,这反过来又控制了代表项在路径上的分布,因为它们的分布与进度百分比成正比。

PathViewpreferredHighlightBeginpreferredHighlightEnd属性是进入高亮的位置,它们都被期望是在0到1范围内的实数值,终点需要大于等于起点。设置这两个属性,例如都设置成0.5,当前项将显示在沿路径百分之五十的位置。译者笔记:这是设置当前项默认的位置,如果都设置成0.3,则最开始当前项将会在30%这个位置,当前项跟显示的大小无关,可以通过判断当前项变化颜色找到当前项的位置。如果将highlightRangeMode设置为PathView.StrictlyEnforceRange,会发现切换当前项目时候,总会显示在30%。对于起点和终点的设置,应该是一个百分比的范围,设置的较宽时候,可能存在多个进入这个范围的情况,通过测试是起点位置优先。

Path中,如同PathPercent元素一样,PathAttribute元素被放置在元素之间,它们允许指定沿路径插入的属性值。这些属性附加到代理,可用于控制任何可能的属性。

image

下面的示例演示了如何使用PathView元素来创建用户可以翻阅的卡片的视图,它采用了许多技巧来做到这一点。 路径由三个PathLine元素组成;使用PathPercent元素,可以使中心元素正确居中并提供足够的空间不会被其他元素弄乱;使用PathAttribute元素,可以控制旋转、大小和z值。

path外,还设置了PathViewpathItemCount属性,这控制了路径上的呈现密度。 preferredHighlightBeginpreferredHighlightEndPathView.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元素的itemZitemAngleitemScale附加属性。 值得注意的是,只能在wrapper中获得代理的附加属性, 因此,如果想在Rotation元素中要访问相关附加属性值的话,需要定义wrapperrotX属性做一个中转桥梁。

PathView的另一个特定值得注意的细节是:PathView.onPath附加属性的使用。 将可见性绑定到this是常见的做法,这允许PathView为缓存的目的保留不可见的元素。 这种目的通常不能通过剪切来处理,因为PathView的代理项目的摆放比ListViewGridView视图更自由。

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)不了解itemZitemAngleitemScale属性。译者笔记:qmlint是Qt附带的一个工具,用于验证QML文件的语法有效性。更多信息请参考Qt Quick tool qmllint (opens new window)

当使用PathView并以编程方式更改其currentIndex时,可能希望控制路径移动的方向,可以使用movementDirection属性来完成操作。它可以设置为PathView.Shortest,这是默认值,这意味着可以在任一方向移动,它具体取决于哪种方式最接近目标值;也可以通过将movementDirection设置为PathView.NegativePathView.Positive来限制方向。

# 表模型

到目前为止,讨论过的所有视图都以一种或另一种方式呈现一个系列的项目,甚至就连GridView也是期望模型里面提供的是一个一维的项目列表。 对于二维的数据表,需要使用TableView元素。

与其他视图类似,TableView组合了一个model和一个delegate来形成一个网格。 如果给定一个面向列的模型,它会显示一列,变得与ListView元素非常相似。 但是,它还可以显示明确定义了列和行的二维模型。

在下面的示例中,使用从C++暴露的自定义模型来设置了一个简单的TableView。 目前,无法直接从QML里面创建面向表的模型,在“Qt与C++”一章中解释了这个概念。 运行示例的结果如下图所示:

image

在下面的示例中,创建了一个TableView,并设置了rowSpacingcolumnSpacing来控制代理之间的水平间隙和垂直间隙。 其余属性的设置与其他类型的视图一样。

TableView {
    id: view
    anchors.fill: parent
    anchors.margins: 20

    rowSpacing: 5
    columnSpacing: 5

    clip: true

    model: tableModel
    delegate: cellDelegate
}

代理本身可以通过implicitWidthimplicitHeight携带隐式大小。 在下面的示例中就将这样做的,这个例子中,实际展示数据内容从模型的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
    // ...
}

请注意,宽度和高度都必须大于零。

当代理提供隐式大小时,每行最高的代理和每列最宽的代理控制大小。 如果项目的宽度取决于行,或者高度取决于列,这会产生有趣的行为。 这是因为并非所有代理都始终被实例化,因此当用户滚动表格时,列的宽度可能会发生变化。译者笔记:这是由于在下面的代理还没有显示出来过,当显示出来后,如果比原来的宽,则所有的根据最宽的确定当前表格的列宽,那么总体表格的列宽会越往下滑动越宽(如果越往下隐式宽度越宽的话)。

为了避免使用隐式代理尺寸指定列宽和行高的问题,可以提供计算这些尺寸的函数,这通过使用columnWidthProviderrowHeightProvider完成控制的。如下所示,这些函数分别返回列宽和行高:

TableView {
    columnWidthProvider: function (column) { return 10 * (column + 1) }
    // ...
}

如果需要动态更改列宽或行高,必须通过调用forceLayout方法通知视图。 这将使视图重新计算所有单元格的大小和位置。

# 来自XML的模型

由于XML是一种普遍存在的数据格式,QML提供了XmlListModel元素,该元素将XML数据暴露成为模型。 该元素可以在本地或远程获取XML数据,然后使用XPath表达式处理数据。

下面的示例演示了从RSS流中获取图像。 source属性通过HTTP指定远程的位置,数据会自动下载。

image

数据被下载后,将其处理为模型项和角色。 XmlListModelquery属性是一个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可以将平铺的列表划分成多个类别,从而提供更深入的体验。

image

为了使用分段,必须设置section.propertysection.criteriasection.property定义了使用哪个属性将内容划分段的依据。在这里,重要的是要知道必须对模型进行排序,以便每个段都由连续元素组成,否则,相同的属性名称可能会出现在多个位置。

section.criteria可以设置为ViewSection.FullStringViewSection.FirstCharacterViewSection.FullString是默认值,可用于具有清晰分段的模型,例如音乐专辑曲目。ViewSection.FirstCharacter依据属性的第一个字符分段,意味着任何属性都可以用于分段,最常见的例子是电话簿中联系人的姓氏。

定义了分段后,可以使用附加属性ListView.sectionListView.previousSectionListView.nextSection从每个项上访问它们。使用这些属性,可以检测一段的第一个和最后一个项目,并采取相应的行动。

也可以将段代理组件分配给ListViewsection.delegate属性。这将创建一个段标题代理,该代理在本段所有项目之前插入。代理组件可以使用附加属性section访问当前段的名称。

下面的示例通过显示按国籍划分的太空人列表来演示分段概念。 将nation用作section.propertysection.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

image

在下面的示例中,将三个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的另一个方面是,可以使用getinsertmoveremoveclear方法动态填充。通过这种方式,模型的内容可以从各种源动态生成,并且仍然可以轻松地显示在单个视图中。

# 带有动作的模型

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来推迟初始化正是这样做的——它会推迟性能问题。这意味着滚动性能将得到改善,但实际内容仍需要时间才能显示。

最后更新: 12/17/2021, 11:17:15 PM