《qt quick核心编程》笔记四

11 Model/View

在这里插入图片描述

Delegate实际上可以看成是Item的一个模板

11.1 ListView

ListView用于显示一个条目列表,数据来自于Model,每个条目的外观来自于Delegate
要使用ListView必须指定一个Model、一个Delegate
Model可以是QML内建类型,如ListModel、XmlListModel,也可以是C++中实现的QAbstracItemModel或QAbstractListModel的派生类

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

Window {
	width: 560
	height: 300
	color: "#EEEEEE"
	visible: true

	Component {
		id: phoneModel
		ListModel {
			ListElement {
				name: "iPhone 3GS"
				cost: "1000"
				manufacturer: "Apple"
			}
			ListElement {
				name: "iPhone 4"
				cost: "1800"
				manufacturer: "Apple"
			}
			ListElement {
				name: "iPhone 4S"
				cost: "2300"
				manufacturer: "Apple"
			}
			ListElement {
				name: "iPhone 5"
				cost: "4900"
				manufacturer: "Apple"
			}
			ListElement {
				name: "B199"
				cost: "1590"
				manufacturer: "HuaWei"
			}
			ListElement {
				name: "MI 2S"
				cost: "1999"
				manufacturer: "XiaoMi"
			}
			ListElement {
				name: "GALAXY S5"
				cost: "4699"
				manufacturer: "Samsung"
			}
		}
	}

	Component {
		id: headview
		Item {
			width: parent.width
			height: 30
			RowLayout {
				anchors.verticalCenter: parent.verticalCenter

				Text {
					text: "Name"
					font.bold: true
					font.pixelSize: 20
					Layout.preferredWidth: 120
				}

				Text {
					text: "Cost"
					font.bold: true
					font.pixelSize: 20
					Layout.preferredWidth: 80
				}

				Text {
					text: "Manufacturer"
					font.bold: true
					font.pixelSize: 20
					Layout.fillWidth: true
				}
			}
		}
	}

	Component {
		id: footerView

		Item {
			signal clean
			signal add
			signal insert
			signal move
			property alias text: txt.text
			width: listView.width
			height: 30
			Text {
				id: txt
				anchors.left: parent.left
				color: "green"
				height: parent.height
				verticalAlignment: Text.AlignVCenter
			}

			Button {
				id: clearAll
				anchors.right: parent.right
				anchors.verticalCenter: parent.verticalCenter
				height: parent.height
				text: "clear"
				onClicked: {
					clean()
				}
			}

			Button {
				id: addOne
				anchors.right: clearAll.left
				anchors.rightMargin: 5
				anchors.verticalCenter: parent.verticalCenter
				height: parent.height
				text: "Add"
				onClicked: {
					add()
				}
			}

			Button {
				id: insertOne
				anchors.right: addOne.left
				anchors.rightMargin: 5
				anchors.verticalCenter: parent.verticalCenter
				height: parent.height
				text: "Insert"
				onClicked: {
					insert()
				}
			}

			Button {
				id: moveDown
				anchors.right: insertOne.left
				anchors.rightMargin: 5
				anchors.verticalCenter: parent.verticalCenter
				height: parent.height
				text: "MoveDown"
				onClicked: {
					move()
				}
			}
		}
	}

	Component {
		id: phoneDelegate
		Item {
			id: wrapper
			width: listView.width
			height: 30
			MouseArea {
				anchors.fill: parent
				onClicked: wrapper.ListView.view.currentIndex = index
				onDoubleClicked: wrapper.ListView.view.model.remove(index)
			}

			RowLayout {
				anchors.verticalCenter: parent.verticalCenter
				width: parent.width
				Text {
					id: col1
					text: name
					color: wrapper.ListView.isCurrentItem ? "red" : "black"
					font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
					Layout.preferredWidth: 120
				}
				Text {
					text: cost
					color: wrapper.ListView.isCurrentItem ? "red" : "black"
					font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
					Layout.preferredWidth: 80
				}
				Text {
					text: manufacturer
					color: wrapper.ListView.isCurrentItem ? "red" : "black"
					font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
					Layout.fillWidth: true
				}
			}
		}
	}
	ListView {
		id: listView
		anchors.fill: parent
		delegate: phoneDelegate
		header: headview
		footer: footerView
		model: phoneModel.createObject(listView)
		focus: true
		highlight: Rectangle {
			color: "lightblue"
		}

		add: Transition {
			ParallelAnimation {
				NumberAnimation {
					property: "opacity"
					from: 0
					to: 1.0
					duration: 1000
				}
				NumberAnimation {
					property: "y"
					from: 0
					duration: 1000
				}
			}
		}

		displaced: Transition {
			SpringAnimation {
				properties: "y"
				spring: 3
				damping: 0.1
				epsilon: 0.25
			}
		}

		remove: Transition {
			SequentialAnimation {
				NumberAnimation {
					properties: "y"
					to: 0
					duration: 600
				}

				NumberAnimation {
					properties: "opacity"
					to: 0
					duration: 400
				}
			}
		}

		move: Transition {
			NumberAnimation {
				properties: "y"
				duration: 300
				easing.type: Easing.InQuart
			}
		}

		populate: Transition {
			NumberAnimation {
				property: "opacity"
				from: 0
				to: 1.0
				duration: 3000
			}
		}

		Component.onCompleted: {
			footerItem.clean.connect(model.clear)
			footerItem.add.connect(addOne)
			footerItem.insert.connect(insertOne)
			footerItem.move.connect(moveDown)
		}

		onCurrentIndexChanged: {
			console.log("index:", currentIndex)
			if (currentIndex >= 0) {
				var data = listView.model.get(currentIndex)
				footerItem.text = "%1 -> %2 -> %3".arg(data.name).arg(
							data.cost).arg(data.manufacturer)
			} else {
				footerItem.text = ""
			}
		}

		function addOne() {
			console.log("add")
			model.append({
							 "name": "max3",
							 "cost": "1799",
							 "manufacturer": "samsung"
						 })
		}

		function insertOne() {
			console.log("insert")
			model.insert(Math.round(Math.random() * model.count), {
							 "name": "HTC One E8",
							 "cost": "2499",
							 "manufacturer": "htc"
						 })
		}

		function moveDown() {
			console.log("moveDown:", currentIndex)
			if (currentIndex + 1 < model.count) {
				model.move(currentIndex, currentIndex + 1, 1)
			}
		}
	}
}
  1. ListModel专门用于定义列表数据,内部维护一个ListElement的列表,一个ListElement对象代表一个数据。
  2. 多个role构成一个ListElementrole包含一个名字和一个值,名字必须以小写字母开头,值必须是简单的常量(字符串、布尔值、数字、枚举值)
  3. ListElement中定义的role,可以在Delegate中通过名称访问
  4. Delegate使用Row管理Text对象来展现role,Text对象的text属性对应于role的名称
  5. ListView的delegate属性类型是Component,Component的顶层元素是Row,Row内嵌三个Text对象来展示Model定义的ListElement的三个role
  6. ListView给delegate暴露一个index属性,代表当前delegate示例对应的Item的索引位置,必要时可通过它来访问数据
  7. ListView定义delayRemoveisCurrentItemnextSectionpreviousSectionsectionview等附加属性,以及addremove两个附加信号,可以在delegate中直接访问(只有delegate的顶层Item才能直接使用这些附加属性和信号,非顶层Item则需通过顶层Item的id来访问这些附加属性)
  8. ListView的highlight属性可以指定一个Component对象,它的Z序小于delegate实例化出来的Item对象。highlightFollowsCurrentItem属性指定高亮背景是否跟随当前条目,当前条目变动时,高亮背景经过一个平滑的动画效果进行过渡
