《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)
}
}
}
}
- ListModel专门用于定义列表数据,内部维护一个
ListElement
的列表,一个ListElement
对象代表一个数据。 - 多个
role
构成一个ListElement
,role
包含一个名字和一个值,名字必须以小写字母开头,值必须是简单的常量(字符串、布尔值、数字、枚举值) - 在
ListElement
中定义的role
,可以在Delegate
中通过名称访问 Delegate
使用Row
管理Text
对象来展现role,Text
对象的text
属性对应于role
的名称- ListView的
delegate
属性类型是Component
,Component的顶层元素是Row,Row内嵌三个Text对象来展示Model定义的ListElement的三个role - ListView给delegate暴露一个
index
属性,代表当前delegate示例对应的Item的索引位置,必要时可通过它来访问数据 - ListView定义
delayRemove
、isCurrentItem
、nextSection
、previousSection
、section
、view
等附加属性,以及add
、remove
两个附加信号,可以在delegate中直接访问(只有delegate的顶层Item才能直接使用这些附加属性和信号,非顶层Item则需通过顶层Item的id来访问这些附加属性) - 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必须从QAbstractItemModel
或QAbstractListModel
继承实现
<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时,比如删除,就需要做如下动作(如删除):
- 调用基类的beginRemoveRows()
- 针对要删除的数据进行特定处理,如释放内存
- 调用基类的endRemoveRows()
11.4 TableView
TableView和ListView类似,多出了滚动条、挑选、可调整尺寸的表头等特性
TableView的数据也通过Model提供,可以使用ListModel、XmlListModel或使用C++从、QAbstractItemModel
和QAbstractTableModel
等继承来实现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.LeftToRight
和GridView.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
}
}
}
}
}