Я пытаюсь создать представление Канбан с помощью QML.
Вот мой код: https://gist.github.com/nuttyartist/66e628a53f014118055474b5823e5c4b
Кажется, все работает достаточно хорошо, включая перетаскивание в тот же ListView задач, но перетаскивание в другой ListView задач не работает. Я хочу воссоздать те же ощущения плавного перетаскивания, которые я сейчас испытываю с тем же ListView.
Я пытался:
Но это не работает, и я немного застрял. Любая помощь будет оценена по достоинству.
Этот ответ находится в стадии разработки, поскольку необходимо реализовать несколько функций.
Одна модель списка.
В вашем примере вы стремились реализовать 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 с перетаскиванием
Когда пользователь начинает перетаскивать элемент, мы устанавливаем для исходных элементов Drag.active значение true. Для пункта назначения мы наблюдаем за свойством containsDrag.
Мы улучшаем внешний вид UI/UX с помощью изменения цвета и изменения z-порядка, чтобы пользователь мог видеть перетаскиваемый элемент.
Кроме того, при перетаскивании из одного ListView в другой необходимо было установить clip: false
, чтобы перетаскиваемый элемент не ограничивался границами исходного ListView. Когда перетаскивание завершено, мы восстанавливаем clip: true
.
Обновление ListModel/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"
}
}
}
Вы можете Попробовать онлайн!
@Примечания, на самом деле, ваш вопрос довольно сложный, по моим оценкам, для получения исчерпывающего ответа потребуется более 3 дней, поэтому я могу делать добавочные обновления ответа только тогда, когда я в состоянии. Структура моего ответа и документации Qt практически одинакова. Дьявол кроется в деталях, следовательно, степень работы по настройке в соответствии с вашими требованиями. На самом деле было бы полезно, если бы вы предоставили ссылку на слово «Канбан», поскольку я не знаком с этим термином. Кстати, сегодня я добавил еще одно обновление к ответу. К сожалению, окончательная редакция до сих пор отсутствует.
Привет, я ценю вашу помощь. Ваш пример не работает последовательно. Много раз он не уронит предмет в нужном месте. Но главная проблема заключается в том, что вы предлагаете совершенно другую архитектуру для моей проблемы (используя ту же модель и т. д.), в то время как я ищу решение, которое работает с моей текущей архитектурой.
Я думаю, что все, что мне нужно добавить в функции DropArea onEntered, это: // Вставить исходную модель в targetListView в позиции // Установить drag.source.held = false, чтобы остановить ее перетаскивание // Сделать делегата вставленным в targetListView активно перетаскивается в позицию курсора // Удалить drag.source из модели sourceListView
Мне удалось получить желаемый эффект с помощью некоторого взлома внутри onEntered, но сигнал onDropped не вызывается. gist.github.com/nuttyartist/a910f59e334ac944f99a413ef0783e02 Знаете почему?
Итак, я придумал (своего рода) хакерское решение.
Всякий раз, когда задача вводится в задачу из другого 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"
}
}
Эй, Стивен! Мне нравится ваша работа (и я видел здесь множество ваших комментариев по вопросам QML, которые немного помогли). Ваш пример не дает ответа на мою проблему. - Во-первых, перетаскивание элемента в другой ListView не вставляет его туда (в чем мне нужна помощь). - Во-вторых, я использую совсем другую структуру, чем ваша (описано в doc.qt.io/qt-6/…), поэтому немного сложно понять, что я могу сделать из вашего кода. — В-третьих, я использую один ListView для канбан-досок, а другой — для задач внутри делегата доски.