11.1.1 ListModel访问数据

count属性代表Model中有多少条数据
dynamicRoles属性为true时,则Model中的roles对应值的类型可以动态改变,但是性能将会严重下降,默认false。要使用它必须在添加数据之前
get(int)方法用于获取指定索引位置的数据,返回一个QML对象

var data = listView.model.get(listView.currentIndex);
listView.footerItem.text = data.name + ", " + data.cost + ", " + data.manufacturer;
11.1.2 ListModel删除数据

remove(int index, int count)方法用于删除数据

  • index指明删除数据的索引位置
  • count指示要删除的数据条数,默认为1
11.1.3 ListModel修改数据

setProperty(int index, string property, variant value)方法用于修改数据

listView.model.setProperty(5, "cost", 16999);
  • index指明修改数据的索引位置
  • property数据内role的名字
  • value要修改成的值

set(int index, jsobject dict)方法用于替换数据

listView.model.set(0, {"name": "Z5S mini", "cost": 1999, "manufacturer": "ZhongXing"});
  • index指明要替换数据的索引位置
  • dict要替换成的数据
11.1.4 ListModel添加数据

append(jsobject dict)用于在末尾添加一条数据
insert(int index, jsobject value)用于在指定位置添加一条数据

listView.model.append(
	{
		"name" : "MX3",
		"cost" : "1799",
		"manufacturer": "MeiZu"
	}
)
11.1.5 ListModel移动数据

move(int indexSrc, int indexDst, int count)用于将一条数据移动到指定的位置

  • indexSrc要移动的数据索引
  • indexDst移动到的目标位置索引
  • count移动的条数
11.1.6 section列表分组
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

Window {
	width: 560
	height: 300
	color: "#EEEEEE"
	visible: true

	Component {
		id: phoneModel
		ListModel {
			// Apple section
			ListElement {
				name: "iPhone 5"
				cost: "4900"
				manufacturer: "Apple"
			}
			ListElement {
				name: "iPhone 3GS"
				cost: "1000"
				manufacturer: "Apple"
			}
			ListElement {
				name: "iPhone 4"
				cost: "1800"
				manufacturer: "Apple"
			}
			ListElement {
				name: "iPhone 4S"
				cost: "2300"
				manufacturer: "Apple"
			}
			//HuaWei section
			ListElement {
				name: "B199"
				cost: "1590"
				manufacturer: "HuaWei"
			}

			// Sumsung section
			ListElement {
				name: "GALAXY S4"
				cost: "3099"
				manufacturer: "Samsung"
			}
			ListElement {
				name: "C8816D"
				cost: "590"
				manufacturer: "HuaWei"
			}
			ListElement {
				name: "GALAXY S5"
				cost: "4699"
				manufacturer: "Samsung"
			}
			// XiaoMi section
			ListElement {
				name: "MI 2S"
				cost: "1999"
				manufacturer: "XiaoMi"
			}
			ListElement {
				name: "MI 3"
				cost: "1999"
				manufacturer: "XiaoMi"
			}
		}
	}
	Component {
		id: phoneDelegate
		Item {
			id: wrapper
			width: parent.width
			height: 30
			ListView.onAdd: {
				console.log("count:", ListView.view.count)
			}
			MouseArea {
				anchors.fill: parent
				onClicked: wrapper.ListView.view.currentIndex = index
			}
			RowLayout {
				anchors.left: parent.left
				anchors.verticalCenter: parent.verticalCenter
				spacing: 8
				Text {
					id: col1
					text: name
					color: wrapper.ListView.isCurrentItem ? "red" : "black"
					font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
					Layout.preferredWidth: 120
				}
				Text {
					text: cost
					color: wrapper.ListView.isCurrentItem ? "green" : "black"
					font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
					Layout.preferredWidth: 80
				}
				Text {
					text: manufacturer
					color: wrapper.ListView.isCurrentItem ? "red" : "black"
					font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
					Layout.fillWidth: true
				}
			}
		}
	}
	Component {
		id: sectionHeader
		Rectangle {
			width: parent.width
			height: childrenRect.height
			color: "lightsteelblue"
			Text {
				text: section
				font.bold: true
				font.pixelSize: 20
			}
		}
	}
	ListView {
		id: listView
		anchors.fill: parent
		delegate: phoneDelegate
		model: phoneModel.createObject(listView)
		focus: true
		highlight: Rectangle {
			color: "lightblue"
		}
		section.property: "manufacturer"
		section.criteria: ViewSection.FullString
		section.delegate: sectionHeader
	}
}

section.property:分组的依据,对应于数据的role-name
section.criteria:指定section.property的判断条件

  • ViewSection.FullString(默认,全串匹配,不区分大小写)
  • ViewSection.Firstcharacter(首字母匹配,不区分大小写)

section.delegate:设定一个Component决定如何显示每个section
section.labelPositioning:决定当前或下一个section标签的显示策略

  • ViewSection.InlineLabels,这是默认方式。分组标签嵌入到Item之间显示。
  • ViewSection.CurrentLabelAtStart,当view移动时,当前分组的标签附着在view的开始。
  • ViewSection.NextLabelAtEnd,当view移动时,下一个分组标签附着在view的尾端。

