我正在尝试使用QML创建看板视图。
下面是目前为止我的代码:https://gist.github.com/nuttyartist/66e628a53f014118055474b5823e5c4b一切似乎都工作得相当好,包括在任务的同一个ListView中拖放,但拖放到任务的不同ListView中不起作用。我想重新创造同样的流畅的拖放感觉,就像我现在对同一个ListView一样。
app的GIF
我试着:
- 将被删除任务的模型插入到新的ListView
- 告诉委托它正在被"保持",所以它开始拖动它并移动到光标位置
- 从ListView中删除前一项
但是它不起作用,我有点卡住了。如有任何帮助,不胜感激。
这个答案是一个正在进行的工作,因为有许多功能需要实现。
ListModel之一。
在您的示例中,您正朝着实现3个ListModel,一个用于Todo,一个用于InProgress,一个用于Done。我建议您将所有模型组合在一起,并使用taskType属性来确定任务是"todo"还是"In progress"。或"Done".
ListModel {
id: tasks
}
Component.onCompleted: {
for (let i = 0; i < 30; i++) {
// let taskId = ...
// let taskType = ...
// let taskText = ...
tasks.append( { taskId, taskType, taskText } );
}
}
可配置的可视化ListViews。
我们创建了3个ListView实例,它们都指向同一个底层任务ListModel,例如
ListView {
model: tasks
//...
}
ListView {
model: tasks
//...
}
ListView {
model: tasks
//...
}
实例的不同是因为每个ListView可以实现不同的过滤器,从而显示来自ListModel的不同记录。
拖放UI/UX
当用户开始拖动一个项目时,我们将源项目设置为"拖动"。主动到真实。对于目标,我们观察containsDrag属性。
我们通过改变颜色和z轴顺序让UI/UX看起来更好,让用户看到被拖动的项目。
同样,当从一个ListView拖动到另一个ListView时,有必要设置clip: false
,这样被拖动的项目就不受源ListView的边界限制。当拖拽完成后,我们恢复clip: true
。
更新ListModel/DelegateModel
当用户完成拖放操作时,我们需要做以下操作:
- 更新ListModel以反映taskType 的变化
- 更新两个DelegateModels以实现taskType更改,因此它从一个可视化模型中删除并添加到另一个
- (尚未实现)更新DelegateModel,使任务出现在用户期望的位置索引
可运行演示
这是一个可运行的演示,目前已经实现了:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
id: main
background: Rectangle { color: "black" }
property int dragSource: -1
property int dragTarget: -1
RowLayout {
width: parent.width
height: 360
ListView {
Layout.fillWidth: true
Layout.preferredWidth: 100
Layout.fillHeight: true
model: tasks
clip: true
headerPositioning: ListView.OverlayHeader
header: TaskHeaderDelegate {
text: "TODO"
count: tasks.countByType("TODO")
}
delegate: TaskItemDelegate { visible: taskType === 'TODO' }
}
ListView {
Layout.fillWidth: true
Layout.preferredWidth: 100
Layout.fillHeight: true
model: tasks
clip: true
headerPositioning: ListView.OverlayHeader
header: TaskHeaderDelegate {
text: "In Progress"
count: tasks.countByType("In Progress")
}
delegate: TaskItemDelegate { visible: taskType === 'In Progress' }
}
ListView {
Layout.fillWidth: true
Layout.preferredWidth: 100
Layout.fillHeight: true
clip: true
model: tasks
headerPositioning: ListView.OverlayHeader
header: TaskHeaderDelegate {
text: "Done"
count: tasks.countByType("Done")
}
delegate: TaskItemDelegate { visible: taskType === 'Done' }
}
}
ListModel {
id: tasks
function countByType(taskType) {
let total = 0;
for (let i = 0; i < count; i++)
if (get(i).taskType === taskType) ++total;
return total;
}
}
Component.onCompleted: {
for (let i = 0; i < 30; i++) {
let taskId = Math.floor(Math.random() * 1000);
let taskType = ["TODO","In Progress","Done"][Math.floor(Math.random()*3)];
let taskText = "Task " + taskId;
tasks.append({taskId,taskType,taskText});
}
}
footer: Frame {
Label {
text: "Dragging: " + tasks.get(dragSource).taskText + " -> " + tasks.get(dragTarget).taskText
color: "yellow"
visible: dragSource !== -1 && dragTarget !== -1
}
}
}
// TaskItemDelegate.qml
import QtQuick
import QtQuick.Controls
Item {
id: taskItem
z: mouseArea.drag.active ? 2 : 0
property ListView listView: ListView.view
property bool show: true
width: listView.width
height: visible ? 50 : 0
DropArea {
anchors.fill: parent
z: 2
onContainsDragChanged: {
dragTarget = containsDrag ? index : -1;
}
Rectangle {
anchors.fill: parent
border.color: parent.containsDrag ? "yellow" : "grey"
color: "transparent"
border.width: 1
radius: 10
}
}
Rectangle {
width: parent.width
height: parent.height
color: "#444"
border.color: "white"
radius: 10
z: mouseArea.drag.active ? 2 : 0
Drag.active: mouseArea.drag.active
Label {
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: 10
text: index
color: "#666"
}
Label {
anchors.centerIn: parent
text: taskText
color: "white"
}
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: parent
onPressed: {
dragSource = index;
dragTarget = -1;
listView.z = 2;
listView.clip = false;
}
onReleased: {
drag.target.x = 0;
drag.target.y = 0;
listView.z = 0
listView.clip = true;
if (dragSource !== -1 && dragTarget !== -1 && dragSource !== dragTarget) {
tasks.setProperty(dragSource, "taskType", tasks.get(dragTarget).taskType);
tasks.move(dragSource, dragTarget, 1);
}
}
}
}
}
// TaskHeaderDelegate.qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Rectangle {
id: header
property ListView listView: ListView.view
width: listView.width
height: 50
color: "black"
property string text: ""
property int count: 0
RowLayout {
anchors.centerIn: parent
spacing: 20
Label {
text: header.count
color: "white"
background: Rectangle {
anchors.fill: parent
anchors.margins: -radius
radius: 5
color: "#666"
}
}
Label {
text: header.text
color: "white"
}
}
}
你可以在网上试试!
所以,我想出了一个(有点)hack的解决方案。
每当从不同的ListView将任务输入到任务中时,我将目标内容的父元素变大,并将内容向上或向下推(取决于y轴,尽管那里的代码可以改进)。当这种情况发生时,我将目标对象绑定到被拖动的对象,所以每当被拖动的对象被释放时,它会将其内容添加到不同的ListView的正确位置。然后我从原来的ListView模型中移除被拖动的对象。
完整代码:
main.qml
import QtQuick
import QtQuick.Controls
Window {
id: root
width: 1200
height: 480
visible: true
title: qsTr("TODOs")
property int textAndTodosSpacing: 20
property int todoColumnWidth: 250
property int topAndBottomColumsMargins: 20
Rectangle {
id: appBackgroundContainer
anchors.fill: parent
color: "#0D1117"
Row {
spacing: textAndTodosSpacing
Rectangle {
id: textContainer
width: 250
height: 250
color: "transparent"
border.width: 1
border.color: "black"
y: (root.height / 2) - (height / 2)
ScrollView {
width: parent.width
height: parent.height
TextArea {
width: parent.width
height: parent.height
color: "white"
}
}
}
Item {
id: todosContainer
width: root.width - textContainer.width - textAndTodosSpacing - root.topAndBottomColumsMargins
height: root.height
anchors.top: parent.top
anchors.topMargin: root.topAndBottomColumsMargins
anchors.bottomMargin: root.topAndBottomColumsMargins
DelegateModel {
id: visualModel
model: TodoColumnModel {}
delegate: TodoColumnDelegate {
todoColumnContentHeight: todosContainer.height - root.topAndBottomColumsMargins*3
rootContainer: todosContainer
}
}
ListView {
id: todosColumnsView
model: visualModel
anchors { fill: parent }
clip: true
orientation: ListView.Horizontal
spacing: 30
cacheBuffer: 20
}
}
}
}
}
TodoColumnDelegate.qml
import QtQuick
import QtQuick.Controls
MouseArea {
id: dragArea
property bool held: false
property int todoColumnContentHeight: 300
property var rootContainer
anchors { top: parent.top; bottom: parent.bottom }
width: todoColumnContent.width
drag.target: held ? todoColumnContent : undefined
drag.axis: Drag.XAxis
onPressed: held = true
onReleased: held = false
Rectangle {
id: todoColumnContent
radius: 20
width: 250
height: dragArea.todoColumnContentHeight
border.width: 1
antialiasing: true
border.color: Qt.rgba(33, 38, 45)
color: "#010409"
scale: dragArea.held ? 1.05 : 1.0
anchors {
id: todoColumnContentAnchors
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
Drag.active: dragArea.held
Drag.source: dragArea
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: "todoColumn"
Behavior on scale {
ScaleAnimator {
duration: 300
easing.type: Easing.InOutQuad
}
}
// // TODO: Mkae anchor animation work...
// // I think the solution is to call the transitions when held is false
// // And we need to change the parent as well?
// transitions: Transition {
// enabled: dragArea.held === false
// AnchorAnimation {
// duration: 300
// easing.type: Easing.InOutQuad
// }
// }
states: [State {
when: dragArea.held
ParentChange { target: todoColumnContent; parent: dragArea.rootContainer}
AnchorChanges {
target: todoColumnContent
anchors { horizontalCenter: undefined; verticalCenter: undefined }
}
}]
Rectangle {
id: numberOfTasksText
x: columnName.x - 28
y: columnName.y + columnName.height/2 - height/2
width: 20
height: 20
radius: 20
color: "#2d3139"
Text {
anchors.centerIn: parent
color: "white"
font.pointSize: 11
text: "49"
}
}
Text {
id: columnName
x: (todoColumnContent.width / 2) - (width / 2)
y: 10
text: model.title
color: "white"
}
// Tasks
Item {
id: tasksContainer
property int marginTop: 10
width: todoColumnContent.width
height: todoColumnContent.height - (columnName.y + columnName.height + marginTop * 2)
y: columnName.y + columnName.height + marginTop
DelegateModel {
id: tasksVisualModel
model: TodoTaskModel {}
delegate: TodoTaskDelegate {
taskContentWidth: tasksContainer.width
rootContainer: dragArea.rootContainer
listViewParent: tasksView
}
}
ListView {
id: tasksView
model: tasksVisualModel
anchors { fill: parent }
clip: true
orientation: ListView.Vertical
spacing: 10
cacheBuffer: 50
}
}
}
DropArea {
anchors { fill: parent}
keys: ["todoColumn"]
onEntered: (drag)=> {
visualModel.items.move(
drag.source.DelegateModel.itemsIndex,
dragArea.DelegateModel.itemsIndex)
}
}
}
TodoColumnModel.qml
import QtQuick
ListModel {
id: todoColumnModel
ListElement {
title: "TODO"
}
ListElement {
title: "In Progress"
}
ListElement {
title: "Done"
}
}
TodoTaskDelegate.qml
import QtQuick
import QtQuick.Controls
MouseArea {
id: taskDragArea
property bool held: false
property int taskContentWidth
// TODO: replace var with proper types
property var rootContainer
property var listViewParent
property int originalIndex: -1
property var listViewModel: model
property string taskText: model.taskText
property int originalHeight
property var targetDragged
anchors { left: parent.left; right: parent.right }
width: taskContent.width
height: taskContent.height
drag.target: held ? taskContent : undefined
drag.axis: Drag.XAndYAxis
onPressed: {
held = true;
originalIndex = DelegateModel.itemsIndex;
}
onReleased: {
if(targetDragged) {
targetDragged.listViewParent.model.items.insert(targetDragged.DelegateModel.itemsIndex, {taskText: model.taskText});
listViewParent.model.items.remove(DelegateModel.itemsIndex);
}
held = false;
}
Component.onCompleted: {
originalHeight = taskContent.height;
}
Rectangle {
id: taskContent
radius: 10
width: taskDragArea.taskContentWidth -20
height: 60
border.width: 1
antialiasing: true
border.color: "#30363d"
color: "#161b22"
scale: taskDragArea.held ? 1.07 : 1.0
anchors {
id: taskContentAnchors
horizontalCenter: parent.horizontalCenter
// verticalCenter: parent.verticalCenter
}
anchors.leftMargin: 5
anchors.rightMargin: 5
Drag.active: taskDragArea.held
Drag.source: taskDragArea
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.keys: "task"
Behavior on scale {
ScaleAnimator {
duration: 300
easing.type: Easing.InOutQuad
}
}
// transitions: Transition {
// AnchorAnimation { duration: 100 }
// }
states: [State {
when: taskDragArea.held
ParentChange { target: taskContent; parent: taskDragArea.rootContainer}
AnchorChanges {
target: taskContent
anchors { top: undefined; horizontalCenter: undefined; verticalCenter: undefined }
}
}
]
Text {
id: taskTextDescription
x: (taskContent.width / 2) - (width / 2)
y: 10
text: model.taskText
color: "white"
}
}
DropArea {
id: dropArea
anchors { fill: parent}
keys: ["task"]
onEntered: (drag)=> {
drag.source.targetDragged = taskDragArea
var sourceListView = drag.source.listViewParent;
var targetListView = taskDragArea.listViewParent;
if (sourceListView === targetListView) {
// Move the task within the same ListView
sourceListView.model.items.move(
drag.source.DelegateModel.itemsIndex,
taskDragArea.DelegateModel.itemsIndex
);
} else {
// Handle different ListView
taskDragArea.height = taskDragArea.originalHeight * 2 + taskDragArea.listViewParent.spacing;
if (drag.source.y > taskDragArea.y) {
taskContent.anchors.top = taskDragArea.top
taskContent.anchors.bottom = undefined
} else {
taskContent.anchors.top = undefined
taskContent.anchors.bottom = taskDragArea.bottom
}
}
}
onExited: {
drag.source.targetDragged = null;
var sourceListView = drag.source.listViewParent;
var targetListView = taskDragArea.listViewParent;
if (sourceListView !== targetListView) {
taskDragArea.height = taskDragArea.originalHeight;
taskContent.anchors.top = undefined;
taskContent.anchors.bottom = undefined;
}
}
}
}
TodoTaskModel.qml
import QtQuick
ListModel {
id: todoTaskModel
ListElement {
taskText: "Task 1"
}
ListElement {
taskText: "Task 2"
}
ListElement {
taskText: "Task 3"
}
ListElement {
taskText: "Task 4"
}
ListElement {
taskText: "Task 5"
}
ListElement {
taskText: "Task 6"
}
ListElement {
taskText: "Task 7"
}
ListElement {
taskText: "Task 8"
}
}