模型-视图-代理(Model-View-Delegate)

注意

最后一次构建:2014年1月20日下午18:00。

这章的源代码能够在assetts folder找到。

在QtQuick中,数据通过model-view(模型-视图)分离。对于每个view(视图),每个数据元素的可视化都分给一个代理(delegate)。QtQuick附带了一组预定义的模型与视图。想要使用这个系统,必须理解这些类,并且知道如何创建合适的代理来获得正确的显示和交互。

概念(Concept)

对于开发用户界面,最重要的一方面是保持数据与可视化的分离。例如,一个电话薄可以使用一个垂直文本链表排列或者使用一个网格联系人图片排列。在这两个案例中,数据都是相同的,但是可视化效果却是不同的。这种方法通常被称作model-view(模型-视图)模式。在这种模式中,数据通常被称作model(模型),可视化处理称作view(视图)。

在QML中,model(模型)与view(视图)都通过delegate(代理)连接起来。功能划分如下,model(模型)提供数据。对于每个数据项,可能有多个值。在上面的电话薄例子中,每个电话薄条目对应一个名字,一个图片和一个号码。显示在view(视图)中的每项数据,都是通过delegate(代理)来实现可视化。view(视图)的任务是排列这些delegate(代理),每个delegate(代理)将model item(模型项)的值显示给用户。

基础模型(Basic Model)

最基本的分离数据与显示的方法是使用Repeater元素。它被用于实例化一组元素项,并且很容易与一个用于填充用户界面的定位器相结合。

最基本的实现举例,repeater元素用于实现子元素的标号。每个子元素都拥有一个可以访问的属性index,用于区分不同的子元素。在下面的例子中,一个repeater元素创建了10个子项,子项的数量由model属性控制。对于每个子项Rectangle包含了一个Text元素,你可以将text属性设置为index的值,因此可以看到子项的编号是0~9。

import QtQuick 2.0

Column {
    spacing: 2

    Repeater {
        model: 10

        Rectangle {
            width: 100
            height: 20

            radius: 3

            color: "lightBlue"

            Text {
                anchors.centerIn: parent
                text: index
            }
        }
    }
}

这是一个不错的编号列表,有时我们想显示一些更复杂的数据。使用一个JavaScript序列来替换整形变量model的值可以达到我们的目的。序列可以使用任何类型的内容,可以是字符串,整数,或者对象。在下面的例子中,使用了一个字符串链表。我们仍然使用index的值作为变量,并且我们也访问modelData中包含的每个元素的数据。

import QtQuick 2.0

Column {
    spacing: 2

    Repeater {
        model: ["Enterprise", "Colombia", "Challenger", "Discovery", "Endeavour", "Atlantis"]

        Rectangle {
            width: 100
            height: 20

            radius: 3

            color: "lightBlue"

            Text {
                anchors.centerIn: parent
                text: index +": "+modelData
            }
        }
    }
}

将数据暴露成一组序列,你可以通过标号迅速的找到你需要的信息。想象一下这个模型的草图,这是一个最简单的模型,也是通常都会使用的模型,ListModel(链表模型)。一个链表模型由许多ListElement(链表元素)组成。在每个链表元素中,可以绑定值到属性上。例如在下面这个例子中,每个元素都提供了一个名字和一个颜色。

每个元素中的属性绑定连接到repeater实例化的子项上。这意味着变量name和surfaceColor可以被repeater创建的每个Rectangle和Text项引用。这不仅可以方便的访问数据,也可以使源代码更加容易阅读。surfaceColor是名字左边圆的颜色,而不是模糊的数据序列列i或者行j。

import QtQuick 2.0

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
            }
        }
    }
}

repeater的内容的每个子项实例化时绑定了默认的属性delegate(代理)。这意味着例1(第一个代码段)的代码与下面显示的代码是相同的。注意,唯一的不同是delegate属性名,将会在后面详细讲解。

import QtQuick 2.0

Column {
    spacing: 2

    Repeater {
        model: 10

        delegate: Rectangle {
            width: 100
            height: 20

            radius: 3

            color: "lightBlue"

            Text {
                anchors.centerIn: parent
                text: index
            }
        }
    }
}

动态视图(Dynamic Views)

Repeater元素适合有限的静态数据,但是在真正使用时,模型通常更加复杂和庞大,我们需要一个更加智能的解决方案。QtQuick提供了ListView和GridView元素,这两个都是基于Flickable(可滑动)区域的元素,因此用户可以放入更大的数据。同时,它们限制了同时实例化的代理数量。对于一个大型的模型,这意味着在同一个场景下只会加载有限的元素。

这两个元素的用法非常类似,我们由ListView开始,然后会描述GridView的模型起点来进行比较。

ListView与Repeater元素像素,它使用了一个model,使用delegate来实例化,并且在两个delegate之间能够设置间隔sapcing。下面的列表显示了怎样设置一个简单的链表。

import QtQuick 2.0

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元素只会显示部分的链表内容。然后由于QtQuick的默认行为导致的问题,列表视图不会限制被显示的代理项(delegates)只在限制区域内显示。这意味着代理项可以在列表视图外显示,用户可以看见在列表视图外动态的创建和销毁这些代理项(delegates)。为了防止这个问题,ListView通过设置clip属性为true,来激活裁剪功能。下面的图片展示了这个结果,左边是clip属性设置为false的对比。

对于用户,ListView(列表视图)是一个滚动区域。它支持惯性滚动,这意味着它可以快速的翻阅内容。默认模式下,它可以在内容最后继续伸展,然后反弹回去,这个信号告诉用户已经到达内容的末尾。

视图末尾的行为是由到boundsBehavior属性的控制的。这是一个枚举值,并且可以配置为默认的Flickable.DragAndOvershootBounds,视图可以通过它的边界线来拖拽和翻阅,配置为Flickable.StopAtBounds,视图将不再可以移动到它的边界线之外。配置为Flickable.DragOverBounds,用户可以将视图拖拽到它的边界线外,但是在边界线上翻阅将无效。

使用snapMode属性可以限制一个视图内元素的停止位置。默认行为下是ListView.NoSnap,允许视图内元素在任何位置停止。将snapMode属性设置为ListView.SnapToItem,视图顶部将会与元素对象的顶部对齐排列。使用ListView.SnapOneItem,当鼠标或者触摸释放时,视图将会停止在第一个可见的元素,这种模式对于浏览页面非常便利。

6.3.1 方向(Orientation)

默认的链表视图只提供了一个垂直方向的滚动条,但是水平滚动条也是需要的。链表视图的方向由属性orientation控制。它能够被设置为默认值ListView.Vertical或者ListView.Horizontal。下面是一个水平链表视图。

import QtQuick 2.0

Rectangle {
    width: 480
    height: 80

    color: "white"

    ListView {
        anchors.fill: parent
        anchors.margins: 20

        clip: true

        model: 100

        orientation: ListView.Horizontal

        delegate: numberDelegate
        spacing: 5
    }

    Component {
        id: numberDelegate

        Rectangle {
            width: 40
            height: 40

            color: "lightGreen"

            Text {
                anchors.centerIn: parent

                font.pixelSize: 10

                text: index
            }
        }
    }
}

按照上面的设置,水平链表视图默认的元素顺序方向是由左到右。可以通过设置layoutDirection属性来控制元素顺序方向,它可以设置为Qt.LeftToRight或者Qt.RightToLeft。

6.3.2 键盘导航和高亮

当使用基于触摸方式的链表视图时,默认提供的视图已经足够使用。在使用键盘甚至仅仅通过方向键选择一个元素的场景下,需要有标识当前选中元素的机制。在QML中,这被叫做高亮。

视图支持设置一个当前视图中显示代理元素中的高亮代理。它是一个附加的代理元素,这个元素仅仅只实例化一次,并移动到与当前元素相同的位置。

在下面例子的演示中,有两个属性来完成这个工作。首先是focus属性设置为true,它设置链表视图能够获得键盘焦点。然后是highlight属性,指出使用的高亮代理元素。高亮代理元素的x,y与height属性由当前元素指定。如果宽度没有特别指定,当前元素的宽度也可以用于高亮代理元素。

在例子中,ListView.view.width属性被绑定用于高亮元素的宽度。关于代理元素的使绑定属性将在后面的章节讨论,但是最好知道相同的绑定属性也可以用于高亮代理元素。

import QtQuick 2.0

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
            }
        }
    }
}
// M1>>

当使用高亮与链表视图(ListView)结合时,一些属性可以用来控制它的行为。highlightRangeMode控制了高亮如何影响视图中当前的显示。默认设置ListView.NoHighLighRange意味着高亮与视图中的元素距离不相关。

ListView.StrictlyEnforceRnage确保了高亮始终可见,如果某个动作尝试将高亮移出当前视图可见范围,当前元素将会自动切换,确保了高亮始终可见。

ListView.ApplyRange,它尝试保持高亮代理始终可见,但是不会强制切换当前元素始终可见。如果在需要的情况下高亮代理允许被移出当前视图。

在默认配置下,视图负责高亮移动到指定位置,移动的速度与大小的改变能够被控制,使用一个速度值或者一个动作持续时间来完成它。这些属性包括highlightMoveSpeed,highlightMoveDuration,highlightResizeSpeed和highlightResizeDuration。默认下速度被设置为每秒400像素,动作持续时间为-1,表明速度和距离控制了动作的持续时间。如果速度与动作持续时间都被设置,动画将会采用速度较快的结果来完成。

为了更加详细的控制高亮的移动,highlightFollowCurrentItem属性设置为false。这意味着视图将不再负责高亮代理的移动。取而代之可以通过一个行为(Bahavior)或者一个动画来控制它。

在下面的例子中,高亮代理的y坐标属性与ListView.view.currentItem.y属性绑定。这确保了高亮始终跟随当前元素。然而,由于我们没有让视图来移动这个高亮代理,我们需要控制这个元素如何移动,通过Behavior on y来完成这个操作,在下面的例子中,移动分为三步完成:淡出,移动,淡入。注意怎样使用SequentialAnimation和PropertyAnimation元素与NumberAnimation结合创建更加复杂的移动效果。

Component {
    id: highlightComponent

    Item {
        width: ListView.view.width
        height: ListView.view.currentItem.height

        y: ListView.view.currentItem.y

        Behavior on y {
            SequentialAnimation {
                PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 0; duration: 200 }
                NumberAnimation { duration: 1 }
                PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 1; duration: 200 }
            }
        }

        Rectangle {
            id: highlightRectangle
            anchors.fill: parent
            color: "lightGreen"
        }
    }
}

6.3.3 页眉与页脚(Header and Footer)

这一节是链表视图最后的内容,我们能够向链表视图中插入一个页眉(header)元素和一个页脚(footer)元素。这部分是链表的开始或者结尾处被作为代理元素特殊的区域。对于一个水平链表视图,不会存在页眉或者页脚,但是也有开始和结尾处,这取决于layoutDirection的设置。

下面这个例子展示了如何使用一个页眉和页脚来突出链表的开始与结尾。这些特殊的链表元素也有其它的作用,例如,它们能够保持链表中的按键加载更多的内容。

import QtQuick 2.0

Rectangle {
    width: 80
    height: 300

    color: "white"

    ListView {
        anchors.fill: parent
        anchors.margins: 20

        clip: true

        model: 4

        delegate: numberDelegate
        spacing: 5

        header: headerComponent
        footer: footerComponent
    }

    Component {
        id: headerComponent

        Rectangle {
            width: 40
            height: 20

            color: "yellow"
        }
    }

    Component {
        id: footerComponent

        Rectangle {
            width: 40
            height: 20

            color: "red"
        }
    }

    Component {
        id: numberDelegate

        Rectangle {
            width: 40
            height: 40

            color: "lightGreen"

            Text {
                anchors.centerIn: parent

                font.pixelSize: 10

                text: index
            }
        }
    }
}

注意

页眉与页脚代理元素不遵循链表视图(ListView)的间隔(spacing)属性,它们被直接放在相邻的链表元素之上或之下。这意味着页眉与页脚的间隔必须通过页眉与页脚元素自己设置。

6.3.4 网格视图(The GridView)

使用网格视图(GridView)与使用链表视图(ListView)的方式非常类似。真正不同的地方是网格视图(GridView)使用了一个二维数组来存放元素,而链表视图(ListView)是使用的线性链表来存放元素。

与链表视图(ListView)比较,网格视图(GridView)不依赖于元素间隔和大小来配置元素。它使用单元宽度(cellWidth)与单元高度(cellHeight)属性来控制数组内的二维元素的内容。每个元素从左上角开始依次放入单元格。

import QtQuick 2.0

Rectangle {
    width: 240
    height: 300

    color: "white"

    GridView {
        anchors.fill: parent
        anchors.margins: 20

        clip: true

        model: 100

        cellWidth: 45
        cellHeight: 45

        delegate: numberDelegate
    }

    Component {
        id: numberDelegate

        Rectangle {
            width: 40
            height: 40

            color: "lightGreen"

            Text {
                anchors.centerIn: parent

                font.pixelSize: 10

                text: index
            }
        }
    }
}

一个网格视图(GridView)也包含了页脚与页眉,也可以使用高亮代理并且支持捕捉模式(snap mode)的多种反弹行为。它也可以使用不同的方向(orientations)与定向(directions)来定位。

定向使用flow属性来控制。它可以被设置为GridView.LeftToRight或者GridView.TopToBottom。模型的值从左往右向网格中填充,行添加是从上往下。视图使用一个垂直方向的滚动条。后面添加的元素也是由上到下,由左到右。

此外还有flow属性和layoutDirection属性,能够适配网格从左到右或者从右到左,这依赖于你使用的设置值。

代理(Delegate)

当使用模型与视图来自定义用户界面时,代理在创建显示时扮演了大量的角色。在模型中的每个元素通过代理来实现可视化,用户真实可见的是这些代理元素。

每个代理访问到索引号或者绑定的属性,一些是来自数据模型,一些来自视图。来自模型的数据将会通过属性传递到代理。来自视图的数据将会通过属性传递视图中与代理相关的状态信息。

通常使用的视图绑定属性是ListView.isCurrentItem和ListView.view。第一个是一个布尔值,标识这个元素是否是视图当前元素,这个值是只读的,引用自当前视图。通过访问视图,可以创建可复用的代理,这些代理在被包含时会自动匹配视图的大小。在下面这个例子中,每个代理的width(宽度)属性与视图的width(宽度)属性绑定,每个代理的背景颜色color依赖于绑定的属性ListView.isCurrentItem属性。

import QtQuick 2.0

Rectangle {
    width: 120
    height: 300

    color: "white"

    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?"gray":"lightGray"

            Text {
                anchors.centerIn: parent

                font.pixelSize: 10

                text: index
            }
        }
    }
}

如果在模型中的每个元素与一个动作相关,例如点击作用于一个元素时,这个功能是代理完成的。这是由事件管理分配给视图的,这个操作控制了视图中元素的导航,代理控制了特定元素上的动作。

最基础的方法是在每个代理中创建一个MouseArea(鼠标区域)并且响应onClicked信号。在后面章节中将会演示这个例子。

6.4.1 动画添加与移除元素(Animating Added and Removed Items)

在某些情况下,视图中的显示内容会随着时间而改变。由于模型数据的改变,元素会添加或者移除。在这些情况下,一个比较好的做法是使用可视化队列给用户一个方向的感觉来帮助用户知道哪些数据被加入或者移除。

为了方便使用,QML视图为每个代理绑定了两个信号,onAdd和onRemove。使用动画连接它们,可以方便创建识别哪些内容被添加或删除的动画。

下面这个例子演示了如何动态填充一个链表模型(ListModel)。在屏幕下方,有一个添加新元素的按钮。当点击它时,会调用模型的append方法来添加一个新的元素。这个操作会触发视图创建一个新的代理,并发送GridView.onAdd信号。SequentialAnimation队列动画与这个信号连接绑定,使用代理的scale属性来放大视图元素。

当视图中的一个代理点击时,将会调用模型的remove方法将一个元素从模型中移除。这个操作将会导致GridView.onRemove信号的发送,触发另一个SequentialAnimation。这时,代理的销毁将会延迟直到动画完成。为了完成这个操作,PropertyAction元素需要在动画前设置GridView.delayRemove属性为true,并在动画后设置为false。这样确保了动画在代理项移除前完成。

import QtQuick 2.0

Rectangle {
    width: 480
    height: 300

    color: "white"

    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: "darkGreen"

        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

            color: "lightGreen"

            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 }
            }
        }
    }
}

6.4.2 形变的代理(Shape-Shifting Delegates)

在使用链表时通常会使用当前项激活时展开的机制。这个操作可以被用于动态的将当前项目填充到整个屏幕来添加一个新的用户界面,或者为链表中的当前项提供更多的信息。

在下面的例子中,当点击链表项时,链表项都会展开填充整个链表视图(ListView)。额外的间隔区域被用于添加更多的信息,这种机制使用一个状态来控制,当一个链表项展开时,代理项都能输入expanded(展开)状态,在这种状态下一些属性被改变。

首先,包装器(wrapper)的高度(height)被设置为链表视图(ListView)的高度。标签图片被放大并且下移,使图片从小图片的位置移向大图片的位置。除了这些之外,两个隐藏项,实际视图(factsView)与关闭按键(closeButton)切换它的opactiy(透明度)显示出来。最后设置链表视图(ListView)。

设置链表视图(ListView)包含了设置内容Y坐标(contentsY),这是视图顶部可见的部分代理的Y轴坐标。另一个变化是设置视图的交互(interactive)为false。这个操作阻止了视图的移动,用户不再能够通过滚动条切换当前项。

由于设置第一个链表项为可点击,向它输入一个expanded(展开)状态,导致了它的代理项被填充到整个链表并且内容重置。当点击关闭按钮时,清空状态,导致它的代理项返回上一个状态,并且重新设置链表视图(ListView)有效。

import QtQuick 2.0

Item {
    width: 300
    height: 480

    ListView {
        id: listView

        anchors.fill: parent

        delegate: detailsDelegate
        model: planets
    }

    ListModel {
        id: planets

        ListElement { name: "Mercury"; imageSource: "images/mercury.jpeg"; facts: "Mercury is the smallest planet in the Solar System. It is the closest planet to the sun. It makes one trip around the Sun once every 87.969 days." }
        ListElement { name: "Venus"; imageSource: "images/venus.jpeg"; facts: "Venus is the second planet from the Sun. It is a terrestrial planet because it has a solid, rocky surface. The other terrestrial planets are Mercury, Earth and Mars. Astronomers have known Venus for thousands of years." }
        ListElement { name: "Earth"; imageSource: "images/earth.jpeg"; facts: "The Earth is the third planet from the Sun. It is one of the four terrestrial planets in our Solar System. This means most of its mass is solid. The other three are Mercury, Venus and Mars. The Earth is also called the Blue Planet, 'Planet Earth', and 'Terra'." }
        ListElement { name: "Mars"; imageSource: "images/mars.jpeg"; facts: "Mars is the fourth planet from the Sun in the Solar System. Mars is dry, rocky and cold. It is home to the largest volcano in the Solar System. Mars is named after the mythological Roman god of war because it is a red planet, which signifies the colour of blood." }
    }

    Component {
        id: detailsDelegate

        Item {
            id: wrapper

            width: listView.width
            height: 30

            Rectangle {
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.top: parent.top

                height: 30

                color: "#ffaa00"

                Text {
                    anchors.left: parent.left
                    anchors.verticalCenter: parent.verticalCenter

                    font.pixelSize: parent.height-4

                    text: name
                }
            }

            Rectangle {
                id: image

                color: "black"

                anchors.right: parent.right
                anchors.top: parent.top
                anchors.rightMargin: 2
                anchors.topMargin: 2

                width: 26
                height: 26

                Image {
                    anchors.fill: parent

                    fillMode: Image.PreserveAspectFit

                    source: imageSource
                }
            }

            MouseArea {
                anchors.fill: parent
                onClicked: parent.state = "expanded"
            }

            Item {
                id: factsView

                anchors.top: image.bottom
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.bottom: parent.bottom

                opacity: 0

                Rectangle {
                    anchors.fill: parent

                    color: "#cccccc"

                    Text {
                        anchors.fill: parent
                        anchors.margins: 5

                        clip: true
                        wrapMode: Text.WordWrap

                        font.pixelSize: 12

                        text: facts
                    }
                }
            }

            Rectangle {
                id: closeButton

                anchors.right: parent.right
                anchors.top: parent.top
                anchors.rightMargin: 2
                anchors.topMargin: 2

                width: 26
                height: 26

                color: "red"

                opacity: 0

                MouseArea {
                    anchors.fill: parent
                    onClicked: wrapper.state = ""
                }
            }

            states: [
                State {
                    name: "expanded"

                    PropertyChanges { target: wrapper; height: listView.height }
                    PropertyChanges { target: image; width: listView.width; height: listView.width; anchors.rightMargin: 0; anchors.topMargin: 30 }
                    PropertyChanges { target: factsView; opacity: 1 }
                    PropertyChanges { target: closeButton; opacity: 1 }
                    PropertyChanges { target: wrapper.ListView.view; contentY: wrapper.y; interactive: false }
                }
            ]

            transitions: [
                Transition {
                    NumberAnimation {
                        duration: 200;
                        properties: "height,width,anchors.rightMargin,anchors.topMargin,opacity,contentY"
                    }
                }
            ]
        }
    }
}

这个技术展示了展开代理来填充视图能够简单的通过代理的形变来完成。例如当浏览一个歌曲的链表时,可以通过放大当前项来对该项添加更多的说明。

高级用法(Advanced Techniques)

6.5.1 路径视图(The PathView)

路径视图(PathView)非常强大,但也非常复杂,这个视图由QtQuick提供。它创建了一个可以让子项沿着任意路径移动的视图。沿着相同的路径,使用缩放(scale),透明(opacity)等元素可以更加详细的控制过程。

当使用路径视图(PathView)时,你必须定义一个代理和一个路径。在这些之上,路径视图(PathView)本身也可以自定义一些属性的区间。通常会使用pathItemCount属性,它控制了一次可见的子项总数。preferredHighLightBegin属性控制了高亮区间,preferredHighlightEnd与highlightRangeMode,控制了当前项怎样沿着路径显示。

在关注高亮区间之前,我们必须先看看路径(path)这个属性。路径(path)属性使用一个路径(path)元素来定义路径视图(PathView)内代理的滚动路径。路径使用startx与starty属性来链接路径(path)元素,例如PathLine,PathQuad和PathCubic。这些元素都使用二维数组来构造路径。

当路径定义好之后,可以使用PathPercent和PathAttribute元素来进一步设置。它们被放置在路径元素之间,并且为经过它们的路径和代理提供更加细致的控制。PathPercent提供了如何控制每个元素之间覆盖区域部分的路径,然后反过来控制分布在这条路径上的代理元素,它们被按比例的分布播放。

preferredHightlightBegin与preferredHighlightEnd属性由PathView(路径视图)输入到图片元素中。它们的值在0~1之间。结束值大于等于开始值。例如设置这些属性值为0.5,当前项只会显示当前百分之50的图像在这个路径上。

在Path中,PathAttribute元素也是被放置在元素之间的,就像PathPercent元素。它们可以让你指定属性的值然后插入的路径中去。这些属性与代理绑定可以用来控制任意的属性。

下面这个例子展示了路径视图(PathView)如何创建一个卡片视图,并且用户可以滑动它。我们使用了一些技巧来完成这个例子。路径由PathLine元素组成。使用PathPercent元素,它确保了中间的元素居中,并且给其它的元素提供了足够的空间。使用PathAttribute元素来控制旋转,大小和深度值(z-value)。

在这个路径之上(path),需要设置路径视图(PathView)的pathItemCount属性。它控制了路径的浓密度。路径视图的路径(PathView.onPath)使用preferredHighlightBegin与preferredHighlightEnd来控制可见的代理项。

PathView {
    anchors.fill: parent

    delegate: flipCardDelegate
    model: 100

    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中可用。因此,rotxs属性在Rotation元素中定义为可访问值。

另一个需要注意的是路径视图(PathView)链接的PathView.onPath属性的用法。通常对于这个属性都绑定为可见,这样允许路径视图(PathView)缓冲不可见的元素。这不是通过剪裁处理来实现的,因为路径视图(PathView)的代理比其它的视图,例如链表视图(ListView)或者栅格视图(GridView)放置更加随意。

Component {
    id: flipCardDelegate

    Item {
        id: wrapper

        width: 64
        height: 64

        visible: PathView.onPath

        scale: PathView.itemScale
        z: PathView.itemZ

        property variant rotX: PathView.itemAngle
        transform: Rotation { axis { x: 1; y: 0; z: 0 } angle: wrapper.rotX; origin { x: 32; y: 32; } }

        Rectangle {
            anchors.fill: parent
            color: "lightGray"
            border.color: "black"
            border.width: 3
        }

        Text {
            anchors.centerIn: parent
            text: index
            font.pixelSize: 30
        }
    }
}

当在路径视图(PathView)上使用图像转换或者其它更加复杂的元素时,有一个性能优化的技巧是绑定图像元素(Image)的smooth属性与PathView.view.moving属性。这意味着图像在移动时可能不够完美,但是能够比较平滑的转换。当视图在移动时,对于平滑缩放的处理是没有意义的,因为用户根本看不见这个过程。

6.5.2 XML模型(A Model from XML)

由于XML是一种常见的数据格式,QML提供了XmlListModel元素来包装XML数据。这个元素能够获取本地或者网络上的XML数据,然后通过XPath解析这些数据。

下面这个例子展示了从RSS流中获取图片,源属性(source)引用了一个网络地址,这个数据会自动下载。

当数据下载完成后,它会被加工作为模型的子项。查询属性(query)是一个XPath代理的基础查询,用来创建模型项。在这个例子中,这个路径是/rss/channel/item,所以,在一个模型子项创建后,每一个子项的标签,都包含了一个频道标签,包含一个RSS标签。

每一个模型项,一些规则需要被提取,由XmlRole元素来代理。每一个规则都需要一个名称,这样代理才能够通过属性绑定来访问。每个这样的属性的值都通过XPath查询来确定。例如标题属性(title)符合title/string()查询,返回内容中在之间的值。

图像源属性(imageSource)更加有趣,因为它不仅仅是从XML中提取字符串,也需要加载它。在流数据的支持下,每个子项包含了一个图片。使用XPath的函数substring-after与substring-before,可以提取本地的图片资源。这样imageSource属性就可以直接被作为一个Image元素的source属性使用。

import QtQuick 2.0
import QtQuick.XmlListModel 2.0

Item {
    width: 300
    height: 480

    Component {
        id: imageDelegate

        Item {
            width: listView.width
            height: 400

        Column {
                Text {
                    text: title
                }

                Image {
                    source: imageSource
                }
            }
        }
    }

    XmlListModel {
        id: imageModel

        source: "http://feeds.nationalgeographic.com/ng/photography/photo-of-the-day/"
        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
    }
}

6.5.3 链表分段(Lists with Sections)

有时,链表的数据需要划分段。例如使用首字母来划分联系人,或者音乐。使用链表视图可以把平面列表按类别划分。

为了使用分段,section.property与section.criteria必须安装。section.property定义了哪些属性用于内容的划分。在这里,最重要的是知道每一段由哪些连续的元素构成,否则相同的属性名可能出现在几个不同的地方。

section.criteria能够被设置为ViewSection.FullString或者ViewSection.FirstCharacter。默认下使用第一个值,能够被用于模型中有清晰的分段,例如音乐专辑。第二个是使用一个属性的首字母来分段,这说明任何属性都可以被使用。通常的例子是用于联系人名单的姓。

当段被定义好后,每个子项能够使用绑定属性ListView.section,ListView.previousSection与ListView.nextSection来访问。使用这些属性,可以检测段的第一个与最后一个子项。

使用链表视图(ListView)的section.delegate属性可以给段指定代理组件。它能够创建段标题,并且可以在任意子项之前插入这个段代理。使用绑定属性section可以访问当前段的名称。

下面这个例子使用国际分类展示了分段的一些概念。国籍(nation)作为section.property,段代理组件(section.delegate)使用每个国家作为标题。在每个段中,spacemen模型中的名字使用spaceManDelegate组件来代理显示。

import QtQuick 2.0

Rectangle {
    width: 300
    height: 290

    color: "white"

    ListView {
        anchors.fill: parent
        anchors.margins: 20

        clip: true

        model: spaceMen

        delegate: spaceManDelegate

        section.property: "nation"
        section.delegate: sectionDelegate
    }

    Component {
        id: spaceManDelegate

        Item {
            width: 260
            height: 20

            Text {
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                anchors.leftMargin: 10

                font.pixelSize: 12

                text: name
            }
        }
    }

    Component {
        id: sectionDelegate

        Rectangle {
            width: 260
            height: 20

            color: "lightGray"

            Text {
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                anchors.leftMargin: 10

                font.pixelSize: 12
                font.bold: true

                text: section
            }
        }
    }


    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"; }
    }
}

6.5.4 性能协调(Tunning Performance)

一个模型视图的性能很大程度上依赖于代理的创建。例如滚动下拉一个链表视图时,代理从外部加入到视图底部,并且从视图顶部移出。如果设置剪裁(clip)属性为false,并且代理项花了很多时间来初始化,用户会感觉到视图滚动体验很差。

为了优化这个问题,你可以在滚动时使用像素来调整。使用cacheBuffer属性,在上诉情况下的垂直滚动,它将会调整在链表视图的上下需要预先准备好多少像素的代理项,结合异步加载图像元素(Image),例如在它们进入视图之前加载。

创建更多的代理项将会牺牲一些流畅的体验,并且花更多的时间来初始化每个代理。这并不代表可以解决一些更加复杂的代理项的问题。在每次实例化代理时,它的内容都会被评估和编辑。这需要花费时间,如果它花费了太多的时间,它将会导致一个很差的滚动体验。在一个代理中包含太多的元素也会降低滚动的性能。

为了补救这个问题,我们推荐使用动态加载元素。当它们需要时,可以初始化这些附加的元素。例如,一个展开代理可能推迟它的详细内容的实例化,直到需要使用它时。每个代理中最好减少JavaScript的数量。将每个代理中复杂的JavaScript调用放在外面来实现。这将会减少每个代理在创建时编译JavaScript。

总结(Summary)

在这个章节中,我们学习了模型,视图与代理。每个数据的入口是模型,视图通过可视化代理来实现数据的可视化。将数据从显示中分离出来。

一个模型可以是一个整数,提供给代理使用的索引值(index )。如果JavaScript数组被作为一个模型,模型数据变量(modelData)代表了数组的数据的当前索引。对于更加复杂的情况,每个数据项需要提供多个值,使用链表模型(ListModel)与链表元素(ListElement)是一个更好的解决办法。

对于静态模型,一个Repeater可以被用作视图。它可以非常方便的使用行(Row),列(Column),栅格(Grid),或者流(Flow)来创建用户界面。对于动态或者大的数据模型,使用ListView或者GridView更加适合。它们会在需要时动态的创建代理,减少在场景下一次显示的元素的数量。

在视图中的代理可以与数据模型中的属性静态绑定,或者动态绑定。使用视图的onAdd与onRemove信号,可以动态播放的它们的显示与消失。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2291184112@qq.com

×

喜欢就点赞,疼爱就打赏