11.2 XmlListModel

XmlListModel用于从XML数据中直接创建一个只读的model,可以用作其他view元素的数据源
XmlListModel使用XPath表达式来提取XML文档中的数据

<videos.xml>

<videos>
    <video name='冰雪奇缘' date='2013-11-19' >
        <attr tag='导演'>詹妮弗·李</attr>
        <attr tag='演员'>伊迪娜·门泽尔/克里斯汀·贝尔</attr>
        <attr tag='评分'>9.2</attr>
        <attr tag='简介'>在四面环海、风景如画的阿伦达王国,生活着两位可爱美丽的小公主,艾莎和安娜。艾莎天生具有制造冰雪的能...</attr>
        <poster img='http://g3.ykimg.com/0516000052D779CD67583960490A8E1A' />
        <page link='http://v.youku.com/v_show/id_XNjk1ODc2NDMy.html' />
        <playtimes>12184709</playtimes>
    </video>
    <video name='功夫' date='2004-12-23' >
        <attr tag='导演'>周星驰</attr>
        <attr tag='演员'>周星驰/元秋/元华/林子聪/梁小龙/陈国坤</attr>
        <attr tag='评分'>7.0</attr>
        <attr tag='简介'>1940年代的上海,自小受尽欺辱的街头混混阿星(周星驰 饰)为了能出人头地,可谓窥见机会的缝隙就往...</attr>
        <poster img='http://g1.ykimg.com/0516000051BAD11A67583912FF0277C1' />
        <page link='http://v.qq.com/cover/u/uiq0rxuywu508qr.html' />
        <playtimes>4012749</playtimes>
    </video>
    <video name='西游·降魔篇' date='2013-02-10'>
        <attr tag='导演'>周星驰</attr>
        <attr tag='演员'>舒淇/文章/黄渤/李尚正/陈炳强/周秀娜</attr>
        <attr tag='评分'>8.1</attr>
        <attr tag='简介'>大唐年间妖魔横行,一小渔村因为饱受鱼妖之害请来道士(冯勉恒 饰)除妖,年轻驱魔人陈玄奘(文章 饰)...</attr>
        <poster img='http://g2.ykimg.com/0516000051B436EB67583928E30DCCDD' />
        <page link='http://v.youku.com/v_show/id_XNTI2Mzg4NjAw.html' />
        <playtimes>25421498</playtimes>
    </video>
    <video name='小时代' date='2013-06-27' >
        <attr tag='导演'>郭敬明</attr>
        <attr tag='演员'>杨幂/郭采洁/郭碧婷/谢依霖/柯震东/凤小岳</attr>
        <attr tag='评分'>8.9</attr>
        <attr tag='简介'>这是一个梦想闪耀的时代,一个理想冷却的时代;这是最坏的时代,这也是最好的时代,这是我们的小时代。在...</attr>
        <poster img='http://g1.ykimg.com/0516000051F22C1C67583931E8015597' />
        <page link='http://v.youku.com/v_show/id_XNTg3NjkzMzIw.html' />
        <playtimes>99075808</playtimes>
    </video>
    <video name='倩女幽魂' date='1987-07-18'>
        <attr tag='导演'>程小东</attr>
        <attr tag='演员'>张国荣/王祖贤/午马</attr>
        <attr tag='评分'>8.1</attr>
        <attr tag='简介'>书生宁采臣(张国荣 饰)收账不成,无处可归,遂夜宿鬼寺兰若寺,遇上侠士燕赤霞(午马 饰),二人成为...</attr>
        <poster img='http://g2.ykimg.com/051600004FC32F0797927377D9052FBF' />
        <page link='http://v.youku.com/v_show/id_XMjE0ODk3MjUy.html' />
        <playtimes>1579516</playtimes>
    </video>
    <video name='那些年,我们一起追的女孩' date='2011-08-19' >
        <attr tag='导演'>九把刀</attr>
        <attr tag='演员'>柯震东/陈妍希/郝邵文</attr>
        <attr tag='评分'>8.5</attr>
        <attr tag='简介'>青春是一场大雨。即使感冒了,还盼望回头再淋它一次。人生就是不停的战斗,在还没有获得女神青睐时,左手...</attr>
        <poster img='http://g3.ykimg.com/05160000531420D26758391C5C08485A' />
        <page link='http://v.qq.com/cover/t/tu0bpgju3a1xno6.html' />
        <playtimes>3807121</playtimes>
    </video>
