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:
ListModel {
ListElement {
role0: "Something"
role1: 0
}
ListElement {
role0: "SomethingElse"
role1: 1
}
}
<2> Same thing done dynamically:
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:
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:
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:
{"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
:
ListModel {
id: objModel
}
To add nodes "Zero", "One" and "Two" you will simply do:
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:
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":
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:
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
}
}
}
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:
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:
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):
- click on the button -> click on the modal window and just press carriage-return
- click on the button -> just press carriage-return (this time the modal window will automatically have the focus)
- repeat <2> 3 more times
- 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
- repeat <2> 3 more times
- click on the button -> go to 2, in the text -> enter 1, (the text should now look like: 2,1,node0) -> press carriage-return
- repeat <2> 3 more times
- Double click on the outermost blue node
- Repeat <8>
- 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.