492 lines
22 KiB
QML
492 lines
22 KiB
QML
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<paModel.count;i++) {
|
||
if (paModel.get(i).items !== undefined && paModel.get(i).items.count > 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
|
||
}
|
||
}
|