65.9K
CodeProject 正在变化。 阅读更多。
Home

QML QtQuick TreeView/Model - 第二部分

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (4投票s)

2013年10月6日

CPOL

6分钟阅读

viewsIcon

36394

Qt Quick 树视图中的操作。

引言

这是上一篇文章的后续文章,上一篇文章介绍了在 QML 中构建 Tree View 的简单概念。它假设您已完全阅读并理解了上一篇文章。在这篇文章中,我们将用一些更好的替代方案改进上一篇文章中匆忙编写的代码,更重要的是,我们将学习如何在树视图中移动数据,这正如 Part-I 中的一个评论所要求的。

请在此处阅读/参考上一篇文章。

使用代码

Tree-view 构建的基础知识保持不变——组件递归,配合适当的 JS 对象模型和控制递归的功能。之前的 JS 模型有 3 个角色:“name”用于树视图中节点的名称,“level”主要用于缩进,以及“subNode”用于嵌套模型(因此最终用于组件递归)。我们要做的第一件事是再添加一个角色,并称之为“parentModel”。这将允许我们保存当前节点的父节点,因此每个子节点都知道构建它的父模型。

在接下来的时间里,除非另有说明,我们将参考上一篇文章中的最终代码列表。

因此,让我们设置父节点。代码中有两个地方我们为树创建新节点。这两个地方都在 TextInputonAccepted 信号内部。一个是为了 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 
                  } 
               } 
            }

其中 objMouseAreaMouseArea 本身的 objectName,其可见性不得关闭。

要移动节点,我们需要拖放功能,因此 MouseArea 将定义一个可拖动的目标,并且还将包含一个 DropArea 来接受放置。这对于拖放操作来说相当标准。简而言之,概念是这样的:我们将一个拖动目标(例如,一个 Rectangle)提供给 MouseArea。每当 MouseArea 上发生拖动时,此拖动目标就会被拖动。拖动事件会使

 drag.active = true

MouseArea。这就是我们监控的。每当有活动的拖动时,我们都会“取消锚定”拖动目标(如果它被锚定了,否则它不会移动 :)),并将其父项设置为最顶层的项。后者是必要的,因为通常会有很多其他组件在此拖动目标之后创建。因此,当拖动目标在屏幕上移动时,当它遇到任何在其之后或在其父项等之后创建的组件时,它将被隐藏。为避免这种情况,我们在拖动期间将其父项更改为最顶层的项,以便被拖动的对象始终可见。这通过以下方式完成:

states: State {
           when: ....
        }

同样,我们定义一个 DropArea(QML 组件)。它识别进入、退出或放置的活动拖动。现在一个重要的问题: 在本文中,我们只想移动具有相同直接父项的项。我们将使用拖放键来限制这一点。通常 DropArea 会响应每个拖动。如果它只需要处理少数几个并拒绝其他,那么我们需要利用被拖动的源的 Drag.keysDropAreakeys 属性。然后,只有当它们匹配时,DropArea 才会响应拖动(参见文档)。在我们的情况下,我们将使用角色“parentModel”中保存的内容作为拖动和放置键。这个“内容”实际上是父模型在内存中的地址。由于所有唯一的父模型在内存中都有不同的物理地址,我们将实现我们的目标,即只允许同一个直接父项下的节点被移动。当鼠标释放(拖动后)时,我们将检查被拖动的对象是否被拖动到一个有效的(可接受的)DropArea。当键匹配并且 Drag.target 附加属性非空时,就会发生这种情况。我们在释放鼠标时检查此条件,如果非空,我们将触发一个放置信号,该信号将调用 DropAreaonDropped 插槽(这都是 QML 的行为——我们不需要进行这些连接——只需调用适当的信号)。在 DropAreaonDropped 插槽中,我们将询问被拖动的对象/节点的索引,并进行适当的移动。例如,如果拖动了索引 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.

双击节点进行展开/折叠。

首先慢慢地将节点拖放到彼此上方,以查看哪些位置允许放置(会有允许放置区域的动画指示)。四处移动节点以熟悉。在展开状态下移动节点可能会在视觉上有些混乱(尽管代码会做正确的事情)。如果出现这种情况,请先折叠节点,然后再移动它。

就这样。使用、复杂化、改进并享受(如果觉得它值得评分,也许可以给我评分)!!

© . All rights reserved.