Click here to Skip to main content
15,886,830 members
Articles
(untagged)

QML TreeModel and TreeView

Rate me:
Please Sign up or sign in to vote.
4.50/5 (7 votes)
6 Aug 2013CPOL6 min read 64.4K   4   11
QML TreeModel and TreeView implementation with ListModel and ListView

Introduction

There seems to be a pressing need for some quick implementation of TreeView and model in QML. This article will teach you one of the ways in which you could do it. Note that to just implement the model and view, I've used no Desktop Components. All can be done with the old qml offerings. Additional imports you see in the final program is merely for user interaction - they are in no way strictly required. The final program should work without corrections. 

Background

The audience is expected to be a little familiar with QML.

For starters, Let's consider two ways of data addition to ListModel:

Eg 1:

<1> Static way:

JavaScript
ListModel {
   ListElement {
      role0: "Something"
      role1: 0
   }
   ListElement {
      role0: "SomethingElse"
      role1: 1
   }
}

<2> Same thing done dynamically:

JavaScript
ListModel { 
   Component.onCompleted: {
      append({"role0": qsTr("Something"), "role1": 0})
      append({"role0": qsTr("SomethingElse"), "role1": 1})
   }
}

Eg 2: A little more complicated model

<1> Static way:

JavaScript
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> Same thing done dynamically:

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

Hope you get the idea. To learn more, read about JavaScript objects.

Using the code

Now back to the topic: 

There are two key concepts used here for the kind of simple tree view implementation which we'll see using ListModel and ListView.

First, we use a recursive component - a component that has an ability to add to itself at run-time, well, itself. Such a component could look like:

JavaScript
Component {
   id: objRecursiveComponent
   
   Column {
      Item {
         id: objData1
      }
           
      Repeater {
         model: someModel
         delegate: objRecursiveComponent
      }
   }
}

Here objData<n=1,2....> can be viewed as data members for a node and Repeater is there to add further sub-nodes - that is what a tree is like.

If someModel is numeric and > 0 or object list with length > 0 then this would lead to infinite recursion. We must provide a way such that someModel is controllable for every new instantiation of object from objRecursiveComponent. One simple way to imagine is exposing an object list of objects with each object having a normal role-field which objData1 would use and another role-field which is an object-list of similar objects as itself. This 2nd member can then be used as a source model (someModel above) to Repeater's model member. If the 2nd member is an empty object-list, the recursion stops and no further subnodes get attached/created. Any update (dynamic) of such object-lists will directly affect the tree due to QML bindings, either adding nodes to the tree or removing from it.

So let's build one such tree that allows dynamic addition of nodes anywhere legal. Other operations should be all trivial once this much is clear. Further every node of the tree shall contain two data members - one the name field (for the name of the node) and the other a chain (like the Repeater above) for attaching the sub-nodes (child nodes).

The second concept is the JavaScript object for the model. If you get the examples in the Background section to this article, this should be easy to follow.

JavaScript object for the ListModel should look like:

JavaScript
{"name": qsTr("Node-Name"), "level": 0, "subNode": []} 

level field is just for some added information that will help us indent the child nodes cheaply - of-course I'm not providing any ideal solution anytime, just a little help for getting started.

Here's a ListModel:

JavaScript
ListModel {
    id: objModel
}

To add nodes "Zero", "One" and "Two" you will simply do:

JavaScript
objModel.append({"name": qsTr("Zero"), "level": 0, "subNode": []})
objModel.append({"name": qsTr("One"), "level": 0, "subNode": []})
objModel.append({"name": qsTr("Two"), "level": 0, "subNode": []}) 

To add nodes "Three" and "Four" to node "One" you'll write:

JavaScript
objModel.get(1).subNode.append({"name": qsTr("Three"), "level": 1, "subNode": []})
objModel.get(1).subNode.append({"name": qsTr("Four"), "level": 1, "subNode": []})

To add "Five" to "Three":

JavaScript
objModel.get(1).subNode.get(0).subNode.append({"name": qsTr("Five"), "level": 2, "subNode": []})

Hope all that was clear.

Now let's create a delegate for the ListView. Our simple delegate will have the following basic properties:

  • node color should be yellow if it contains no further child nodes, blue otherwise
  • child nodes should have indentation (otherwise it'll look visually confusing) - for this we will use the level field of the objects in the ListModel
  • nodes with child nodes should be collapsible and expandable

Here's an attempt at such a component:

JavaScript
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
     }
  }
}

