Click here to Skip to main content
Click here to Skip to main content
Go to top

QML QtQuick TreeView/Model - Part-II

, 5 Oct 2013
Rate this:
Please Sign up or sign in to vote.
Manipulations in a Qt Quick Tree View

Introduction

This is a follow up article on the previous one which introduced simple concepts to build a Tree View in QML. It assumes that the previous article is completely read and understood. In this we will improve some hastily written code in the previous article with little better alternatives and more importantly we will learn how to move data around in the tree view as requested in one of the comments in Part-I.

Please read/refer the previous article here.

Using the code

The fundamentals of Tree-view construction remain the same - Component Recursion with an appropriate JS object-model and functionality to control the recursion. The JS model previously had 3 roles: "name" for the name of the node in the tree-view, "level" which was primarily used for indentation and "subNode" for nested models (and hence ultimately Component Recursion). The 1st thing we will do here is add another role and call it "parentModel". This will allow us to hold the parent node to the current node, hence every child will know the parent model that constructed it.

For sometime now, unless mentioned, we will be referring to the final code listing in the previous article.

So let's set up the parent node. There are two places in the code where we create new nodes for the tree. Both are inside the onAccepted signal of the TextInput. One is for the special case of level 0 and other for levels > 0. In both these places we will add the new mentioned role. Thus replace

objModel.append({"name": szSplit[0], "level": 0, "subNode": []})

with

objModel.append({"name": szSplit[0], "level": 0, "parentModel": objModel, "subNode": []})

and

node.subNode.append({"name": szSplit[i], "level": i, "subNode": []})

with

node.subNode.append({"name": szSplit[i], "level": i, "parentModel": node.subNode, "subNode": []})

The above code is easy - to whatever model we are appending a new object, pass that very model to the object to be created.

The code to expand/collapse nodes with sub-nodes via double clicking is modified to traverse all its immediate children and toggle their visibility if the objectName is anything other than that of the MouseArea itself (if the MouseArea is itself rendered invisible, we would have no way to bring back its visibility). This is done because QML repeater positioning within a parent View (Row, Column, GridView etc) seems bizzarre if the content is updated dynamically - I currently lack knowledge of the behavior. When repeater creates stuffs statically, it's easy to predict which child it itself is (0th, 1st, 2nd etc), but moment there is a dynamic update to the model of the repeater things take a subtle turn (try printing the objectNames of all children of a Row which has a Repeater which in turn has its model dynamically updated. I noticed the index of the repeater itself changes). In order to avoid complications, visibility is toggled based on filtering children via objectNames. The loop to toggle visibility in the MouseArea now looks like this:

onDoubleClicked: { 
               for(var i = 0; i < parent.children.length; ++i) { 
                  if(parent.children[i].objectName !== "objMouseArea") { 
                     parent.children[i].visible = !parent.children[i].visible 
                  } 
               } 
            }

where objMouseArea is the objectName of the MouseArea itself, the visibility of which must not be switched off.

To shuffle nodes, we will require drag-drop facilities and thus MouseArea will define a draggable target and also will contain a DropArea to accept drops. This is pretty standard for a drag-drop operation. In brief the concept is this: We give a drag target (eg., a Rectangle) to the MouseArea. This drag target will be dragged whenever there is a drag on the MouseArea. The drag event makes

 drag.active = true

for MouseArea. This is what we monitor. Whenever there is an active drag we will "un-anchor" the drag target if it is anchored (otherwise it won't move Smile | :) ) and set its parent to the most-root item. This latter part is necessary because normally there would be lot of other components created after this drag target. Thus the drag target will be hidden as it travels across the screen whenever it comes across any component which was created after it, or after its parent etc. To avoid this, we will change its parent for the duration of drag to the most-root item so that the dragged object is always visible. This is done via

states: State {
           when: ....
        }

Similarly, we define a DropArea (QML component). This recognizes active drags being entered, exited or dropped. Now an important point: in this article we will only want to shuffle items which have same immediate parent. We will restrict this with the drag-drop keys. Normally DropArea will acknowledge every drag. If there are only a few which it must handle and reject the rest we need to make use of Drag.keys of the source being dragged and keys property of DropArea. Then, only when these match does DropArea acknowledge the drag (See Docs.). In our case we will use the stuff held by the role "parentModel" as the drag as well as the drop keys. This "stuff" will actually be the address in memory of the parent model. Since all unique parent models will have different physical addresses in the memory, we will have achieved our goal of allowing only nodes under the same immediate parent to be shuffled. When the mouse is released (after the drag) we will check if the dragged object was dragged to a valid (accepting) DropArea. This happens when the keys match and when it happens the dragged object's Drag.target attached property will be non-null. We check for this condition on the release of the mouse and if it's non-null we fire a drop signal which will invoke onDropped slot of the DropArea (this is all QML behaviour - we don't need to make these connections - only call the appropriate signal). In the onDropped slot of the DropArea we will enquire about the index of the object/node which was being dragged and move it appropriately. For eg., if index 2 was dragged and released over index 4, index 4 object would be positioned at index 3 and 4th index would be occupied by the object with index 2. If index 4 object is dragged and released over index 2 object, then index 2 object would become the index 3 object and index 2 would now be occupied by the index 4 object. (Note the subtle difference between the lower index being released over a higher one and vice-versa).

To let the user know if the DropArea for the chosen object/node is valid, we will use an animation to indicate it.

Here's a complete code listing that should (I hope) work without corrections:

 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()
         }
      }
   }
}  

Operations (in order):

<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.

Double-click nodes to expand/collapse.

Drag nodes over each other slowly first to see what places the drop will be allowed (there will be an animated indication for allowed drop areas). Move nodes around to get a feel. Moving nodes around in expanded stated might be a bit visually confusing (although the code will do the correct thing). If so, 1st collapse the node and then move it around.

That's all. Use, complicate, improve and enjoy (and maybe rate this article if you find it worthy of being rated) !!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Spandan_Sharma

United States United States
No Biography provided

Comments and Discussions

 
QuestionPerformance? Pinmembergemmell7-Mar-14 16:58 
AnswerRe: Performance? [modified] Pinmembergemmell10-Mar-14 16:25 
GeneralRe: Performance? PinmemberSpandan_Sharma10-Mar-14 19:38 
GeneralRe: Performance? Pinmembergemmell10-Mar-14 20:37 
BugFYI Pinmembergemmell7-Mar-14 16:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140926.1 | Last Updated 6 Oct 2013
Article Copyright 2013 by Spandan_Sharma
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid