Wednesday, March 14, 2012

QML TreeView Exersice

Here's a bit of exersice I had to go through to implement a treeview-like structure in QML. The control has to process the XML file, read data and display it in the form that will allow the user to navigate the tree structure. As an example, the XML file is shown below.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<level1item name="level1item 1">
<level2item name="module 1.1">
<level3item name="1.1.1" attr1="0" attr2="1">
</level3item>
<level3item name="1.1.2" attr1="0" attr2="1">
</level3item>
</level2item>
<level2item name="module 1.2">
</level2item>
<level2item name="module 1.3">
</level2item>
<level2item name="module 1.4">
</level2item>
<level2item name="module 1.5">
</level2item>
</level1item>
<level1item name="level1item 2">
<level2item name="module 2.1">
</level2item>
<level2item name="module 2.3">
</level2item>
<level2item name="module 2.4">
</level2item>
<level2item name="module 2.5">
</level2item>
</level1item>
<level1item name="level1item 3">
</level1item>
</configuration>

The QML only has very basic means to parse XML files. In fact, the only suitable way I have found so far is to use XmlListModel. This means that I have to assume that the schema of the XML document is known beforehand. A more generic way would be to use C++ to parse the XML, but that was out of scope for me. I based my control on some existing solutions that are referenced at the end. Essentially, when the control is first loaded, a first level of the XML tree is read and the data is loaded into the ListView XmlListModel, which is defined in the main QML file.

The next idea is to check if each element of the ListView has any children at all. For that purpose, a separate XmlListModel is defined in the ListView delegate. Whenever the ListView item is created, the model is constructed and the query uses the delegate's data to retrieve the children of the item. I want a certain image button (with an arrow indicating that the user can navigate to the children of the item) to become visible only in case there are more than 0 children. First, the query is assigned to the model, and the onQueryChanged event fires. The button is made invisible on this event. Next, the query returns the results and the onCountChanged fires. If the result returned more than 0 items, the button is made visible.


XmlListModel{
id: delegateXmlModel
source: "tree1.xml"
query: buildQuery(false, name);

XmlRole{name: "name"; query: buildRoleQuery();}

onQueryChanged: {
button.visible = false;
}

onCountChanged: {
if(count > 0)
button.visible = true;
}
}

The "back" button is displayed at the bottom of the ListView if the user has moved past the first level of the tree and has an option to move back. The functionality revolves around using the global variable level, which indicates which level of the XML tree is current at the moment. Most of the JavaScript deals with constructing the correct queries for the XmlListModels.

First level of the tree

Last level of the tree

Full listing:

// XMLTree

import QtQuick 1.0

Rectangle {
id:main
width: 360
height: 480

property int level: 0 //0: level1item; 1: level2item; 2: level3item
property string topElement : "configuration"
property string level0: "level1item"
property string level1: "level2item"
property string level2: "level3item"
property string level0Name: ""
property string level1Name: ""
property string level2Name: ""

function buildRoleQuery()
{
return "@name/string()";
}

function buildQuery(isMainTree, name)
{
var level0path = "/" + topElement + "/" + level0;
var level1path = level0path + "[@name=\""
+ level0Name + "\"]/" + level1;
var level2path = level1path + "[@name=\""
+ level1Name + "\"]/" + level2;

var level0query = level0path + "[@name=\""
+ name + "\"]/" + level1;
var level1query = level0path + "[@name=\""
+ level0Name + "\"]/" + level1 + "[@name=\""
+ name + "\"]/" + level2;

if(level == 0)
{
if(isMainTree)
return level0path;
else
return level0query;
}
if(level == 1)
{
if(isMainTree)
return level1path;
else
return level1query;
}
if(level == 2)
{
if(isMainTree)
return level2path;
else
return level0query; //unused
}
return "/";
}

function setQueries()
{
listModel.query = buildQuery(true, "");
listModel.roles[0].query = buildRoleQuery();
listModel.reload();
}

Component.onCompleted: {
backButton.visible = false;
}

ListView {
id: listView
height: parent.height-50
width: parent.width
anchors.top: parent.top
model: listModel

delegate: ListViewDelegate{}
focus: true
}

Rectangle{
id: backRect
height: 50
width: parent.width
anchors.bottom: parent.bottom

Rectangle{
id:backButton
anchors.right: parent.right
width: 40
height: 40

Image{
id:backIcon
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: "icons/arrowBack.png"
}

MouseArea{
id: backMouseArea
anchors.fill: parent

onClicked: {
level--;
if(level == 0)
backButton.visible = false;
setQueries();
}
}
}
}

XmlListModel{
id: listModel
source: "tree1.xml"
query: "/" + topElement + "/" + level0

XmlRole {name: "name"; query: "@name/string()"}
XmlRole {name: "type"; query: "@type/string()"}
XmlRole {name: "attr1"; query: "@attr1/string()"}
XmlRole {name: "attr2"; query: "@attr2/string()"}
}
}
//ListView delegate

import QtQuick 1.0

Rectangle{
id: delegate
width: parent.width
height: textDelegate.height

property string fullText : ""

function addComma()
{
if(fullText)
fullText = fullText + ", ";
}

function getText()
{
fullText = "";
if(level == 0)
{
if(type)
fullText = "
" + "type=" + type;
if(attr1)
{
addComma();
fullText = fullText + "
" + "attr1=" + attr1;
}
return name + fullText;
}
else if(level == 1)
return name;
else if(level == 2)
{
if(attr1)
fullText = "
" + "attr1: " + attr1;
if(attr2)
{
addComma();
fullText = fullText + "attr2: " + attr2;
}
return name + fullText;
}
else
return name;
return "";
}

Component.onCompleted: {
textDelegate.text = getText();
}

XmlListModel{
id: delegateXmlModel
source: "tree1.xml"
query: buildQuery(false, name);

XmlRole{name: "name"; query: buildRoleQuery();}

onQueryChanged: {
button.visible = false;
}

onCountChanged: {
if(count > 0)
button.visible = true;
}
}

Rectangle{
id: contentDelegate
anchors.left: parent.left
anchors.right: nextButton.left
height: textDelegate.height
border.color: "red"
border.width: 1
z:1

Image{
id: elementIcon
height: textDelegate.height
fillMode: Image.PreserveAspectFit
}

Text{
id: textDelegate
anchors.left: elementIcon.right
anchors.leftMargin: 10
anchors.right: parent.right
height: 70
text: ""
font.pixelSize: 15
horizontalAlignment: Text.AlignHLeft
wrapMode: Text.WordWrap
}
}

Rectangle{
id: nextButton
anchors.right: parent.right
anchors.rightMargin: 4
width: 40
height: contentDelegate.height

Rectangle{
id: button
anchors.centerIn: parent
width: 40
height: 40

radius: 5
border{ color: "gray"; width: 3}
visible: (level < 2) //hide 'next' button when lowest level reached

Image{
id: nextIcon
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: "icons/arrow.png"
}

MouseArea{
id: nextMouseArea
anchors.fill: parent

onClicked: {
if(level == 0)
level0Name = name;
else if(level == 1)
level1Name = name;
level++;
if(level > 0)
backButton.visible = true;
setQueries();
listView.model = listModel;
}
}
}
}
}

References

QML Treeview
New Version of the QML TreeView by . Also posted on my website

No comments: