QML TreeModel 和 TreeView






4.50/5 (7投票s)
使用 ListModel 和 ListView 实现 QML TreeModel 和 TreeView
引言
看起来 QML 中对 TreeView 和 Model 的快速实现存在迫切需求。本文将教你一种实现方法。请注意,为了实现 Model 和 View,我没有使用任何桌面组件。所有功能都可以通过旧的 QML 功能实现。你在最终程序中看到的额外导入仅仅是为了用户交互,它们并不是严格必需的。最终程序应该可以无需修改地运行。
背景
受众应略微熟悉 QML。
首先,让我们考虑向 ListModel
添加数据的两种方法
示例 1
<1> 静态方式
ListModel {
ListElement {
role0: "Something"
role1: 0
}
ListElement {
role0: "SomethingElse"
role1: 1
}
}
<2> 动态实现相同功能
ListModel {
Component.onCompleted: {
append({"role0": qsTr("Something"), "role1": 0})
append({"role0": qsTr("SomethingElse"), "role1": 1})
}
}
示例 2:一个更复杂的 Model
<1> 静态方式
ListModel {
ListElement {
role0: "ABC"
contents: [
ListElement {
someRole0: "aqs"
someRole1: 123
},
ListElement {
someRole0: "qwer"
someRole1: 12378
}
]
}
ListElement {
role0: "ABC"
contents: [
ListElement {
someRole0: "aqs"
someRole1: 123
},
ListElement {
someRole0: "qwer"
someRole1: 12378
}
]
}
}
<2> 动态实现相同功能
ListModel { Component.onCompleted: { for(var i = 0; i < 2; ++i) { append({ "role0": qsTr("ABC"), "contents": [ {"someRole0": qsTr("aqs"), "someRole1": 123}, {"someRole0": qsTr("qwer"), "someRole1": 12378} ] }) } } }
希望你已经明白了。要了解更多,请阅读有关 JavaScript 对象的内容。
使用代码
现在回到主题:
在使用 ListModel
和 ListView
进行简单的 TreeView 实现时,这里使用了两个关键概念。
首先,我们使用一个递归组件——一个组件,它可以在运行时添加它自身,也就是它自己。这样的组件可能看起来像这样:
Component {
id: objRecursiveComponent
Column {
Item {
id: objData1
}
Repeater {
model: someModel
delegate: objRecursiveComponent
}
}
}
这里的 objData<n=1,2....>
可以看作是节点的成员数据,而 Repeater
用于添加进一步的子节点——这正是树的结构。
如果 someModel
是数字且大于 0,或者是一个长度大于 0 的对象列表,那么这将导致无限递归。我们必须提供一种方法,使得 someModel
对于 objRecursiveComponent
的每个新实例化对象都是可控的。一个简单的设想是公开一个对象列表,其中每个对象都有一个普通的 role 字段(objData1
将使用它),以及另一个 role 字段,它是一个包含与自身相似对象的对象列表。这个第二个成员可以作为 Repeater
的 model
成员的源 Model(即上面的 someModel
)。如果第二个成员是一个空的对象列表,递归就会停止,不会创建/附加进一步的子节点。任何此类对象列表的(动态)更新将由于 QML 绑定而直接影响树,添加节点到树或从树中删除节点。
因此,让我们构建一个允许在任何合法位置动态添加节点的树。一旦理解了这一点,其他操作都应该很简单。此外,树的每个节点将包含两个数据成员——一个名称字段(用于节点的名称),另一个是链(类似于上面的 Repeater
),用于附加子节点(子节点)。
第二个概念是 Model 的 JavaScript 对象。如果你理解了本文背景部分中的示例,那么这个应该很容易理解。
ListModel
的 JavaScript 对象应该看起来像这样:
{"name": qsTr("Node-Name"), "level": 0, "subNode": []}
level
字段仅用于一些附加信息,这将帮助我们廉价地缩进子节点——当然,我随时都不提供任何理想的解决方案,只是提供一点帮助来入门。
这是一个 ListModel
:
ListModel {
id: objModel
}
要添加节点“Zero”、“One”和“Two”,你只需执行:
objModel.append({"name": qsTr("Zero"), "level": 0, "subNode": []})
objModel.append({"name": qsTr("One"), "level": 0, "subNode": []})
objModel.append({"name": qsTr("Two"), "level": 0, "subNode": []})
要将节点“Three”和“Four”添加到节点“One”下,你将编写:
objModel.get(1).subNode.append({"name": qsTr("Three"), "level": 1, "subNode": []})
objModel.get(1).subNode.append({"name": qsTr("Four"), "level": 1, "subNode": []})
要将“Five”添加到“Three”下:
objModel.get(1).subNode.get(0).subNode.append({"name": qsTr("Five"), "level": 2, "subNode": []})
希望以上内容都清晰明了。
现在,让我们为 ListView
创建一个 delegate
。我们简单的 delegate
将具有以下基本属性:
- 如果节点不包含进一步的子节点,则节点颜色应为黄色;否则为蓝色。
- 子节点应有缩进(否则看起来会令人困惑)——为此,我们将使用 ListModel 中对象的 level 字段。
- 带有子节点的节点应可折叠和展开。
这是一个尝试实现此功能的组件:
Component {
id: objRecursiveDelegate
Column {
id: objRecursiveColumn
clip: true
MouseArea {
width: objRow.implicitWidth
height: objRow.implicitHeight
//for collapsing and expanding
onDoubleClicked: {
//remember that Repeater is also a child of objRecursiveColumn. Altering
//it's visiblity serves no purpose and is wasteful so thus we avoid looping
//over it by parent.children.length - 1. i starts from 1 because we don't
//want to affect the visiblity of 0th child which is this MouseArea itself.
//Also note that making a child invisible will also make all
//children/grandchildren of the child invisible too - nice.
for(var i = 1; i < parent.children.length - 1; ++i) {
parent.children[i].visible = !parent.children[i].visible
}
}
Row {
id: objRow
//for indentation
Item {
height: 1
width: model.level * 20
}
Text {
//if collapsed, show + else show -. If no child-nodes then show nothing
//(cannot be collapsed or expanded)
text: (objRecursiveColumn.children.length > 2 ?
objRecursiveColumn.children[1].visible ?
qsTr("- ") : qsTr("+ ") : qsTr(" ")) + model.name
color: objRecursiveColumn.children.length > 2 ? "blue" : "yellow"
font { bold: true; pixelSize: 14 }
}
}
}
Repeater {
model: subNode
delegate: objRecursiveDelegate
}
}
}
我们已经讨论了其中较棘手的部分。其余部分,我希望借助代码片段中的一些内联注释,都能轻松理解。请注意,每个 subNode
本身就是一个 ListElement
,因此当 Repeater
的 Model 是 subNode
时(如上所示),delegate
将可以访问 model.name
、model.level
和 model.subNode
这些字段/角色,我们在上面的 delegate
中使用了这三者。
到目前为止,我们已经看到了 ListModel
的代码,我们将提供给 ListView
的递归 delegate 的代码,以及一些用于向 ListModel
添加数据的代码。这就是所有必需的。为了更好地理解这个概念,我们将添加用户交互。用户可以向树中的任何合法位置添加节点。当用户仅输入一个字符串(例如,“XYZ”)时,它将被添加为第 0 级的节点 0。如果用户再次仅输入一个字符串(例如,“ABC”),它将被添加为第 0 级的节点 1。现在我们有了第 0 级的两个节点,用户可以向其中任何一个添加子节点。假设需要节点 1(“ABC”)包含一个子节点(“QWE”)。用户将通过输入:1,QWE 来表达此意图。这意味着节点 1(“ABC”)应该包含子节点“QWE”。如果后续输入是 1,RTY,那么“ABC”将有两个子节点“QWE”和“RTY”。要将“UIO”添加到“QWE”下,他将输入:1,0,UIO。这是因为 1 是“ABC”。然后,“ABC”内部的 0 是“QWE”。类似地,要将“123”添加到“UIO”下,用户将输入:1,0,0,123。希望你已经掌握了窍门。
为此,我们将添加一个按钮,点击该按钮将显示一个模态 TextInput。在这里,用户可以输入信息并按回车键。我们将对文本输入设置一个正则表达式验证器,尽管这是可选的(我喜欢正则表达式,因为我大量使用 Vim,所以即使在不需要的地方也会添加它,只是为了好玩 :))。实际重要的部分是我们检查输入是否合法。如果是,则将数据添加到 Model;否则,在控制台显示错误。例如,对于上面的示例,输入 1,3,ASDF 是一个非法输入,因为节点 1(“ABC”)没有节点 3。它只有两个节点“QWE”和“RTY”,即节点 0 和节点 1。
这是代码
//Assume text is the input user supplies to TextInput.
var szSplit = text.split(',')
if(szSplit.length === 1) {
objModel.append({"name": szSplit[0], "level": 0, "subNode": []})
}
else {
if(objModel.get(parseInt(szSplit[0])) === undefined) {
console.log("Error - Given node does not exist !")
return
}
var node = objModel.get(parseInt(szSplit[0]))
for(var i = 1; i < szSplit.length - 1; ++i) {
if(node.subNode.get(parseInt(szSplit[i])) === undefined) {
console.log("Error - Given node does not exist !")
return
}
node = node.subNode.get(parseInt(szSplit[i]))
}
node.subNode.append({"name": szSplit[i], "level": i, "subNode": []})
这没什么特别的。只需根据“,”进行拆分,并将最后一个字符串视为代理组件 objRecursiveDelegate
的文本节点名称数据。
这是完整的代码列表:
import QtQuick 2.0
import QtQuick.Window 2.0
import QtQuick.Layouts 1.0
import QtQuick.Controls 1.0
Rectangle {
width: 600
height: 600
color: "black"
ListModel {
id: objModel
}
Component {
id: objRecursiveDelegate
Column {
id: objRecursiveColumn
clip: true
MouseArea {
width: objRow.implicitWidth
height: objRow.implicitHeight
onDoubleClicked: {
for(var i = 1; i < parent.children.length - 1; ++i) {
parent.children[i].visible = !parent.children[i].visible
}
}
Row {
id: objRow
Item {
height: 1
width: model.level * 20
}
Text {
text: (objRecursiveColumn.children.length > 2 ?
objRecursiveColumn.children[1].visible ?
qsTr("- ") : qsTr("+ ") : qsTr(" ")) + model.name
color: objRecursiveColumn.children.length > 2 ? "blue" : "yellow"
font { bold: true; pixelSize: 14 }
}
}
}
Repeater {
model: subNode
delegate: objRecursiveDelegate
}
}
}
ColumnLayout {
anchors.fill: parent
ListView {
Layout.fillHeight: true
Layout.fillWidth: true
model: objModel
delegate: objRecursiveDelegate
}
Window {
id: objModalInput
modality: Qt.ApplicationModal
visible: false
height: 30
width: 200
color: "yellow"
TextInput {
anchors.fill: parent
font { bold: true; pixelSize: 20 }
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
validator: RegExpValidator {
regExp: /(\d{1,},)*.{1,}/
}
onFocusChanged: {
if(focus) {
selectAll()
}
}
text: qsTr("node0")
onAccepted: {
if(acceptableInput) {
objModalInput.close()
var szSplit = text.split(',')
if(szSplit.length === 1) {
objModel.append({"name": szSplit[0], "level": 0, "subNode": []})
}
else {
if(objModel.get(parseInt(szSplit[0])) === undefined) {
console.log("Error - Given node does not exist !")
return
}
var node = objModel.get(parseInt(szSplit[0]))
for(var i = 1; i < szSplit.length - 1; ++i) {
if(node.subNode.get(parseInt(szSplit[i])) === undefined) {
console.log("Error - Given node does not exist !")
return
}
node = node.subNode.get(parseInt(szSplit[i]))
}
node.subNode.append({"name": szSplit[i], "level": i, "subNode": []})
}
}
}
}
}
Button {
text: "add data to tree"
onClicked: {
objModalInput.show()
}
}
}
}
以下是可用于入门的操作(按顺序执行):
- 点击按钮 -> 点击模态窗口并只按回车键
- 点击按钮 -> 只按回车键(这次模态窗口将自动获得焦点)
- 重复 <2> 3 次
- 点击按钮 -> 转到模态窗口文本的开头(按“Home”)-> 输入 2,(将其余文本保留原样)-> 按回车键
- 重复 <2> 3 次
- 点击按钮 -> 转到文本中的 2, -> 输入 1,(此时文本应显示为:2,1,node0)-> 按回车键
- 重复 <2> 3 次
- 双击最外面的蓝色节点
- 重复 <8>
- 对其他蓝色节点重复此操作
到目前为止,你已经掌握了它。
既然你知道在 QML/JavaScript 中创建 TreeView 是多么简单,你可以继续修改它、使用它,或者从头开始编写一些东西,并构建具有信号槽/拖放/拖放插入/洗牌/排序等功能的复杂 TreeModel。我已经这样做了(在复习了良好的算法之后,我往往会变得生疏,因为如今一切都是现成的),并且效果非常好。