</videos>

<main.qml>

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.XmlListModel 2.15

Window {
	width: 560
	height: 300
	color: "#EEEEEE"
	visible: true

	Component {
		id: videoModel
		XmlListModel {
			source: "videos.xml"
			id: xmlModel
			query: "/videos/video"
			XmlRole {
				name: "name"
				query: "@name/string()"
			}
			XmlRole {
				name: "date"
				query: "@date/string()"
			}
			XmlRole {
				name: "img"
				query: "poster/@img/string()"
			}
			XmlRole {
				name: "director_tag"
				query: "attr[1]/@tag/string()"
			}
			XmlRole {
				name: "director "
				query: "attr[1]/string()"
			}
			XmlRole {
				name: "actor_tag"
				query: "attr[2]/@tag/string()"
			}
			XmlRole {
				name: "actor"
				query: "attr[2]/string()
"
			}
			XmlRole {
				name: "rating"
				query: "attr[3]/number()"
			}
			XmlRole {
				name: "desc"
				query: "attr[4]/string()"
			}
			XmlRole {
				name: "playtimes"
				query: "playtimes/number()"
			}
		}
	}
	Component {
		id: videoDelegate
		Item {
			id: wrapper
			width: listView.width
			height: 120
			MouseArea {
				anchors.fill: parent
				onClicked: wrapper.ListView.view.currentIndex = index
			}
			Image {
				id: poster
				anchors.left: parent.left
				anchors.top: parent.top
				source: img
				width: 80
				height: 120
				fillMode: Image.PreserveAspectFit
			}
			ColumnLayout {
				anchors.left: poster.right
				anchors.leftMargin: 4
				anchors.right: wrapper.right
				anchors.top: poster.top
				height: parent.height
				spacing: 2
				Text {
					Layout.fillWidth: true
					text: "<b>" + name + "</b>(" + rating + "," + playtimes + ")"
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: date
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: director_tag + ": <font color=\"#0000aa\">" + director + "</font>"
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: actor_tag + " : <font color=\"#0000aa\"> " + actor + "</font>"
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: desc
					Layout.fillHeight: true
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 16
					wrapMode: Text.Wrap
					maximumLineCount: 2
					elide: Text.ElideRight
				}
			}
		}
	}
	ListView {
		id: listView
		anchors.fill: parent
		spacing: 4
		delegate: videoDelegate
		model: videoModel.createObject(listView)
		focus: true
		highlight: Rectangle {
			width: parent.width
			color: "lightblue"
		}
	}
}

source:指定XmlListModel使用的XML文档的位置
xml:指定作为model数据源头的XML字符串,utf-8编码,优先生效
query:一个XPath表达式,以"/“或”//"开始,和XmlRole的query结合使用
roles:XmlRole对象的列表,XmlListModel通过它来从XML文档中提取数据
count:当前model内数据的个数
namespaceDeclarations:保存在XPath中使用的命名空间
status:model的当前状态

  • XmlListModel.Null
  • XmlListModel.Ready
  • XmlListModel.Loading
  • XmlListModel.Error

progress:表示当前XML文档的下载进度,real类型,从0.0到1.0
get():得到索引位置的数据对象,然后可以根据role-name访问数据
reload():重新加载model,可以通过指定关键角色来只更新和关键角色匹配的数据

11.3 使用C++ Model

ListView可以使用C++中定义的Model,XmlListModel就是C++实现(QQuickXmlListModel)
C++实现Model必须从QAbstractItemModelQAbstractListModel继承实现
<videoListModel.h>

