如何为我的自定义 QML TreeView 制作全部展开/全部折叠功能?

问题描述 投票:0回答:1

对于一个基于Python作为后端和QML作为前端的Qt项目,我制作了一个自定义组件,用于将数据显示为树结构。该组件使用 ListModel、模型中存储数据名称的字段名称(因此我可以为我拥有的任何模型重用此组件)、模型中包含 id 的字段名称以及包含数据父 ID 的模型字段的名称(出于同样的原因)。

每个数据都可以有一个父级,因此如果数据至少有一个子级,组件会通过在数据行上显示一个圆形按钮来处理此问题,而当数据没有子级时则不显示它。

该组件使用Qt的Loader组件来处理数据显示的递归。

我想了解如何使用我当前处理数据层次结构的方法来实现 expandAllcollapseAll 函数,此外,我如何能够根据其 id 显示数据,例如:

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
  

例如,如果我想查看“数据 5”,如何使我的树结构显示“数据 1”、“数据 2”、“数据 4”和“数据 5”?这样,我的意思是,上述所有数据的父级如何使用我的代码中声明的扩展方法,以便显示通向特定数据的整个路径?

这是我当前使用的代码。非常感谢 Stephen Quan 在上一篇文章中帮助我了解如何在 QML 中显示数据 :

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

TreeView {
    id: customerTreeView
    width: 500
    height: 500
    dataModel: customerDataModel
    dataId: "CustomerID"
    dataName: "CustomerName"
    dataParentId: "CustomerParentId"
}

TreeView.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);
            }
        }
    }
}

Blank.qml 不显示任何内容:

import QtQuick 2.13
import QtQuick.Controls 2.5

Item {
}

谢谢您的帮助!

qt qml treeview qt5
1个回答
1
投票

目前,我不推荐将这种结构用于树视图解决方案。有更好的选项可用于显示树视图。

在这个答案中,我尝试保持基本代码完整,只是添加了

expandAll
collapseAll
expandPath
等功能,以及一些其他修改。

  • expandAll
    是一个递归函数,为所有子级调用
    expandAll
  • collapseAll
    只是隐藏并折叠基础子级。根据当前的方法,这将销毁子项目,将它们重置为折叠状态。
  • expandPath
    获取从根到特定子级的 ID 数组。例如,对于 (id: 5),应提供
    [2, 4, 5]
    。该数组也可以从
    pathToId
    中的
    OctopusTreeView
    函数检索。

扩展路径功能:

这里,我也使用了递归方法,如果找到了内部子节点,则调用

expandPath

我还使用了一个技巧来过滤
visibleChildren
并将其转换为 JavaScript 数组。

要展开某个项目,只需设置

isChecked = true
,该项目就会展开。

function expandPath(path) {
    if (path && path.length) {
        const current = path[0];
        const nodes = Array.from(visibleChildren).filter(n => 'nodes' in n);
        const node = nodes.find(n => n.nodeId == current);

        if (node) {
            node.isChecked = true; // Expand node

            const innerItem = node.loader.item;
            if (innerItem.expandPath) innerItem.expandPath(path.slice(1));
        }
    }
}

其他修改:

在我看来,当前的代码需要大量重构。不过,我做了一些更改,使源代码更清晰、更短:

  • 使用
    component
    来表示内联可重复使用的项目。
  • 避免在行/列/布局内使用锚点。
  • 避免在按钮内使用
    MouseArea
    ;他们已经有
    onClicked
    和其他信号。
  • 使用
    palette
    更改
    Control
    组件的颜色。
  • 使按钮可检查(如果适用)。
  • 等等

main.qml

palette { // Try using palettes in Qt 5 to set button and window colors.
    base: '#badfd7'
    window: '#f1f2f3'
    button: '#60bfc1'
    highlight: '#fdb7b9'

    text: '#343536'
    windowText: '#343536'
    buttonText: '#f1f2f3'
    highlightedText: '#343536'
}

ListModel { /* ... */}
OctopusTreeView { /* ... */ }

OctopusTreeView.qml

Page {
    id: page

    property var dataTable: ([])
    property var dataModel: []

    property string dataId: ""
    property string dataName: ""
    property string dataParentId: ""
    property var selectionList: []
    signal selectionChanged()

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

    function pathToId(id) {
        let path = [];

        while(id) {
            const target = dataTable.find(([i, n, p]) => i === id);
            if(target) {
                path.unshift(id);
                id = target[2];
            } else {
                return undefined;
            }
        }

        return path;
    }

    spacing: 5
    padding: 5

    component HeaderBtn: RoundButton {
        Layout.fillWidth: true
        Layout.fillHeight: true

        radius: 3
        font.bold: true
    }

    header: Control {
        height: 35
        padding: 3
        contentItem: RowLayout {
            spacing: 3

            HeaderBtn {
                text: "Expand All"
                onClicked: appTreeView.expandAll();
            }

            HeaderBtn {
                text: "Collapse All"
                onClicked: appTreeView.collapseAll();
            }

            HeaderBtn {
                text: "Expand id: 5 (Amazon FR)"
                onClicked: appTreeView.expandPath(pathToId(5) ?? []);
            }
        }
    }

    contentItem:  ScrollView {
        id: treeViewScrollView
        implicitHeight: contentHeight
        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]);
        }
        appTreeView.model = selectRecursive(0);
    }
}

AppTreeView.qml

Column {
    id: tv
    property var model
    property int indentation

    function expandAll() {
        const nodes = Array().filter.call(visibleChildren, n => 'nodes' in n)
        nodes.forEach(n => {
            n.isChecked = true; // Expand node

            const innerItem = n.loader.item;
            if(innerItem && innerItem.expandAll) innerItem.expandAll();
        });
    }

    function collapseAll() {
        const nodes = Array().filter.call(visibleChildren, n => 'nodes' in n)
        nodes.forEach(n => {
            /// Based on the current solution, the inner items get destroyed, so there is no need for recursive traversal.
            n.isChecked = false;
        });
    }

    function expandPath(path) {
        if(path && path.length) {
            const current = path[0];
            const nodes = Array().filter.call(visibleChildren, n => 'nodes' in n)
            const node = nodes.find(n => n.nodeId == current);

            if(node) {
                node.isChecked = true; // Expand node

                const innerItem = node.loader.item;
                if(innerItem.expandPath) innerItem.expandPath(path.slice(1));
            }
        }
    }

    Repeater {
        model: tv.model ? tv.model.nodes : 0
        delegate: Column {
            property alias isChecked: button.checked
            property alias loader: loader

            property string nodeId: modelData.id
            property string nodeName: modelData.dataName
            property var nodes: modelData.nodes

            Grid {
                spacing: 5
                width: childrenRect.width
                verticalItemAlignment: Qt.AlignVCenter

                leftPadding: indentation + (button.visible ? 0 : (25 + spacing))

                RoundButton {
                    id: button
                    visible: "nodes" in modelData.nodes
                    checkable: true
                    radius: 5
                    width: 25; height: width
                    text: isChecked ? "-" : "+"
                    font.bold: true

                    onCheckedChanged: checked ? expand(nodes) : collapse();
                }

                CheckBox {
                    padding: 0
                    spacing: 0
                    checked: isItemSelected(modelData)
                    onCheckedChanged: checked ? addToSelection(modelData) :removeFromSelection(modelData)
                    indicator{ width: 25; height: 25 }
                    Component.onCompleted: indicator.radius = 3
                }

                Label {
                    id: text
                    text: nodeName
                    padding: 5
                    background: Rectangle {
                        border { width: 1; color: palette.mid }
                        radius: 3
                    }
                }
            }

            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);
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.