logo头像

单枪匹马亦对饮,历经磨难记初心

玩转Qml(9)-Model和View

本文于 535 天之前发表,文中内容可能已经过时。

gallery-img

简介

本文是《玩转Qml》系列文章的第九篇,涛哥将教大家,Qml中Model和View的知识。

源码

《玩转Qml》系列文章,配套了一个优秀的开源项目:TaoQuick

github https://github.com/jaredtao/TaoQuick

访问不了或者速度太慢,可以用国内的镜像网站gitee

https://gitee.com/jaredtao/TaoQuick

界面、数据和逻辑分离

界面架构的理念发展的非常快,主要在Web技术的驱动下,就有这么多架构:

MVC、MVP、 MVVM、 Flux、Redux。

架构太多太复杂,只要抓住一些关键点就够了:界面、数据和逻辑要分别处理,最终要能够正确处理用户输入并显示结果。

先来看一下Qt中提供的架构:

预览

Model代表数据,View代表界面,这个Delegate嘛,就是用来定制View的显示方式和Controll的调用,也应该算进View里面去。

这样看来Qt是M-V架构 ? 其实Qt算是MVC架构,这个Controll一般是自己实现的,和Model放在一起的。

不过Qt有信号/槽机制,在QtQuick中以属性绑定的方式出现。信号/槽相当于Gof设计模式中的观察者模式,也相当于Flux中的订阅/发布模式。

涛哥按自己的实践和理解,画了一个Qt的Model-View架构草图:

预览

Qt内置的Model-View

View包括 ListView、TableView、TreeView这三种

(ComboBox也可以算作ListView)

预览

对应的Model包括 ListModel、TableModel、TreeModel

预览

Qt提供了一些抽象的Model类,需要自己去继承并实现接口,也有一些可以直接用。

下图是涛哥整理的Qt中model继承关系:

预览

其中的QStringListModel不是抽象类,可以直接用在ListView中。

QStandardItemModel也不是抽象类,可以直接用在任意一种View中。

在数据量大、有性能要求的地方,需要继承QAbstractItemModel类,重新实现一个model。

对于性能要求不高的数据展示,会有一些更加方便、取巧的方式,接着往下看吧。

(友情提示:涛哥不关心QWidget,只说QtQuick/Qml)

整数做model

在ListView中,一个整数作为model,就可以创建多个delegate实例。

整数作为model,也可以用在GridView、Combobox、Repeater等需要model的地方。

<Qml组件化编程6-进度条定制>一文中,展示渐变效果,就用的整数作为model

预览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import QtQuick 2.9
import QtQuick.Controls 2.5

Item {
anchors.fill: parent

GridView {
id: g
anchors.fill: parent
anchors.margins: 20
cellWidth: 160
cellHeight: 160
model: 180 //这里的数据Model直接给个整数180
clip: true
property var invalidList: [27, 39, 40, 45, 71, 74, 105, 111, 119, 130, 135, 141] //这几个是不能用的,看过运行报错后手动列出来的。
delegate: Item{
width: 160
height: 160
Rectangle{
width: 150
height: 150
anchors.centerIn: parent
color: "white"
radius: 10
Text {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 2
text: index + 1
}
Rectangle {
width: 100
height: width
radius: width / 2
//编号在列表里的,直接渐变赋值为null,就不会在Qml运行时报警告了
gradient: g.invalidList.indexOf(modelData + 1) < 0 ? modelData + 1 : null
anchors.centerIn: parent
anchors.verticalCenterOffset: 10
}
}
}
}
}

关于delegate

简单说一下delegate:

上面GridView的 model设置为180,表示这个View要产生180个相同的构件实例,按照Grid的方式布局排列。

而delegate就相当于是一个模板,用来描述这180个相同的构件长啥样。当然每个实例不可能完全长得一样,我们可以通过

绑定delegate提供的内置属性或其它属性,达到”大同小异”的目的。

delegate中一般会提供一个index和一个modelData,详细的说明需要参考相应的View文档。

View与Repeater的区别

上面的GridView虽然会创建180个实例,但并不是一次创建全部的,而是只创建能看见的那几个,否则会占用很多CPU、内存和GPU资源。

而Repeater这种就是直接生成180个,并没有做任何内置处理。

(Repeater也可以通过自己控制visible的方式,实现部分创建,后面涛哥有个RingView特效会用这种方式)

ListModel

Qml提供了ListModel这样的一个封装,可以直接在Qml中定义静态的model

静态ListModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import QtQuick 2.0