#ifndef VIDEOLISTMODEL_H
#define VIDEOLISTMODEL_H
#include <QAbstractListModel>
class VideoListModelPrivate;
class VideoListModel : public QAbstractListModel {
  Q_OBJECT
  Q_PROPERTY(QString source READ source WRITE setSource)
public:
  VideoListModel(QObject *parent = 0);
  ~VideoListModel();
  int rowCount(const QModelIndex &parent) const;
  QVariant data(const QModelIndex &index, int role) const;
  QHash<int, QByteArray> roleNames() const;
  QString source() const;
  void setSource(const QString &filePath);
  Q_INVOKABLE QString errorString() const;
  Q_INVOKABLE bool hasError() const;
  Q_INVOKABLE void reload();
  Q_INVOKABLE void remove(int index);

private:
  VideoListModelPrivate *m_dptr;
};
#endif

<videoListModel.cpp>

#include "videoListModel.h"
#include <QDebug>
#include <QFile>
#include <QVector>
#include <QXmlStreamReader>
typedef QVector<QString> VideoData;
class VideoListModelPrivate {
public:
  VideoListModelPrivate() : m_bError(false) {
	int role = Qt::UserRole;
	m_roleNames.insert(role++, "name");
	m_roleNames.insert(role++, "date");
	m_roleNames.insert(role++, "director_tag");
	m_roleNames.insert(role++, "director");
	m_roleNames.insert(role++, "actor_tag");
	m_roleNames.insert(role++, "actor");
	m_roleNames.insert(role++, "rating_tag");
	m_roleNames.insert(role++, "rating");
	m_roleNames.insert(role++, "desc_tag");
	m_roleNames.insert(role++, "desc");
	m_roleNames.insert(role++, "img");
	m_roleNames.insert(role++, "playpage");
	m_roleNames.insert(role++, "playtimes");
  }

  ~VideoListModelPrivate() { clear(); }

  void load() {
	QXmlStreamReader reader;
	QFile file(m_strXmlFile);
	if (!file.exists()) {
	  m_bError = true;
	  m_strError = "File Not Found!";
	  return;
	}
	if (!file.open(QFile::ReadOnly)) {
	  m_bError = true;
	  m_strError = file.errorString();
	  return;
	}
	reader.setDevice(&file);
	QStringRef elementName;
	VideoData *video;
	while (!reader.atEnd()) {
	  reader.readNext();
	  if (reader.isStartElement()) {
		elementName = reader.name();
		if (elementName == "video") {
		  video = new VideoData();
		  QXmlStreamAttributes attrs = reader.attributes();
		  video->append(attrs.value("name").toString());
		  video->append(attrs.value("date").toString());
		} else if (elementName == "attr") {
		  video->append(reader.attributes().value("tag").toString());
		  video->append(reader.readElementText());
		} else if (elementName == "poster") {
		  video->append(reader.attributes().value("img").toString());
		} else if (elementName == "page") {
		  video->append(reader.attributes().value("link").toString());
		} else if (elementName == "playtimes") {
		  video->append(reader.readElementText());
		}
	  } else if (reader.isEndElement()) {
		elementName = reader.name();
		if (elementName == "video") {
		  m_videos.append(video);
		  video = 0;
		}
	  }
	}
	file.close();
	if (reader.hasError()) {
	  m_bError = true;
	  m_strError = reader.errorString();
	}
  }

  void reset() {
	m_bError = false;
	m_strError.clear();
	clear();
  }

  void clear() {
	int count = m_videos.size();
	if (count > 0) {
	  for (int i = 0; i < count; i++) {
		delete m_videos.at(i);
	  }
	  m_videos.clear();
	}
  }

  QString m_strXmlFile;
  QString m_strError;
  bool m_bError;
  QHash<int, QByteArray> m_roleNames;
  QVector<VideoData *> m_videos;
};

VideoListModel::VideoListModel(QObject *parent)
	: QAbstractListModel(parent), m_dptr(new VideoListModelPrivate) {}
VideoListModel::~VideoListModel() { delete m_dptr; }
int VideoListModel::rowCount(const QModelIndex &parent) const {
  return m_dptr->m_videos.size();
}
QVariant VideoListModel::data(const QModelIndex &index, int role) const {
  VideoData *d = m_dptr->m_videos[index.row()];
  return d->at(role - Qt::UserRole);
}
QHash<int, QByteArray> VideoListModel::roleNames() const {
  return m_dptr->m_roleNames;
}
QString VideoListModel::source() const { return m_dptr->m_strXmlFile; }
void VideoListModel::setSource(const QString &filePath) {
  m_dptr->m_strXmlFile = filePath;
  reload();
  if (m_dptr->m_bError) {
	qDebug() << " VideoListModel,error - " << m_dptr->m_strError;
  }
}
QString VideoListModel::errorString() const { return m_dptr->m_strError; }
bool VideoListModel::hasError() const { return m_dptr->m_bError; }
void VideoListModel::reload() {
  beginResetModel();
  m_dptr->reset();
  m_dptr->load();
  endResetModel();
}
void VideoListModel::remove(int index) {
  beginRemoveRows(QModelIndex(), index, index);
  delete m_dptr->m_videos.takeAt(index);
  endRemoveRows();
}

