Qt Quick中的模型和视图#

如何在Qt Quick中显示和格式化数据

大多数应用都需要格式化数据并显示。Qt Quick拥有模型(models)、视图(views)和代理(delegates)的概念来显示数据。它们将数据的可视化模块化,以便开发者或设计师可以控制数据的各个方面。开发者可以通过对数据进行较小的修改,用列表视图替换网格视图。同样,通过在代理中封装数据的实例,开发者可以决定如何表示或处理数据。

../_images/modelview-overview.png
  • 模型 - 包含数据及其结构。创建模型提供了几种QML类型。

  • 视图 - 一个用于显示数据的容器。视图可能以列表或网格的形式显示数据。

  • 代理 - 决定数据在视图中应如何显示。代理接收模型中的每个数据单元并对其进行封装。数据可通过代理访问。代理还可以将数据写回可编辑的模型(如在TextField的onAccepted处理器中)。

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

使用视图显示数据#

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

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

  • ListView - 以水平或垂直列表排列项

  • GridView - 在可用空间内以网格形式排列项

  • PathView - 在路径上排列项

  • TableView - 以表格形式排列来自QAbstractTableModel的数据

  • TreeView - 以树形结构排列来自QAbstractItemModel的数据

这些类型具有自身特有的属性和行为。访问它们各自的文档获取更多信息。

此外,Qt Quick Controls包含一些额外的视图和代理,它们的样式与应用程序的风格一致,例如HorizontalHeaderView和VerticalHeaderView。

装饰视图#

视图通过如headerfootersection等装饰属性允许视觉自定义。通过将这些属性绑定到一个对象(通常是另一个可视化对象),可以将视图装饰化。页脚可能包括一个Rectangle类型以显示边框,或顶部显示标识的页眉。

假设某个特定的俱乐部希望用其品牌颜色装饰其成员列表。一个成员列表在model中,而delegate将显示模型的内容。

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

俱乐部可以通过将视觉对象绑定到headerfooter属性上来装饰成员列表。视觉对象可以就地定义,在另一个文件中定义,或者在组件类型中定义。

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"}
}
../_images/listview-decorations.png

鼠标和触摸处理#

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

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

ListView部分#

ListView内容可以组合为部分,其中相关列表项将根据其部分进行标记。此外,部分还可以用'代表'进行装饰。

一个列表可以包含一个列出人们姓名和属于哪个团队的人员的列表。

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的附加属性,可以将相邻和相关的类型组合成一个部分。该section.property确定要使用哪个列表类型属性作为部分。该section.criteria可以规定部分名称的显示方式,而该section.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
            }
        }
    }
}
../_images/listview-section.png

视图代表#

视图需要一个代表来在列表中视觉上表示一个项目。视图将根据代表的模板视觉化每个列表项目。模型中的项可以通过index属性以及项的属性进行访问。

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

        required property int index
        required property string type
    }
}
../_images/listview-setup.png

从代表访问视图和模型#

与代表绑定的是列表视图,可以通过代表的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
    }
}

模型#

数据通过命名数据角色提供给代理,代理可以将这些角色绑定。以下是一个具有两个角色 type 和 age 的 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 类型具有(非必需的)type 或 age 属性,上面示例中的文本将显示这些属性的值而不是模型项的 type 和 age 值。在这种情况下,可以像 model.typemodel.age 一样引用这些属性以确保代理显示模型项的属性值。为了使这生效,您需要在代理中要求一个 model 属性(除非您正在使用上下文属性)。

一个特殊的 index 角色也包含模型中项的索引,可用于代理。请注意,如果项从模型中删除,则此索引设置为 -1。如果绑定到索引角色,请确保逻辑考虑到索引可能为 -1 的可能性,即项不再有效。(通常项将很快被销毁,但在某些视图中可以通过一个 delayRemove附加属性来延迟代理的销毁。)

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

此类模型为代理的每个实例提供单一、匿名的数据片段。访问该数据片段是使用 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组件使用。有关创建这些模型的信息,请参阅使用C++模型与Qt Quick视图和创建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

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

RSS 新闻演示展示了如何使用 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

注意

整数模型中项的数目限制为 100,000,000。

对象实例作为模型#

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

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

C++ 数据模型#

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

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

数组模型#

您可以使用 JavaScript 数组以及各种类型的 QML 列表作为模型。根据上述规则,列表元素将作为模型和模型数据提供:整数或字符串等单一数据将作为单一模型数据提供。JavaScript 对象或 QObjects 等结构化数据将作为结构化模型和模型数据提供。

如果您按需请求,也会提供单个模型角色。由于我们无法事先知道数组中会出现什么对象,因此任何代理中所需属性都将被填充,可能需要对 undefined 进行类型强制。尽管如此,QML 上下文中并不会提供单个模型角色。它们将覆盖所有其他上下文属性。

重复器#

repeater-index1

重复器使用来自模型的日期创建项以与其他定位器一起使用。结合重复器和定位器是一种轻松布置大量项目的方法。将 Repeater 项目放入定位器中,并为定位器生成要定位的项目。

每个重复器通过将使用 model 属性指定的模型中的每个元素数据与模板项(在重复器内部定义为子项)结合来创建一定数量的项目。总项目数量由模型中的数据量确定。

以下示例显示了与 Grid 项目一起使用重复器来排列一系列 Rectangle 项。重复器项目为 Grid 项生成一系列 24 个矩形,使 Grid 项目能够将其按 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 } }
        }
    }
}

Repeater创建的项目数量由其count属性控制。无法设置此属性以确定要创建的项目数量。相反,如上例所示,我们使用整数作为模型。

有关更多详细信息,请参阅QML数据模型文档。

如果模型是字符串列表,委托也公开了通常为只读的modelData属性,该属性包含字符串。例如

Column {
    Repeater {
        model: ["apples", "oranges", "pears"]
        Text {
            required property string modelData
            text: "Data: " + modelData
        }
    }
}
../_images/repeater-modeldata.png

还可以使用委托作为Repeater创建的项目模板。这通过delegate属性指定。

更改模型数据

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

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

注意

edit角色等于Qt::EditRole。有关内置角色名称,请参阅roleNames()。然而,现实世界的模型通常会注册自定义角色。

注意

如果模型角色绑定到必需的属性,则分配给该属性将不会修改模型。它将相反打破与模型的绑定(就像分配给任何其他属性一样打破现有绑定)。如果想要使用必需的属性并更改模型数据,请使模型也成为一个必需的属性,并分配给model.propertyName

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

使用过渡#

过渡可用于对添加到、在位置器内移动或从位置器中删除的项目进行动画处理。

添加任务的过渡适用于作为位置器一部分创建的项目,以及那些重新父化以成为位置器孩子的项目。

删除任务的过渡适用于位置器内的项目,这些项目被删除,以及那些从位置器中删除并被分配新父项的项目。

注意

将项目的透明度更改为零不会导致它们从位置器中消失。可以通过更改visible属性来删除并重新添加它们。