We have already discussed the trickier parts of it. Rest of it I hope is simple enough to follow with the aid of a few inline comments in the code snippet above. Note that each subNode is itself a ListElement, so when the model of the Repeater is subNode (as in above) the delegate will have access to fields/roles model.name, model.level and model.subNode, all three of which we use in the delegate above.

Thus far we have seen the codes for ListModel, the recursive delegate which we will present to ListView and some codes to add data to our ListModel. That all that's required. To appreciate the concept a little more we'll add user interaction. The user can add nodes to any legal place in the tree. When the user just states a string (e.g., "XYZ") it'll be added to level 0 as node 0. If user again just states a string (e.g., "ABC") it will be added to level 0 as node 1. Now that we have 2 nodes at level 0, the user can add child node to any of them. Say it is desired that node 1 ("ABC") should contain a sub-node ("QWE"). The user will let this intention be known via input: 1,QWE. By this he means that node 1 (which "ABC") should contain child-node "QWE". If a subsequent input is 1,RTY then "ABC" will have 2 child-nodes "QWE" and "RTY". To add to "UIO" to "QWE" he'll mention: 1,0,UIO as input. This is because 1 is "ABC". Then inside "ABC" 0 is "QWE". Similarly to add "123" to "UIO" the user will mention: 1,0,0,123. Hope you have got the hang of it.

For this we will add a Button which when clicked will display a Modal TextInput. Here the user can supply input and press Carriage-return. We'll put a regex validator on the textinput though it's optional (I love regex because I use Vim so much - i put it in places not even required just for the heck of it :) ). The actual important stuff we'll do is check if the input is legal. If it is then add the data to the model else display an error on the console. Eg., of an illegal input would be: 1,3,ASDF for the above example as node 1 ("ABC") has no node 3. It has only 2 nodes "QWE" and "RTY" ie., node 0 and node 1.

Here's the code:

JavaScript
//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": []})

There is nothing out of the world there. Just splitting on basis of "," and considering the last string to be the node name data for the text of the delegate objRecursiveDelegate.

Here is a complete listing of the code:

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

Here are possible operations to get you started (to be done in order):

  1. click on the button -> click on the modal window and just press carriage-return
  2. click on the button -> just press carriage-return (this time the modal window will automatically have the focus)
  3. repeat <2> 3 more times
  4. click on the button -> go to the beginning of the text in the modal window (press "Home") -> enter 2, (leave the rest of the text as it is) -> press carriage-return
  5. repeat <2> 3 more times
  6. click on the button -> go to 2, in the text -> enter 1, (the text should now look like: 2,1,node0) -> press carriage-return
  7. repeat <2> 3 more times
  8. Double click on the outermost blue node
  9. Repeat <8>
  10. Do this for other blue nodes

By now you'll have the hang of it.

Now that you know how simple it is to create a TreeView in QML/JavaScript, you can go ahead and tweak this or use this or write something completely from scratch and build complex TreeModel with all signal-slots/drag-drop/drag-drop-insert/shuffle/sort etc., features. I have done it (after brushing up on good algorithms on which I tend to become rusty as everything is ready-made these days) and it works wonderfully.

License

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


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionVersion with autolevel Pin
MVova10-Sep-21 2:20
MVova10-Sep-21 2:20 
QuestionDrag&Drop Pin
Realola Lifeival12-Nov-13 6:54
Realola Lifeival12-Nov-13 6:54 
AnswerRe: Drag&Drop Pin
Spandan_Sharma12-Nov-13 18:43
Spandan_Sharma12-Nov-13 18:43 
GeneralRe: Drag&Drop Pin
Realola Lifeival14-Nov-13 6:04
Realola Lifeival14-Nov-13 6:04 
QuestionAn extension: Drawing a rectangle around it and all children... Pin
gemmell4-Oct-13 22:30
gemmell4-Oct-13 22:30 
AnswerRe: An extension: Drawing a rectangle around it and all children... Pin
gemmell5-Oct-13 17:30
gemmell5-Oct-13 17:30 
GeneralRe: An extension: Drawing a rectangle around it and all children... Pin
Spandan_Sharma5-Oct-13 19:13
Spandan_Sharma5-Oct-13 19:13 
QuestionLooks good Pin
gemmell18-Sep-13 18:12
gemmell18-Sep-13 18:12 
AnswerRe: Looks good Pin
Spandan_Sharma19-Sep-13 17:15
Spandan_Sharma19-Sep-13 17:15 
GeneralRe: Looks good Pin
gemmell26-Sep-13 2:26
gemmell26-Sep-13 2:26 
GeneralMy vote of 5 Pin
Member 86937947-Aug-13 19:34
Member 86937947-Aug-13 19:34 

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

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