<main.cpp>

#include "videoListModel.h"
#include <QApplication>
#include <QColor>
#include <QQmlApplicationEngine>
#include <QtQml>

int main(int argc, char *argv[]) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
  QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
  QApplication app(argc, argv);
  qmlRegisterType<VideoListModel>("an.qt.CModel", 1, 0, "VideoListModel");

  QQmlApplicationEngine engine;
  engine.load("qrc:/main.qml");
  return app.exec();
}
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import an.qt.CModel 1.0

Window {
	width: 560
	height: 300
	color: "#EEEEEE"
	visible: true

	Component {
		id: videoDelegate
		Item {
			id: wrapper
			width: listView.width
			height: 120
			MouseArea {
				anchors.fill: parent
				onClicked: wrapper.ListView.view.currentIndex = index
			}
			Image {
				id: poster
				anchors.left: parent.left
				anchors.top: parent.top
				source: img
				width: 80
				height: 120
				fillMode: Image.PreserveAspectFit
			}
			ColumnLayout {
				anchors.left: poster.right
				anchors.leftMargin: 4
				anchors.right: wrapper.right
				anchors.top: poster.top
				height: parent.height
				spacing: 2
				Text {
					Layout.fillWidth: true
					text: "<b>" + name + "</b>(" + rating + "," + playtimes + ")"
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: date
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: director_tag + ": <font color=\"#0000aa\">" + director + "</font>"
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: actor_tag + " : <font color=\"#0000aa\"> " + actor + "</font>"
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 18
					elide: Text.ElideRight
				}
				Text {
					text: desc
					Layout.fillHeight: true
					Layout.fillWidth: true
					color: wrapper.ListView.isCurrentItem ? "blue" : "black"
					font.pixelSize: 16
					wrapMode: Text.Wrap
					maximumLineCount: 2
					elide: Text.ElideRight
				}
			}
		}
	}
	ListView {
		id: listView
		anchors.fill: parent
		spacing: 4
		delegate: videoDelegate
		model: VideoListModel {
			source: ".\\videos.xml"
		}
		focus: true
		highlight: Rectangle {
			width: parent.width
			color: "lightblue"
		}
	}
}

当允许在QML中修改C++实现的Model时,比如删除,就需要做如下动作(如删除):

  1. 调用基类的beginRemoveRows()
  2. 针对要删除的数据进行特定处理,如释放内存
  3. 调用基类的endRemoveRows()

11.4 TableView

TableView和ListView类似,多出了滚动条、挑选、可调整尺寸的表头等特性
TableView的数据也通过Model提供,可以使用ListModel、XmlListModel或使用C++从、QAbstractItemModelQAbstractTableModel等继承来实现Model

11.5 GridView

GridView和ListView类似,不同在于Item的呈现方式

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.XmlListModel 2.15

Window {
	width: 480
	height: 400
	visible: true

	Component {
		id: videoModel
		XmlListModel {
			source: "videos.xml"
			id: xmlModel
			query: "/videos/video"
			XmlRole {
				name: "name"
				query: "@name/string()"
			}
			XmlRole {
				name: "img"
				query: "poster/@img/string()"
			}
			XmlRole {
				name: "rating"
				query: "attr[3]/number()"
			}
		}
	}
	Component {
		id: videoDelegate
		Item {
			id: wrapper
			width: videoView.cellWidth
			height: videoView.cellHeight
			MouseArea {
				anchors.fill: parent
				onClicked: wrapper.GridView.view.currentIndex = index
			}
			Image {
				id: poster
				anchors.horizontalCenter: parent.horizontalCenter
				anchors.top: parent.top
				anchors.topMargin: 3
				source: img
				width: 100
				height: 150
				fillMode: Image.PreserveAspectFit
			}
			Text {
				anchors.top: poster.bottom
				anchors.topMargin: 4
				width: parent.width
				text: name
				color: wrapper.GridView.isCurrentItem ? "blue" : "black"
				font.pixelSize: 18
				horizontalAlignment: Text.AlignHCenter
				elide: Text.ElideMiddle
			}
		}
	}
	GridView {
		id: videoView
		anchors.fill: parent
		cellWidth: 120
		cellHeight: 190
		delegate: videoDelegate
		model: videoModel.createObject(videoView)
		focus: true
		highlight: Rectangle {
			height: videoView.cellHeight - 8
			color: "lightblue"
		}
	}
}

