import QtQuick import QtQuick.Controls import FluentUI.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects /** 树控件 作者:surfsky.cnblogs.com 2014-10 协议:MIT 请保留本文档说明 功能 /递归树显示 /左侧一个箭头,点击可展开显示子树 /选中节点变色 /节点点击事件 /tag属性,携带类似id的数据 异步方式,点击箭头后请求子数据。异步模式的话,节点要加上isLeaf属性,点击箭头后动态加载数据 使用 TreeView { anchors.fill: parent id: tree model: modelTree2 onSelectedItemChanged: console.log(item.text) } ListModel { id: modelTree2 Component.onCompleted: { modelTree2.append([ {title: "Node 1"}, {title: "Node 2", items: [ {title: "Node 21", items:[ {title: "Node 211"}, {title: "Node 212"} ]}, {title: "Node 22"} ] }, {title: "Node 3"} ]); } } 参考 http://qt-project.org/forums/viewthread/30521/ */ ScrollView { id: view // frameVisible: true clip: true // 输入属性 property var model property int rowHeight: 30 property int columnIndent: 15 property int lineHeight: 1 property string expanderImage : "qrc:/qt/qml/Gallery/res/image/components/expand.png" // 私有属性 property var currentNode // 当前节点数据 property var currentItem // 当前节点UI property var start_drag: false // 信号 signal selectedItemChanged(var item) Timer { id: timer // Start the timer and execute the provided callback on every X milliseconds function startTimer(callback, milliseconds) { timer.interval = milliseconds; timer.repeat = false; timer.triggered.connect(callback); timer.start(); } // Stop the timer and unregister the callback function stopTimer(callback) { timer.stop(); timer.triggered.disconnect(callback); } } // 节点数据展示的UI property Component delegate: Label { id: label text: model.title ? model.title : 0 color: currentItem === model ? "#2a2a2a" : "white" elide: Text.ElideRight property var tag : model.tag } // Loader { id: content onLoaded: item.isRoot = true sourceComponent: treeBranch property var items: model // 背景条纹 // Column { // anchors.fill: parent // Repeater { // model: 1 + Math.max(view.contentItem.height, view.height) / rowHeight // Rectangle { // objectName: "Faen" // color: index % 2 ? "#eee" : "white" // width: view.width ; height: rowHeight // } // } // } // 树节点组件 Component { id: treeBranch Item { id: root property bool isRoot: false implicitHeight: column.implicitHeight implicitWidth: column.implicitWidth // Rectangle{ // width: { // console.log(column.implicitHeight + "," + column.implicitWidth) // return 0 // } // height: column.implicitHeight // color: "cyan" // } Column { id: column x: 2 Item { height: isRoot ? 0 : rowHeight; width: 1 } Repeater { model: items Item { id: filler width: Math.max(loader.width + columnIndent, row.width) height: Math.max(row.height, loader.height) property var _model: model // 当前行背景色块--可拖拽! Rectangle { id: rowfill x: view.mapToItem(rowfill, 0, 0).x width: view.width height: filler.height // rowHeight visible: currentItem === model color: Qt.rgba(15/255,147/255,83/255, 0.1) border.color: Qt.rgba(34/255,197/255,94/255, 0.3) border.width: 1 } Rectangle { id: rowfill_cur x: view.mapToItem(rowfill_cur, 0, 0).x width: view.width height: rowHeight visible: currentItem === model color: Qt.rgba(15/255,147/255,83/255, 0.1) gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0.0; color: "#77ED8B" } GradientStop { position: 1.0; color: "#22C55E" } } radius: 5 } Rectangle{ id: dragRect x: view.mapToItem(rowfill, 0, 0).x width: view.width height: filler.height color: "transparent" MouseArea { id: mouseArea anchors.fill: parent drag.target: dragRect onPressed: { // timer.startTimer(() => { view.start_drag = true dragRect.Drag.active = true dragRect.Drag.source = dragRect // }, 100) } onReleased: { timer.stopTimer(()=> { }) if (view.start_drag === true) { dragRect.Drag.drop() dragRect.Drag.active = false dragRect.x = 0 dragRect.y = 0 } } onClicked: { currentItem = model console.log("click modelId =" + model.id) } // hoverEnabled: true // onEntered: { // // console.log(rowfill_cur.mapToItem(view, 0, 0).x) // } // onExited: { // } } Drag.active: mouseArea.drag.active Drag.dragType: Drag.Automatic Drag.hotSpot.x: dragRect.width / 2 Drag.hotSpot.y: dragRect.height / 2 Drag.mimeData: { "uuid": model.id } } // 行数据UI Row { id: row // 行图标 Item { width: rowHeight height: rowHeight opacity: (!!model.items && !!model.items.count > 0) ? 1 : 0 Image { id: expander source: view.expanderImage height: view.rowHeight * 0.6 fillMode: Image.PreserveAspectFit opacity: mouse.containsMouse ? 1 : 0.7 anchors.centerIn: parent rotation: loader.expanded ? 90 : 0 Behavior on rotation {NumberAnimation { duration: 120}} ColorOverlay{ anchors.fill: parent color: "#2a2a2a" source: parent visible: currentItem === model } } MouseArea { id: mouse anchors.fill: parent hoverEnabled: true onClicked: loader.expanded = !loader.expanded } } Item{ width: 19 height: rowHeight Image{ width: 14 height: 12 anchors.centerIn: parent fillMode: Image.PreserveAspectFit source: "qrc:/qt/qml/Gallery/res/image/components/" + model.ico ColorOverlay{ anchors.fill: parent color: "#2a2a2a" source: parent visible: currentItem === model } } } // 行文本 Loader { width: { // console.log(view.width + "," + rowfill_cur.x) return view.width + rowfill_cur.x - rowHeight - 20 - 60 } property var model: _model sourceComponent: delegate anchors.verticalCenter: parent.verticalCenter } RowLayout{ width: 60 spacing: 5 Layout.rightMargin: 5 Rectangle{ Layout.preferredWidth: 25 Layout.preferredHeight: 25 color: "transparent" radius: 5 Image{ source: "qrc:/qt/qml/Gallery/res/image/components/tree/ico_unlock.png" width: 14 height: 12 // icon.color: currentItem === model ? "#2a2a2a" : "#ffffff" ColorOverlay{ anchors.fill: parent color: "#2a2a2a" source: parent visible: currentItem === model } anchors.centerIn: parent } MouseArea{ anchors.fill: parent hoverEnabled: true onEntered: parent.color = Qt.rgba(255/255,255/255,255/255, 0.05) onExited: parent.color = "transparent" } } Rectangle{ Layout.preferredWidth: 25 Layout.preferredHeight: 25 color: "transparent" radius: 5 Image{ source: "qrc:/qt/qml/Gallery/res/image/components/tree/ico_eye.png" width: 14 height: 12 ColorOverlay{ anchors.fill: parent color: "#2a2a2a" source: parent visible: currentItem === model } anchors.centerIn: parent } MouseArea{ anchors.fill: parent hoverEnabled: true onEntered: parent.color = Qt.rgba(255/255,255/255,255/255, 0.05) onExited: parent.color = "transparent" } } } } DropArea { id: mid_area width: view.width height: rowHeight - 10 anchors.left: parent.left anchors.top: parent.top Rectangle{ id: top_area_rect width: parent.width height: rowHeight color: "transparent" opacity: 0.2 } onEntered: { top_area_rect.color = "white" } onExited: { top_area_rect.color = "transparent" } onDropped: { top_area_rect.color = "transparent" var sourceId = drop.getDataAsString("uuid") var targetId = model.id if (sourceId === targetId) { console.log("same id!") return } var sinfo = view.getInfoById(sourceId, view.model) var einfo = view.getInfoById(targetId, view.model) if (sinfo.mm == einfo.mm) { // 先添加,后删除。 einfo.mm.get(einfo.index).items.append(sinfo.mm.get(sinfo.index)) sinfo.mm.remove(sinfo.index) } else { var tmp = view.getInfoById(targetId, sinfo.mm.get(sinfo.index).items) if (!tmp) { console.log('no same parent...OK') // 先添加,后删除。 einfo.mm.get(einfo.index).items.append(sinfo.mm.get(sinfo.index)) sinfo.mm.remove(sinfo.index) } else { console.log('error luoji.') } } console.log("sin=" + sinfo.index + ",ein=" + einfo.index) } } DropArea { width: view.width height: rowHeight * 0.2 anchors.left: parent.left anchors.bottom: parent.bottom Rectangle{ id: bot_area_rect width: parent.width height: lineHeight anchors.left: parent.left anchors.bottom: parent.bottom color: "transparent" } onEntered: { bot_area_rect.color = "white" } onExited: { bot_area_rect.color = "transparent" } onDropped: { bot_area_rect.color = "transparent" var sourceId = drop.getDataAsString("uuid") var targetId = model.id if (sourceId === targetId) { console.log("same id!") return } var sinfo = view.getInfoById(sourceId, view.model) var einfo = view.getInfoById(targetId, view.model) if (sinfo.mm !== einfo.mm) { var tmp = view.getInfoById(targetId, sinfo.mm.get(sinfo.index).items) if (!tmp) { console.log("OK...") var ryAdd = sinfo.mm.get(sinfo.index) var addTmp = {id:ryAdd.id, title: ryAdd.title, items: ryAdd.items} console.log(einfo.index + "------") einfo.mm.insert(einfo.index + 1, ryAdd) sinfo.mm.remove(sinfo.index) } } else { if (sinfo.index < einfo.index) { sinfo.mm.move(sinfo.index, einfo.index, 1) } else if (sinfo.index > einfo.index) { sinfo.mm.move(sinfo.index, einfo.index + 1, 1) } } } } // 子树(递归自身) Loader { id: loader x: columnIndent height: expanded ? implicitHeight : 0 property var node: model property bool expanded: false property var items: model.items property var text: model.title sourceComponent: (expanded && !!model.items) ? treeBranch : undefined } } } } } } } function getInfoById(id, paModel) { var index = -1 var mm = null if (paModel) { for (var i = 0;i 0) { var info = getInfoById(id, paModel.get(i).items) if (info) { index = info.index mm = info.mm break } } if (paModel.get(i).id == id) { index = i mm = paModel break } } } if (mm) { return {index, mm} } return null } }