ListModel {
id: fruitModel

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

然后在ListView中使用

1
2
3
4
5
6
7
8
9
10
11
12
ListView {
anchors.fill: parent
model: fruitModel
delegate: Item {
Text {
text: modelData.name
}
Text {
text: cost
}
}
}

第一个text通过modelData.name获取到name值

第二个text直接用了cost,其实是modelData.cost省略了modelData。这种写法在静态的ListModel中是可以用的。

动态ListModel

ListModel还提供了一些动态修改数据的接口:

预览

像append、 set、insert这些,参数里的jsobject就是js环境中的Object类型,可以参考JS手册

这里涛哥示例一下,动态添加元素

1
2
3
4
5
6
7
8
9
10
11
12
13
...
onClicked: {
var banana = new Object()

//或者这样也行,按照js语法即可
//var bababa = Object.create(null)

banana["name"]="banana" //方括号 + key的方式设置成员
babana.cost=15 //点+名字的方式设置成员

fruitModel.append(banana) //将创建的banana添加到model
}
...

更详细的用法,可以参考 涛哥两年前写过的一个Qml表格编辑器

里面有ListModel的JSON序列化和反序列化、动态增、删、改,Ubuntu风格的查找、Redo、UnDo等大部分功能。

TaoQuick项目的插件机制,也是通过JSON动态添加Model元素。TaoQuick

XmlListModel

处理xml的model,可以方便地使用XPath。

1
2
3
4
5
6
7
8
XmlListModel {
id: feedModel
source: "http://rss.news.yahoo.com/rss/oceania"
query: "/rss/channel/item"
XmlRole { name: "title"; query: "title/string()" }
XmlRole { name: "link"; query: "link/string()" }
XmlRole { name: "description"; query: "description/string()" }
}

ObjectModel

可视对象的集合,做为model,连Delegate都省了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}
}

C++导出Model

除了以上这些,C++中导出的一些类型也可以作为数据model。

这里的导出包括Q_PROPERTY和 Q_INVOKABLE函数的返回值、槽函数的返回值,以及

setContextProperty注册到上下文的可用作model的类型。

一般使用Q_PROPERTY (本质上也是属性的get函数返回值,在js中做了转换)

QList

QList 字符串列表,可以直接用,不用多说了。

QList<QObject*> QObject列表,List中的任意一个QObject有一些属性变更时,都能通知到Qml。

QJsonArray

QJsonArray也是可以直接导出给ListView用,不过注意是只读的。

QQmlPropertyMap

QQmlPropertyMap 是一个Map结构, 但是这个结构注册后,Qml中可以直接用”点 + 名字”的方式访问其中的数据

1
2
3
4
5
6
7
8
9
10
11
12
// create our data
QQmlPropertyMap ownerData;
ownerData.insert("name", QVariant(QString("John Smith")));
ownerData.insert("phone", QVariant(QString("555-5555")));

// expose it to the UI layer
QQuickView view;
QQmlContext *ctxt = view.rootContext();
ctxt->setContextProperty("owner", &ownerData);

view.setSource(QUrl::fromLocalFile("main.qml"));
view.show();
1
2
3
4
//main.qml
Text {
text: owner.name + " " + owner.phone
}

ListView缺失的灵魂

Qml这个ListView是残缺不全的,很多功能都要自己实现。

搜索与排序

前面提到的QSortFilterProxyModel是一种在数据上实现排序和过滤的方法。

还有一种在View层实现搜索和过滤的方式,即DelegateModelGroup。(已经有案例在用,后续再放出代码)

当然Qt5.12的ListView/TableView提供了行和列 隐藏控制的功能,View层做搜索会更方便一些。(还没有实践)

选中

按住Ctrl 再鼠标点击,多选, 再点击一下反选。

按住Shift再鼠标点击,连选。

旧的QtQuick.Controls 1中也有一个ListView,带SelectonModel功能,直接支持多选、反选。

5.12开始,QtQuick.Controls 1模块被废弃了,而Controls2中的ListView不带这功能了。只能自己记键盘按键来模拟实现。

(顺便吐槽一下,5.12直接把Controls 1的TreeView废掉了,Controls 2又没有TreeView。Controls 1的那个虽然还能用,程序跑起来就是一堆js 异常)

拖拽

拖动和放置功能也得自己做。

特效

ListView提供过度动画,下拉刷新一类的效果很多人已经做了,涛哥就不重复了。

(涛哥正在给TaoQuick开发高级插件TaoEffect,将会包含大量酷炫特效组件,敬请期待)

支付宝打赏 微信打赏

为众人抱薪者,不可使其冻毙于霜雪