flow:指定Item的流模式,GridView.LeftToRightGridView.TopToBottom
cellWidth:单元格宽度
cellHeight:单元格高度

11.6 Repeater

Repeater用于创建多个基于Item的组件,丢给它的父(通常是定位器或布局管理器)来管理
count:指定要创建多少个基于Item的对象
model:指定数据类型,数字、字符串列表、对象列表、ListModel等常见的model
delegate:待实例化的组件,默认属性,定义时通常不显示初始化
itemAt(index):根据索引返回对应的delegate实例

11.6.1 model为数字
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.XmlListModel 2.15

Window {
	width: 480
	height: 400
	visible: true

	RowLayout {
		anchors.fill: parent
		spacing: 4
		Repeater {
			model: 8
			Rectangle {
				width: 46
				height: 30
				color: "steelblue"
				Text {
					anchors.fill: parent
					color: "black"
					font.pointSize: 14
					verticalAlignment: Text.AlignVCenter
					horizontalAlignment: Text.AlignHCenter
					text: index
				}
			}
		}
	}
}
11.6.2 model为字符串列表
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.XmlListModel 2.15

Window {
	width: 480
	height: 400
	visible: true

	Row {
		anchors.centerIn: parent
		spacing: 8
		Repeater {
			model: ["Hello", "Qt", "Quick"]
			Text {
				color: "blue"
				font.pointSize: 18
				font.bold: true
				verticalAlignment: Text.AlignVCenter
				horizontalAlignment: Text.AlignHCenter
				text: modelData
			}
		}
	}
}
11.6.3 model为对象列表
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.XmlListModel 2.15

Window {
	width: 480
	height: 400
	visible: true

	Column {
		anchors.fill: parent
		anchors.margins: 4
		spacing: 4
		Repeater {
			model: [{
					"name": "Zhang San",
					"mobile": "13888888888
"
				}, {
					"name": "Wang Er",
					"mobile": "13999999999
"
				}, {
					"name": "Liu Wu",
					"mobile": "15866666666"
				}]
			Row {
				height: 30
				Text {
					width: 100
					color: "blue"
					font.pointSize: 13
					font.bold: true
					verticalAlignment: Text.AlignVCenter
					text: modelData.name
				}
				Text {
					width: 200
					font.pointSize: 13
					verticalAlignment: Text.AlignVCenter
					text: modelData.mobile
				}
			}
		}
	}
}
11.6.4 model为ListModel
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.XmlListModel 2.15

Window {
	width: 480
	height: 400
	visible: true

	Column {
		anchors.fill: parent
		anchors.margins: 4
		spacing: 4
		Repeater {
			model: ListModel {
				ListElement {
					name: "MI4"
					cost: "1999"
					manufacturer: "Xiaomi"
				}
				ListElement {
					name: "MX4"
					cost: "1999"
					manufacturer: "Meizu"
				}
				ListElement {
					name: "iPhone6"
					cost: "5500"
					manufacturer: "Apple"
				}
				ListElement {
					name: "C199"
					cost: "1599"
					manufacturer: "Huawei"
				}
			}
			Row {
				height: 30
				Text {
					width: 120
					color: "blue"
					font.pointSize: 14
					font.bold: true
					verticalAlignment: Text.AlignVCenter
					text: name
				}
				Text {
					width: 100
					font.pointSize: 14
					verticalAlignment: Text.AlignVCenter
					text: cost
				}
				Text {
					width: 100
					font.pointSize: 12
					verticalAlignment: Text.AlignVCenter
					text: manufacturer
				}
			}
		}
	}
}