QML QtQuick TreeView/Model - 第二部分





4.00/5 (4投票s)
Qt Quick 树视图中的操作。
引言
这是上一篇文章的后续文章,上一篇文章介绍了在 QML 中构建 Tree View 的简单概念。它假设您已完全阅读并理解了上一篇文章。在这篇文章中,我们将用一些更好的替代方案改进上一篇文章中匆忙编写的代码,更重要的是,我们将学习如何在树视图中移动数据,这正如 Part-I 中的一个评论所要求的。
使用代码
Tree-view 构建的基础知识保持不变——组件递归,配合适当的 JS 对象模型和控制递归的功能。之前的 JS 模型有 3 个角色:“name
”用于树视图中节点的名称,“level
”主要用于缩进,以及“subNode
”用于嵌套模型(因此最终用于组件递归)。我们要做的第一件事是再添加一个角色,并称之为“parentModel
”。这将允许我们保存当前节点的父节点,因此每个子节点都知道构建它的父模型。
在接下来的时间里,除非另有说明,我们将参考上一篇文章中的最终代码列表。
因此,让我们设置父节点。代码中有两个地方我们为树创建新节点。这两个地方都在 TextInput
的 onAccepted
信号内部。一个是为了 level 0 的特殊情况,另一个是为了 level > 0 的情况。在这两个地方,我们将添加新提到的角色。因此,替换
objModel.append({"name": szSplit[0], "level": 0, "subNode": []})
用
objModel.append({"name": szSplit[0], "level": 0, "parentModel": objModel, "subNode": []})
和
node.subNode.append({"name": szSplit[i], "level": i, "subNode": []})
用
node.subNode.append({"name": szSplit[i], "level": i, "parentModel": node.subNode, "subNode": []})
上面的代码很容易——我们要向任何模型附加一个新对象,就将该模型本身传递给要创建的对象。
通过双击展开/折叠带有子节点的节点的代码已被修改为遍历其所有直接子节点,并在 objectName
不是 MouseArea
本身(如果 MouseArea 本身渲染为不可见,我们将无法恢复其可见性)时切换它们的可见性。这样做是因为 QML 嵌套器在父视图(Row、Column、GridView 等)中的定位在动态更新内容时似乎很奇怪——我目前对此行为不了解。当嵌套器静态创建内容时,很容易预测它本身是哪个子项(第 0、1、2 个等),但一旦嵌套器的模型有动态更新,事情就会发生微妙的变化(尝试打印具有动态更新模型的 Row 的所有子项的 objectNames。我注意到嵌套器本身的索引会发生变化)。为了避免复杂性,我们通过 objectNames
过滤子节点来切换可见性。MouseArea
中切换可见性的循环现在如下所示:
onDoubleClicked: {
for(var i = 0; i < parent.children.length; ++i) {
if(parent.children[i].objectName !== "objMouseArea") {
parent.children[i].visible = !parent.children[i].visible
}
}
}
其中 objMouseArea
是 MouseArea
本身的 objectName
,其可见性不得关闭。
要移动节点,我们需要拖放功能,因此 MouseArea
将定义一个可拖动的目标,并且还将包含一个 DropArea
来接受放置。这对于拖放操作来说相当标准。简而言之,概念是这样的:我们将一个拖动目标(例如,一个 Rectangle
)提供给 MouseArea
。每当 MouseArea
上发生拖动时,此拖动目标就会被拖动。拖动事件会使
drag.active = true
的 MouseArea
。这就是我们监控的。每当有活动的拖动时,我们都会“取消锚定”拖动目标(如果它被锚定了,否则它不会移动 :)),并将其父项设置为最顶层的项。后者是必要的,因为通常会有很多其他组件在此拖动目标之后创建。因此,当拖动目标在屏幕上移动时,当它遇到任何在其之后或在其父项等之后创建的组件时,它将被隐藏。为避免这种情况,我们在拖动期间将其父项更改为最顶层的项,以便被拖动的对象始终可见。这通过以下方式完成:
states: State { when: .... }
同样,我们定义一个 DropArea
(QML 组件)。它识别进入、退出或放置的活动拖动。现在一个重要的问题: 在本文中,我们只想移动具有相同直接父项的项。我们将使用拖放键来限制这一点。通常 DropArea
会响应每个拖动。如果它只需要处理少数几个并拒绝其他,那么我们需要利用被拖动的源的 Drag.keys
和 DropArea
的 keys
属性。然后,只有当它们匹配时,DropArea
才会响应拖动(参见文档)。在我们的情况下,我们将使用角色“parentModel
”中保存的内容作为拖动和放置键。这个“内容”实际上是父模型在内存中的地址。由于所有唯一的父模型在内存中都有不同的物理地址,我们将实现我们的目标,即只允许同一个直接父项下的节点被移动。当鼠标释放(拖动后)时,我们将检查被拖动的对象是否被拖动到一个有效的(可接受的)DropArea
。当键匹配并且 Drag.target
附加属性非空时,就会发生这种情况。我们在释放鼠标时检查此条件,如果非空,我们将触发一个放置信号,该信号将调用 DropArea
的 onDropped
插槽(这都是 QML 的行为——我们不需要进行这些连接——只需调用适当的信号)。在 DropArea
的 onDropped
插槽中,我们将询问被拖动的对象/节点的索引,并进行适当的移动。例如,如果拖动了索引 2 并将其放置在索引 4 上方,则索引 4 的对象将被定位在索引 3,并且索引 4 将被索引为 2 的对象占据。如果将索引 4 的对象拖动并放置在索引 2 的对象上方,则索引 2 的对象将成为索引 3 的对象,索引 2 现在将被索引为 4 的对象占据。(请注意,较低索引被释放到较高索引和反之亦然之间的细微差别)。
为了让用户知道所选对象/节点的 DropArea
是否有效,我们将使用动画来指示它。
这是一个完整的代码列表,它应该(我希望)无需更正即可工作
import QtQuick 2.0
import QtQuick.Window 2.0
import QtQuick.Layouts 1.0
import QtQuick.Controls 1.0
Rectangle {
id: objRoot
objectName: "objRoot"
width: 600
height: 600
color: "black"
ListModel {
id: objModel
objectName: "objModel"
}
Component {
id: objRecursiveDelegate
Column {
id: objRecursiveColumn
objectName: "objRecursiveColumn"
property int m_iIndex: model.index
property var m_parentModel: model.parentModel
clip: true
MouseArea {
id: objMouseArea
objectName: "objMouseArea"
width: objRow.implicitWidth
height: objRow.implicitHeight
onDoubleClicked: {
for(var i = 0; i < parent.children.length; ++i) {
if(parent.children[i].objectName !== "objMouseArea") {
parent.children[i].visible = !parent.children[i].visible
}
}
}
drag.target: objDragRect
onReleased: {
if(objDragRect.Drag.target) {
objDragRect.Drag.drop()
}
}
Row {
id: objRow
Item {
id: objIndentation
height: 20
width: model.level * 20
}
Rectangle {
id: objDisplayRowRect
height: objNodeName.implicitHeight + 5
width: objCollapsedStateIndicator.width + objNodeName.implicitWidth + 5
border.color: "green"
border.width: 2
color: "#31312c"
DropArea {
keys: [model.parentModel]
anchors.fill: parent
onEntered: objValidDropIndicator.visible = true
onExited: objValidDropIndicator.visible = false
onDropped: {
objValidDropIndicator.visible = false
if(drag.source.m_objTopParent.m_iIndex !== model.index) {
objRecursiveColumn.m_parentModel.move(
drag.source.m_objTopParent.m_iIndex,
model.index,
1
)
}
}
Rectangle {
id: objValidDropIndicator
anchors.fill: parent
visible: false
onVisibleChanged: {
visible ? objAnim.start() : objAnim.stop()
}
SequentialAnimation on color {
id: objAnim
loops: Animation.Infinite
running: false
ColorAnimation { from: "#31312c"; to: "green"; duration: 400 }
ColorAnimation { from: "green"; to: "#31312c"; duration: 400 }
}
}
}
Rectangle {
id: objDragRect
property var m_objTopParent: objRecursiveColumn
Drag.active: objMouseArea.drag.active
Drag.keys: [model.parentModel]
border.color: "magenta"
border.width: 2
opacity: .85
states: State {
when: objMouseArea.drag.active
AnchorChanges {
target: objDragRect
anchors { horizontalCenter: undefined; verticalCenter: undefined }
}
ParentChange {
target: objDragRect
parent: objRoot
}
}
anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
height: objDisplayRowRect.height
width: objDisplayRowRect.width
visible: Drag.active
color: "red"
Text {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: model.name
font { bold: true; pixelSize: 18 }
color: "blue"
}
}
Text {
id: objCollapsedStateIndicator
anchors { left: parent.left; top: parent.top; bottom: parent.bottom }
width: 20
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: objRepeater.count > 0 ? objRepeater.visible ? qsTr("-") : qsTr("+") : qsTr("")
font { bold: true; pixelSize: 18}
color: "yellow"
}
Text {
id: objNodeName
anchors { left: objCollapsedStateIndicator.right; top: parent.top; bottom: parent.bottom }
text: model.name
color: objRepeater.count > 0 ? "yellow" : "white"
font { bold: true; pixelSize: 18 }
verticalAlignment: Text.AlignVCenter
}
}
}
}
Rectangle {
id: objSeparator
anchors { left: parent.left; right: parent.right; }
height: 1
color: "black"
}
Repeater {
id: objRepeater
objectName: "objRepeater"
model: subNode
delegate: objRecursiveDelegate
}
}
}
ColumnLayout {
objectName: "objColLayout"
anchors.fill: parent
ScrollView {
Layout.fillHeight: true
Layout.fillWidth: true
ListView {
objectName: "objListView"
model: objModel
delegate: objRecursiveDelegate
interactive: false
}
}
Window {
id: objModalInput
objectName: "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, "parentModel": objModel, "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, "parentModel": node.subNode, "subNode": []})
}
}
}
}
}
Button {
text: "add data to tree"
onClicked: {
objModalInput.show()
}
}
}
}
操作(按顺序)
<1> click on "add data to tree" button -> enter "node0" (without quotes) and press carriage-return. <2> click on "add data to tree" button -> enter "node1" (without quotes) and press carriage-return. <3> click on "add data to tree" button -> enter "node2" (without quotes) and press carriage-return. <4> click on "add data to tree" button -> enter "node3" (without quotes) and press carriage-return. <5> click on "add data to tree" button -> enter "1,node0" (without quotes) and press carriage-return. <6> click on "add data to tree" button -> enter "1,node1" (without quotes) and press carriage-return. <7> click on "add data to tree" button -> enter "1,node2" (without quotes) and press carriage-return. <9> click on "add data to tree" button -> enter "1,node3" (without quotes) and press carriage-return. <10> click on "add data to tree" button -> enter "1,2,node0" (without quotes) and press carriage-return. <11> click on "add data to tree" button -> enter "1,2,node1" (without quotes) and press carriage-return. <12> click on "add data to tree" button -> enter "1,2,node2" (without quotes) and press carriage-return. <13> click on "add data to tree" button -> enter "1,2,node3" (without quotes) and press carriage-return. <14> click on "add data to tree" button -> enter "3,nodeA" (without quotes) and press carriage-return. <15> click on "add data to tree" button -> enter "3,nodeB" (without quotes) and press carriage-return. <16> click on "add data to tree" button -> enter "3,nodeC" (without quotes) and press carriage-return. <17> click on "add data to tree" button -> enter "3,nodeD" (without quotes) and press carriage-return. <18> click on "add data to tree" button -> enter "3,1,nodeQ" (without quotes) and press carriage-return. <19> click on "add data to tree" button -> enter "3,1,nodeR" (without quotes) and press carriage-return. <20> click on "add data to tree" button -> enter "3,1,nodeS" (without quotes) and press carriage-return. <21> click on "add data to tree" button -> enter "3,1,nodeT" (without quotes) and press carriage-return.
双击节点进行展开/折叠。
首先慢慢地将节点拖放到彼此上方,以查看哪些位置允许放置(会有允许放置区域的动画指示)。四处移动节点以熟悉。在展开状态下移动节点可能会在视觉上有些混乱(尽管代码会做正确的事情)。如果出现这种情况,请先折叠节点,然后再移动它。
就这样。使用、复杂化、改进并享受(如果觉得它值得评分,也许可以给我评分)!!