For a Qt project based on Python for the backend and QML for the frontend, I made a custom component used to display data as a tree structure. This component uses a ListModel, the name of the field in the model where the name of the data is stored (so I can reuse this component for any model I have), the name of the field in the model containing the id, and the name of the field of the model containing the data's parent ID (for the same reason).
Each data can have a parent, so the component handles this by displaying a round button on the data row if the data has at least one child, and not displaying it when a data has no children.
The component is using Qt's Loader component to handle the recursion of the data's display.
I would like to understand how I could implement an expandAll and collapseAll function with my current method of handling data hierarchy, and furthermore, how would I be able to display data based on its id, for example:
name: "Data 1", id: 1, parentId: 0
|
|
|________ name: "Data 2", id: 2, parentId: 1
| |
| |
| |________ name: "Data 3", id: 3, parentId: 2
|
|
|________ name: "Data 4", id: 4, parentId: 1
|
|
|________ name: "Data 5", id: 5, parentId: 4
For example, how would I be able to make my tree structure display "Data 1", "Data 2", "Data 4" and "Data 5" if I would like to see the "Data 5" ? In this way, I mean how could all the above parents of a data use the expand method declared in my code so the entire path leading to a specific data would be displayed ?
Here is the code I'm currently using. Many thanks to Stephen Quan who helped me understanding how to display data's in QML in a previous post :
main.qml :
//Edit : Added a simple example of customerDataModel :
ListModel {
id: customerDataModel
ListElement {
CustomerID: 1
CustomerName: "Google"
CustomerParentId: 0
}
ListElement {
CustomerID: 2
CustomerName: "Amazon"
CustomerParentId: 0
}
ListElement {
CustomerID: 3
CustomerName: "Amazon US"
CustomerParentId: 2
}
ListElement {
CustomerID: 4
CustomerName: "Amazon EU"
CustomerParentId: 2
}
ListElement {
CustomerID: 5
CustomerName: "Amazon FR"
CustomerParentId: 4
}
ListElement {
CustomerID: 6
CustomerName: "Apple"
CustomerParentId: 0
}
ListElement {
CustomerID: 7
CustomerName: "Apple NTH"
CustomerParentId: 6
}
}
OctopusTreeView {
id: customerTreeView
width: 500
height: 500
dataModel: customerDataModel
dataId: "CustomerID"
dataName: "CustomerName"
dataParentId: "CustomerParentId"
}
OctopusTreeView.qml :
import QtQuick 2.13
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3
Item {
property var dataTable: ([])
property var dataModel: []
property string dataId: ""
property string dataName: ""
property string dataParentId: ""
property var selectionList: []
signal selectionChanged()
height: parent.height
function getSelectionList() {
return selectionList;
}
function insertRecord(dataId, dataName, dataParentId) {
dataTable.push([dataId, dataName, dataParentId]);
}
function insertRecords(records) {
for (const [dataId, dataName, dataParentId] of records)
insertRecord(dataId, dataName, dataParentId)
}
function selectRecords(dataParentId) {
return dataTable.filter(d => d[2] === dataParentId);
}
function selectRecursive(dataParentId) {
let obj = ({ id :0 ,dataId: dataParentId });
for (const [dataId, dataName, _dataParentId] of selectRecords(dataParentId)) {
if (! ("nodes" in obj) ) obj.nodes = [];
obj.nodes.push({"id" :dataId , "dataName" :dataName, "nodes" : selectRecursive(dataId)});
}
return obj;
}
Rectangle {
id: rect
width: parent.width
height: parent.height
clip: true
radius: 10
RowLayout {
id: buttonRowLayout
implicitWidth: parent.width
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 20
spacing: 20
RoundButton {
id: expandButton
implicitWidth: buttonRowLayout.width /2 - 10
implicitHeight: 30
radius: 10
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 10
text: "Expand All"
}
background: Rectangle {
id: expandButtonRect
anchors.fill: parent
radius: 10
}
MouseArea {
anchors.fill: parent
onClicked: {
appTreeView.expandAll()
}
}
}
RoundButton {
id: collapseButton
implicitWidth: buttonRowLayout.width /2 - 10
implicitHeight: 30
radius: 10
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 10
text: "Collapse All"
}
background: Rectangle {
id: collapseButtonRect
anchors.fill: parent
radius: 10
}
MouseArea {
anchors.fill: parent
onClicked: {
appTreeView.collapseAll()
}
}
}
}
ScrollView {
id: treeViewScrollView
anchors.top: buttonRowLayout.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
contentHeight: appTreeView.height
contentWidth: appTreeView.implicitWidth
anchors.margins: 20
clip: true
AppTreeView {
id: appTreeView
indentation: 0
}
}
}
Component.onCompleted: {
for (let i = 0; i < dataModel.count; i++) {
let item = dataModel.get(i);
insertRecord(item[dataId], item[dataName], item[dataParentId]);
}
let m = selectRecursive(0);
appTreeView.model = m;
}
}
AppTreeView.qml :
import QtQuick 2.13
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.3
Column {
id: tv
property var model
property int indentation
function expandAll() {
//TODO
}
function collapseAll() {
//TODO
}
Repeater {
id : repeater
model: tv.model ? tv.model.nodes : 0
delegate: Column {
id : column
property bool isChecked: false
Row {
id: row
spacing: 10
height: 30
width: childrenRect.width
Item {
id: indentationItem
height: parent.height
width: indentation
}
RoundButton {
id: button
visible: {
var obj = modelData.nodes
if (! ("nodes" in obj) ) {
return false
} else {
return true
}
}
radius: 100
width: 25
height: width
text: "V"
rotation: isChecked ? 0 : -90
anchors.verticalCenter: parent.verticalCenter
anchors.left: indentationItem.right
background: Rectangle {
radius: 100
}
onClicked: {
isChecked = !isChecked;
if (isChecked) {
expand(modelData.nodes);
} else {
collapse();
}
}
}
CheckBox {
id: checkBox
checked: isItemSelected(modelData)
onCheckedChanged: {
if (checked === true) {
addToSelection(modelData);
} else if (checked === false){
removeFromSelection(modelData);
}
}
anchors.left: button.right
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: rect
width: childrenRect.width
height: 30
anchors.verticalCenter: parent.verticalCenter
radius: 10
anchors.left: checkBox.right
anchors.leftMargin: 10
Text {
anchors.verticalCenter: parent.verticalCenter
id: text
text: modelData.dataName
}
}
}
Loader {
id: loader
}
function expand(modelData) {
loader.setSource(
"AppTreeView.qml",
{ model: modelData,
indentation: indentation + 30
}
);
}
function collapse() {
loader.source = "Blank.qml";
}
function addToSelection(modelData) {
if (!isItemSelected(modelData)) {
console.log("Adding", modelData.dataName, "to selection");
selectionList.push(modelData.id);
selectionChanged();
}
}
function removeFromSelection(modelData) {
var index = selectionList.indexOf(modelData.id);
if (index !== -1) {
console.log("Removing", modelData.dataName, "from selection");
selectionList.splice(index, 1);
selectionChanged();
}
}
function isItemSelected(modelData) {
return selectionList.includes(modelData.id);
}
}
}
}
And Blank.qml display nothing :
import QtQuick 2.13
import QtQuick.Controls 2.5
Item {
}
Thank you for your help !
Currently, I do not recommend this structure for a tree view solution. There are much better options available to display a tree view.
In this answer, I have attempted to keep the base code intact and just added functions such as
expandAll,collapseAll, andexpandPath, along with some other modifications.expandAllis a recursive function that callsexpandAllfor all children.collapseAlljust hides and collapses the base children. Based on the current approach, this would destroy the child items, resetting them to the collapsed state.expandPathtakes an array of IDs from the root to a specific child. For example, for (id: 5),[2, 4, 5]should be provided. This array can also be retrieved from thepathToIdfunction inOctopusTreeView.expandPath Function:
Here, I have also used a recursive approach and called
expandPathfor inner children if the child is found.I also have used a trick to filter and convert
visibleChildreninto a JavaScript array.To expand an item, you only need to set
isChecked = true, and the item will expand.Other Modifications:
In my opinion, the current code requires significant refactoring. However, I have made some changes to make the source code cleaner and shorter:
componentfor inline reusable items.MouseAreainside buttons; they already haveonClickedand other signals.paletteto change the color ofControlcomponents.main.qml
OctopusTreeView.qml
AppTreeView.qml