Hello Qt(五十二)———QtQuick中的模型视图

能翔宇
2023-12-01

一、QtQuick模型视图基础

模型视图是一种数据和显示相分离的技术。QtQuick提供了一系列预定义的模型和视图。

1、Repeater

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适用于少量的静态数据集。

2、动态视图

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。前者按照先从左向右、再从上到下的顺序填充,滚动条出现在竖直方向;后者按照先从上到下、再从左到右的顺序填充,滚动条出现在水平方向。

二、QtQuick视图代理

1、视图代理

在自定义用户界面中,代理扮演着重要的角色。模型中的每一个数据项都要通过一个代理向用户展示,用户看到的可视部分就是代理。

每一个代理都可以访问一系列属性和附加属性。这些属性及附加属性中,有些来自于数据模型,有些则来自于视图。前者为代理提供了每一个数据项的数据信息;后者则是有关视图的状态信息。

代理中最常用到的是来自于视图的附加属性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信号。

2、代理中事件的处理

一个视图中的数据项并不是固定不变的,需要动态地增加、移除。视图中数据项的增加、移除其实是对底层模型的修改的反应。添加动画效果可以让用户清晰地明白哪些数据发生了改变。

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,保证在代理被销毁前动画能够顺利完成。

3、代理的定制

在表现列表时,常常会有一种机制:当数据项被选中时,数选中据项项会变大以充满屏幕。这种行为可以将被激活的数据项放置在屏幕中央,或者为用户显示更详细的信息。

本例中,ListView的每一个数据项在点击时都会充满整个列表视图,多出来的额外空间用于显示更多信息。可以使用状态实现这种机制。在这个过程中,列表的很多属性都会发生改变。

首先,wrapper的高度会被设置为ListView的高度;缩略图会变大,从先前的位置移动到一个更大的位置。除此以外,两个隐藏的组件,factsView和closeButton会显示在恰当的位置。最后,ListView的contentsY属性会被重新设置为代理的y值。contentsY属性其实就是视图的可见部分的顶部距离。视图的interactive属性会被设置为false,这可以避免用户通过拖动滚动条使视图移动。当数据项第一次被点击时,它会进入expanded状态,是其代理充满整个ListView,并且重新布局内容。当点击关闭按钮时,expanded状态被清除,代理重新回到原始的状态,ListView的交互也重新被允许。

三、QtQuick模型视图高级技术

1、PathView

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属性控制代理的可见性。

2、从XML加载模型

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属性值。

3、分组列表

为了使用分组,需要设置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
    }
}

4、模型视图的性能

模型视图的性能很大程度上取决于创建新的代理所造成的消耗。例如,如果clip属性设置为false,当向下滚动ListView时,系统会在列表末尾创建新的代理,并且将列表上部已经不可显示的代理移除。初始化代理需要消耗大量时间时,用户在快速拖动滚动条时,初始化代理和删除不可显示代理会造成一定程度的影响。

为了避免创建代理造成的消耗,可以调整被滚动视图的外边框的值。通过修改cacheBuffer属性即可达到这一目的。在上面所述的有关竖直滚动的例子中,这个属性会影响到列表上方和下方会有多少像素。这些像素则影响到是否能够容纳这些代理。例如,将异步加载图片与此结合,就可以实现在图片真正加载完毕之后才显示出来。

更多的代理意味着更多的内存消耗,从而影响到用户的操作流畅度,同时也有关代理初始化的时间。对于复杂的代理,通过修改cacheBuffer属性并不能从根本上解决问题。代理初始化一次,代理内容就会被重新计算,会消耗时间,如果消耗时间很长,会显著降低用户体验。代理中子元素的个数同样也有影响,因为移动更多的元素当然要更多的时间。为了解决代理的消耗问题,推荐使用Loader元素。Loader元素允许延时加载额外的元素。例如,一个可展开的代理,只有当用户点击时,才会显示详细信息,包含一个很大的图片。利用Loader元素,可以做到只有其被显示时才进行加载,否则不加载。同样,每个代理中包含的JavaScript代码应该尽可能少,最好能做到在代理之外调用复杂的JavaScript代码。这会减少代理创建时编译JavaScript所消耗的时间。

 类似资料: