Qt Quick 中的模型和视图

大多数应用程序需要格式化数据并显示数据。Qt Quick 有 模型视图代理 的概念来显示数据。它们模块化了数据的可视化,以便开发者或设计师可以控制数据的各个方面。开发者可以以对数据进行少量修改的方式将列表视图与网格视图进行交换。同样,将数据的实例封装在代理中允许开发者指定如何呈现或处理数据。

  • 模型 - 包含数据和其结构。有几种 QML 类型可以创建模型。
  • 视图 - 一个显示数据的容器。视图可以以列表或网格的形式显示数据。
  • 代理 - 决定数据应在视图中如何显示。代理封装模型中的每个数据单位。数据可以通过代理访问。代理还可以将数据写回可编辑的模型(例如,在 TextField 的 onAccepted 处理程序中)。

要可视化数据,将视图的 model 属性绑定到模型,将 delegate 属性绑定到组件或其他兼容类型。

使用视图显示数据

视图是项集合的容器。它们功能丰富,可以根据样式或行为要求进行自定义。

Qt Quick 图形类型的基本集中提供了一组标准的视图

这些类型具有各自特有的属性和行为。有关更多信息,请参阅它们的相应文档。

此外,Qt Quick Controls 包含了一些额外的视图和代理,这些视图和代理根据应用程序的样式进行样式化,例如 HorizontalHeaderViewVerticalHeaderView

装饰视图

视图允许通过如头部(header)、尾部(footer)和部分(section)等装饰属性进行视觉自定义。通过将这些属性绑定到对象上,通常是另一个可视对象,视图就可以被装饰。尾部可能包含一个显示边界的矩形(Rectangle)类型或显示在列表顶部的徽标。

假设一个特定的俱乐部希望用其品牌颜色装饰其成员名单。成员名单在模型(model)中,模型将显示模型的内容。

ListModel {
    id: nameModel
    ListElement { name: "Alice" }
    ListElement { name: "Bob" }
    ListElement { name: "Jane" }
    ListElement { name: "Harry" }
    ListElement { name: "Wendy" }
}
Component {
    id: nameDelegate
    Text {
        required property string name
        text: name
        font.pixelSize: 24
    }
}

俱乐部可以通过将可视化对象绑定到头部(header)和尾部(footer)属性来装饰成员名单。可视化对象可以内联定义,在另一个文件中定义或在组件(Component)类型中定义。

ListView {
    anchors.fill: parent
    clip: true
    model: nameModel
    delegate: nameDelegate
    header: bannercomponent
    footer: Rectangle {
        width: parent.width; height: 30;
        gradient: clubcolors
    }
    highlight: Rectangle {
        width: parent.width
        color: "lightgray"
    }
}

Component {     //instantiated when header is processed
    id: bannercomponent
    Rectangle {
        id: banner
        width: parent.width; height: 50
        gradient: clubcolors
        border {color: "#9EDDF2"; width: 2}
        Text {
            anchors.centerIn: parent
            text: "Club Members"
            font.pixelSize: 32
        }
    }
}
Gradient {
    id: clubcolors
    GradientStop { position: 0.0; color: "#8EE2FE"}
    GradientStop { position: 0.66; color: "#7ED2EE"}
}

鼠标和触摸处理

视图处理它们内容的拖动和快速滑动,但它们不处理与单个代表对象的触摸交互。为了使代表对象对触摸输入做出反应,例如设置currentIndex,代表必须提供一个具有适当的触摸处理逻辑的MouseArea

注意,如果将 highlightRangeMode 设置为 StrictlyEnforceRange,则 currentIndex 将受到拖动/快速滑动的 影响,因为视图将始终确保 currentIndex 在指定的突出显示范围内。

ListView 部分

ListView 内容可以被分组到 部分 中,其中相关列表项目根据其部分进行标记。此外,部分也可以使用代表对象(delegates)进行装饰。

一个列表可以包含一个表示人的名字和他们所属团队的数据列表。

ListModel {
    id: nameModel
    ListElement { name: "Alice"; team: "Crypto" }
    ListElement { name: "Bob"; team: "Crypto" }
    ListElement { name: "Jane"; team: "QA" }
    ListElement { name: "Victor"; team: "QA" }
    ListElement { name: "Wendy"; team: "Graphics" }
}
Component {
    id: nameDelegate
    Text {
        text: name;
        font.pixelSize: 24
        anchors.left: parent.left
        anchors.leftMargin: 2
    }
}

ListView 类型有一个 section 附加属性,可以将相邻的、相关联的类型组合成一个部分。《code translate="no">section.property 决定使用哪个列表类型属性作为部分,《code translate="no">section.criteria 可以指定如何显示部分名称,《code translate="no">section.delegate 与视图的 delegate 属性类似。

ListView {
    anchors.fill: parent
    model: nameModel
    delegate: nameDelegate
    focus: true
    highlight: Rectangle {
        color: "lightblue"
        width: parent.width
    }
    section {
        property: "team"
        criteria: ViewSection.FullString
        delegate: Rectangle {
            color: "#b0dfb0"
            width: parent.width
            height: childrenRect.height + 4
            Text { anchors.horizontalCenter: parent.horizontalCenter
                font.pixelSize: 16
                font.bold: true
                text: section
            }
        }
    }
}

视图代表

视图需要一个 代表 来在列表中直观地表示一个项。视图将根据代表定义的模板显示每个列表项。可以通过 index 属性以及项的属性来访问模型中的项。

Component {
    id: petdelegate
    Text {
        id: label
        font.pixelSize: 24
        text: index === 0 ? type + " (default)" : type

        required property int index
        required property string type
    }
}

视图代表的定位

视图的类型将决定项的定位方式。《a href="qml-qtquick-listview.html" translate="no">ListView 将根据 orientation 将项定位在直线上,而 GridView 可以在二维网格中布局。不建议直接在 xy 上绑定,因为视图的布局行为将始终优于任何定位绑定。

从代表处访问视图和模型

委托所绑定的查看列表可以通过委托的 ListView.view 属性访问。同样,GridViewGridView.view 对委托也是可用的。因此,相应的模型及其属性可以通过 ListView.view.model 获取。此外,模型中定义的任何信号或方法也是可访问的。

当您想为多个视图使用相同的委托,但每个视图的装饰或其他功能不同时,此机制非常有用。您可能还希望访问或显示模型的某些属性。

在下面的示例中,委托显示了模型的 language 属性,其中某些字段的颜色取决于视图的 fruit_color 属性。

Rectangle {
     width: 200; height: 200

    ListModel {
        id: fruitModel
        property string language: "en"
        ListElement {
            name: "Apple"
            cost: 2.45
        }
        ListElement {
            name: "Orange"
            cost: 3.25
        }
        ListElement {
            name: "Banana"
            cost: 1.95
        }
    }

    Component {
        id: fruitDelegate
        Row {
                id: fruit
                Text { text: " Fruit: " + name; color: fruit.ListView.view.fruit_color }
                Text { text: " Cost: $" + cost }
                Text { text: " Language: " + fruit.ListView.view.model.language }
        }
    }

    ListView {
        property color fruit_color: "green"
        model: fruitModel
        delegate: fruitDelegate
        anchors.fill: parent
    }
}

模型

通过名称数据角色将数据提供给委托,委托可以将这些角色绑定。这里有一个具有两个角色(typeage)的 ListModel,以及一个与这些角色绑定以显示其值的 ListView

import QtQuick

Item {
    width: 200
    height: 250

    ListModel {
        id: myModel
        ListElement { type: "Dog"; age: 8; noise: "meow" }
        ListElement { type: "Cat"; age: 5; noise: "woof" }
    }

    component MyDelegate : Text {
        required property string type
        required property int age
        text: type + ", " + age
        // WRONG: Component.onCompleted: () => console.log(noise)
        // The above line would cause a ReferenceError
        // as there is no required property noise,
        // and the presence of the required properties prevents
        // noise from being injected into the scope
    }

    ListView {
        anchors.fill: parent
        model: myModel
        delegate: MyDelegate {}
    }
}

在大多数情况下,您应该使用 必需属性 将模型数据传递到您的委托中。如果委托包含必需属性,QML 引擎将检查必需属性名称是否与模型角色名称相匹配。如果是这样,该属性将绑定到模型中的对应值。

在极少数情况下,您可能希望通过 QML 上下文而不是必需属性来传递模型属性。如果您的委托中没有必需属性,则命名角色作为上下文属性提供

import QtQuick

Item {
    width: 200; height: 250

    ListModel {
        id: myModel
        ListElement { type: "Dog"; age: 8 }
        ListElement { type: "Cat"; age: 5 }
    }

    Component {
        id: myDelegate
        Text { text: type + ", " + age }
    }

    ListView {
        anchors.fill: parent
        model: myModel
        delegate: myDelegate
    }
}

上下文属性对工具不可见,并阻止 Qt Quick 编译器 对您的代码进行优化。它们使推理委托期望的特定数据变得更困难。从 QML 中无法显式填充 QML 上下文。如果您的组件期望通过 QML 上下文传递数据,则只能将其用于通过本地方式提供正确上下文的位置。这可以是您自己的 C++ 代码或周围元素的特定实现。相反,从 QML 或通过本地方式可以设置多种必需属性。因此,通过 QML 上下文传递数据会降低您组件的可重用性。

如果模型属性与委托属性之间存在名称冲突,则可以使用有资格的 model 名称来访问角色。例如,如果一个 Text 类型有(非必需的)typeage 属性,上面的示例将显示那些属性值而不是模型项中的 typeage 值。在这种情况下,可以通过 model.typemodel.age 来引用这些属性,以确保委托显示模型项的属性值。为了使此工作,您需要在委托中要求一个 model 属性(除非您正在使用上下文属性)。

委托还可以访问一个特殊的 index 角色对象,包含模型中项的索引。注意,如果项从模型中移除,此索引将被设置为 -1。如果绑定到索引角色,请确保逻辑处理了索引可能为 -1 的可能性,即项不再是有效的。通常,项将很快被销毁,但在某些视图中可以通过附加属性 delayRemove 延迟委托销毁。

请记住,您可以使用整数或数组作为模型

Repeater {
    model: 5
    Text {
        required property int modelData
        text: modelData
    }
}
Repeater {
    model: ["one", "two", "three"]
    Text {
        required property string modelData
        text: modelData
    }
}

此类模型为每个委派的实例提供一个独立、匿名的数据项。访问此数据项是使用 modelData 的主要原因,但其他模型也提供 modelData

通过 model 角色提供的对象有一个空名的属性。这个匿名属性包含 modelData。此外,通过 model 角色提供的对象还有一个名为 modelData 的属性。此属性已弃用,也包含 modelData

除了 model 角色外,还提供了一个 modelData 角色。modelData 角色包含与 modelData 属性以及通过 model 角色提供的对象的匿名属性相同的数据。

model 角色和访问 modelData 的各种方式之间的区别如下

  • 没有命名角色(如整数或字符串数组)的模型通过 modelData 角色提供其数据。在这种情况下,modelData 角色不一定包含对象。在整数模型的例子中,它可能包含一个整数(当前模型项的索引)。在字符串数组的例子中,它可能包含一个字符串。《model》 角色仍然包含一个对象,但没有用于命名角色的属性。model 仍然包含其通常的 modelData 和匿名属性。
  • 如果模型只有一个命名角色,则 modelData 角色包含与命名角色相同的数据。它不一定是对象,并且不会如通常那样以命名属性的形式包含命名角色。model 角色仍然包含一个带有命名角色的对象,并在此情况下包含 modelData 和匿名属性。
  • 对于具有多个角色的模型,modelData 角色仅作为必需属性提供,而不是作为上下文属性。这是由于与较旧版本的 Qt 的向后兼容性。

model 上的匿名属性允许你干净地编写接收来自外部的模型数据和它们应响应的角色名称作为属性的委派。你可以提供一个带有或不带有唯一命名角色的模型,以及一个空字符串作为角色。在这种情况下,一个简单地访问 model[role] 的绑定将执行你预期的操作。你不需要为这种情况添加特殊代码。

注意:如果委派包含必需属性,则无法访问 modelindexmodelData 角色除非它也有具有匹配名称的必需属性。

QML 在 QML 类型集合中提供了多种数据模型。此外,可以使用 Qt C++ 创建这些模型,然后将其提供给 QQmlEngine 以供 QML 组件使用。有关创建这些模型的信息,请访问 在 Qt Quick 视图中使用 C++ 模型创建 QML 类型 文章。

可以使用 Repeater 位置模型中的项。

列表模型

ListModel 是在 QML 中指定的一个简单的类型层次结构。可用的角色由 ListElement 属性指定。

ListModel {
    id: fruitModel

    ListElement {
        name: "Apple"
        cost: 2.45
    }
    ListElement {
        name: "Orange"
        cost: 3.25
    }
    ListElement {
        name: "Banana"
        cost: 1.95
    }
}

上述模型有两个角色,namecost。它们可以通过例如一个 ListView 委派进行绑定。

ListView {
    anchors.fill: parent
    model: fruitModel
    delegate: Row {
        id: delegate
        required property string name
        required property real cost

        Text { text: "Fruit: " + delegate.name }
        Text { text: "Cost: $" + delegate.cost }
    }
}

ListModel 提供了直接通过 JavaScript 操作 ListModel 的方法。在这种情况下,第一个插入的项目决定了所有使用该模型的所有视图可用的角色。例如,如果通过 JavaScript 创建了一个空的 ListModel 并填充,则第一次插入提供的角色是唯一将显示在视图中的角色。

ListModel { id: fruitModel }
    ...
MouseArea {
    anchors.fill: parent
    onClicked: fruitModel.append({"cost": 5.95, "name":"Pizza"})
}

当点击 MouseArea 时,fruitModel 将有两个角色,costname。即使之后添加了更多的角色,也只处理前两个使用该模型的视图。要重置模型中可用的角色,请调用 ListModel::clear

XML 模型

XmlListModel 允许从一个 XML 数据源构建模型。通过 XmlListModelRole 类型指定角色。需要导入该类型。

import QtQml.XmlListModel

以下模型有三个角色,titlelinkpubDate

XmlListModel {
     id: feedModel
     source: "http://rss.news.yahoo.com/rss/oceania"
     query: "/rss/channel/item"
     XmlListModelRole { name: "title"; elementName: "title" }
     XmlListModelRole { name: "link"; elementName: "link" }
     XmlListModelRole { name: "pubDate"; elementName: "pubDate" }
}

query 属性指定 XmlListModel 为 XML 文档中的每个 <item> 生成一个模型项。

RSS News 示例 展示了如何使用 XmlListModel 来显示 RSS 源。

对象模型

ObjectModel 包含用于视图的可视项。当在视图中使用 ObjectModel 时,视图不需要代理,因为 ObjectModel 已经包含了可视代理(项)。

以下示例将三个彩色矩形放入一个 ListView 中。

import QtQuick 2.0
import QtQml.Models 2.1

Rectangle {
    ObjectModel {
        id: itemModel
        Rectangle { height: 30; width: 80; color: "red" }
        Rectangle { height: 30; width: 80; color: "green" }
        Rectangle { height: 30; width: 80; color: "blue" }
    }

    ListView {
        anchors.fill: parent
        model: itemModel
    }
}

整数作为模型

整数可以作为包含一定数量类型的模型使用。在这种情况下,模型没有任何数据角色。

以下示例创建了一个包含五个元素的 ListView

Item {
    width: 200; height: 250

    Component {
        id: itemDelegate

        Text {
            required property int index
            text: "I am item number: " + index
        }
    }

    ListView {
        anchors.fill: parent
        model: 5
        delegate: itemDelegate
    }

}

注意:整数模型中项的数目上限为 100,000,000。

对象实例作为模型

对象实例可以使用来指定一个具有单个对象类型的模型。对象属性作为角色提供。

以下示例创建了一个包含一个项目并列出 myText 文本的颜色的列表。注意使用完全限定的 model.color 属性以避免与代理中 Text 类型的 color 属性冲突。

Rectangle {
    width: 200; height: 250

    Text {
        id: myText
        text: "Hello"
        color: "#dd44ee"
    }

    Component {
        id: myDelegate

        Text {
            required property var model
            text: model.color
        }
    }

    ListView {
        anchors.fill: parent
        anchors.topMargin: 30
        model: myText
        delegate: myDelegate
    }
}

C++ 数据模型

可以在 C++ 中定义模型,然后将其提供给 QML。此机制对于将现有的 C++ 数据模型或其他复杂数据集公开给 QML 非常有用。

有关信息,请访问 使用 Qt Quick 视图与 C++ 模型 文章。

数组模型

可以使用 JavaScript 数组和各种类型的 QML 列表作为模型。列表的元素将按上述规则提供作为模型和模型数据:单个数据(如整数或字符串)作为单个模型数据提供。结构化数据(如 JavaScript 对象或 QObjects)作为结构化模型和模型数据提供。

如果需要,还可以根据需要获取单个模型角色。由于我们事先不知道数组中会出现哪些对象,因此代理中的任何所需属性都将被填充,可能需要将 undefined 强制转换为所需类型。不过,单个模型角色不会通过 QML 上下文提供。否则它将覆盖所有其他上下文属性。

重复器

重复器通过使用从模型中获取的数据,从模板创建项目以供位置器使用。将重复器和位置器组合起来是一种轻松布置大量项目的方法。将 Repeater 项目放置在位置器内,并由包围的位置器对这些项目进行排列。

每个重复器将根据在 model 属性中指定的模型中的每个元素组合一个项目,并在重复器内部定义的模板项目中定义,从而创建一定数量的项目。项目的总数量由模型中的数据量确定。

以下示例展示了使用 Grid 项目排列一系列 Rectangle 项目的重复器。重复器项目创建了一个包含 24 个矩形的系列,以 5x5 的排列放置。

import QtQuick

Rectangle {
    width: 400; height: 400; color: "black"

    Grid {
        x: 5; y: 5
        rows: 5; columns: 5; spacing: 10

        Repeater { model: 24
                   Rectangle { width: 70; height: 70
                               color: "lightgreen"

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

重复器创建的项目数量由它的 count 属性保存。无法设置此属性以确定创建的项目数量。相反,像上面示例中那样,我们使用整数作为模型。

更多详情,请参阅 QML 数据模型 文档。

如果模型是一个字符串列表,则代理也将公开访问通常的只读 modelData 属性,该属性保存字符串。

Column {
    Repeater {
        model: ["apples", "oranges", "pears"]
        Text {
            required property string modelData
            text: "Data: " + modelData
        }
    }
}

还可以将代理用作重复器创建的项目模板。这通过 delegate 属性指定。

修改模型数据

要修改模型数据,可以给 model 属性分配更新的值。默认情况下,QML ListModel 是可编辑的,而 C++ 模型必须实现 setData() 来变得可编辑。整数和 JavaScript 数组模型是只读的。

假设有一个基于 QAbstractItemModel 并实现 setData 方法的 C++ 模型已注册为名为 EditableModel 的 QML 类型。然后可以这样将数据写入模型

ListView {
    anchors.fill: parent
    model: EditableModel {}
    delegate: TextEdit {
        required property var model

        width: ListView.view.width
        height: 30
        text: model.edit
        Keys.onReturnPressed: model.edit = text
    }
}

注意: 属性的角色与 Qt::EditRole 相等。有关内置角色名称,请参阅 roleNames()。不过,现实生活中的模型通常会注册自定义角色。

注意: 如果模型角色绑定到一个 所需属性,将该属性分配给该属性将不会修改模型。相反,它将断开与模型的绑定(就像分配给任何其他属性会中断现有绑定一样)。如果您想要使用所需属性并更改模型数据,请将模型也作为一个所需属性并分配给 model.propertyName

更多详情,请访问 使用 C++ 模型与 Qt Quick 视图 文章。

使用转换

可以使用转换来对添加到、在内部移动或从位置器中删除的项目进行动画处理。

用于添加项的转换适用于作为位置器一部分创建的项目,以及被重新归父以成为位置器子项的项目。

用于删除项目的转换适用于被删除的位置器内部的项目,以及从位置器中移除并给予在文档中新的父项的项目。

注意:将项的透明度更改为零不会使它们从定位器中消失。可以通过更改可见性属性来删除并重新添加。

© 2024 Qt公司有限公司。本文件中包含的文档贡献归其各自的所有者所有。所提供文档的许可协议适用于Free Software Foundation发布的GNU自由文档许可证1.3版。Qt及其相关标志是芬兰及其它全球国家的Qt公司有限公司的商标。所有其他商标均为其各自所有者的财产。