Перетаскивание в разные ListViews с помощью QML (приложение Kanban)

Я пытаюсь создать представление Канбан с помощью QML.

Вот мой код: https://gist.github.com/nuttyartist/66e628a53f014118055474b5823e5c4b

Кажется, все работает достаточно хорошо, включая перетаскивание в тот же ListView задач, но перетаскивание в другой ListView задач не работает. Я хочу воссоздать те же ощущения плавного перетаскивания, которые я сейчас испытываю с тем же ListView.

GIF приложения

Я пытался:

  1. Вставка модели отброшенной задачи в новый ListView
  2. Сообщить делегату, что он «удерживается», поэтому он начинает перетаскивать его и переходит к местоположению курсора.
  3. Удалить предыдущий элемент из ListView

Но это не работает, и я немного застрял. Любая помощь будет оценена по достоинству.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
116
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Этот ответ находится в стадии разработки, поскольку необходимо реализовать несколько функций.

Одна модель списка.

В вашем примере вы стремились реализовать 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

Когда пользователь завершает операцию перетаскивания, нам нужно сделать следующее:

  1. Обновите ListModel, отражая изменение типа задачи.
  2. Обновите два DelegateModels, чтобы понять, что taskType изменился, чтобы он был удален из одной визуальной модели и добавлен в другую.
  3. (Еще не реализовано) Обновите 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"
        }
    }
}

Вы можете Попробовать онлайн!

Эй, Стивен! Мне нравится ваша работа (и я видел здесь множество ваших комментариев по вопросам QML, которые немного помогли). Ваш пример не дает ответа на мою проблему. - Во-первых, перетаскивание элемента в другой ListView не вставляет его туда (в чем мне нужна помощь). - Во-вторых, я использую совсем другую структуру, чем ваша (описано в doc.qt.io/qt-6/…), поэтому немного сложно понять, что я могу сделать из вашего кода. — В-третьих, я использую один ListView для канбан-досок, а другой — для задач внутри делегата доски.

Notes 03.04.2023 15:03

@Примечания, на самом деле, ваш вопрос довольно сложный, по моим оценкам, для получения исчерпывающего ответа потребуется более 3 дней, поэтому я могу делать добавочные обновления ответа только тогда, когда я в состоянии. Структура моего ответа и документации Qt практически одинакова. Дьявол кроется в деталях, следовательно, степень работы по настройке в соответствии с вашими требованиями. На самом деле было бы полезно, если бы вы предоставили ссылку на слово «Канбан», поскольку я не знаком с этим термином. Кстати, сегодня я добавил еще одно обновление к ответу. К сожалению, окончательная редакция до сих пор отсутствует.

Stephen Quan 04.04.2023 01:59

Привет, я ценю вашу помощь. Ваш пример не работает последовательно. Много раз он не уронит предмет в нужном месте. Но главная проблема заключается в том, что вы предлагаете совершенно другую архитектуру для моей проблемы (используя ту же модель и т. д.), в то время как я ищу решение, которое работает с моей текущей архитектурой.

Notes 04.04.2023 09:02

Я думаю, что все, что мне нужно добавить в функции DropArea onEntered, это: // Вставить исходную модель в targetListView в позиции // Установить drag.source.held = false, чтобы остановить ее перетаскивание // Сделать делегата вставленным в targetListView активно перетаскивается в позицию курсора // Удалить drag.source из модели sourceListView

Notes 04.04.2023 09:02

Мне удалось получить желаемый эффект с помощью некоторого взлома внутри onEntered, но сигнал onDropped не вызывается. gist.github.com/nuttyartist/a910f59e334ac944f99a413ef0783e02 Знаете почему?

Notes 04.04.2023 17:07
Ответ принят как подходящий

Итак, я придумал (своего рода) хакерское решение.

Всякий раз, когда задача вводится в задачу из другого 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"
    }
}

Другие вопросы по теме