对于一个基于Python作为后端和QML作为前端的Qt项目,我制作了一个自定义组件,用于将数据显示为树结构。该组件使用 ListModel、模型中存储数据名称的字段名称(因此我可以为我拥有的任何模型重用此组件)、模型中包含 id 的字段名称以及包含数据父 ID 的模型字段的名称(出于同样的原因)。
每个数据都可以有一个父级,因此如果数据至少有一个子级,组件会通过在数据行上显示一个圆形按钮来处理此问题,而当数据没有子级时则不显示它。
该组件使用Qt的Loader组件来处理数据显示的递归。
我想了解如何使用我当前处理数据层次结构的方法来实现 expandAll 和 collapseAll 函数,此外,我如何能够根据其 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 {
}
谢谢您的帮助!
目前,我不推荐将这种结构用于树视图解决方案。有更好的选项可用于显示树视图。
在这个答案中,我尝试保持基本代码完整,只是添加了
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);
}
}
}
}