Как стать автором
Обновить

Drag'n'Drop в QML — это просто! Или 5 шагов до цели

Время на прочтение 6 мин
Количество просмотров 18K
Этот пост участвует в конкурсе „Умные телефоны за умные посты“.

Drag'n'Drop является неоспоримо важным элементом взаимодействия пользователя и графического окружения. К сожалению, в QML нет встроенного механизма Drag'n'Drop для View. Поэтому, я написал небольшой пример на основе GridView с 16 изображениями.

Этот пример Drag'n'Drop-а не претендует на совершенство (есть несколько других реализаций, которые визуально возможно более совершенны), а больше преследует цель показать, что QML является очень гибким и простым средством разработки.

Для начала небольшое видео, а под катом 5 простых шагов для получения подобного результата.



Шаг 1. Создаем GridView


Начнем с создания GridView и небольшой модели. В качестве тестовых данных я взял стандартные изображения из Nokia N8.
Сделаем наш грид размером 4х4 изображения.
Rectangle {
    width: 420
    height: 420
    color: "#000000"

    Component {
        id: dndDelegate
        Item {
            id: wrapper
            width: dndGrid.cellWidth
            height: dndGrid.cellHeight
            Image {
                id: itemImage
                source: imagePath
                anchors.centerIn: parent
                width: 90
                height: 90
                smooth: true
                fillMode: Image.PreserveAspectFit
            }
        }
    }

    ListModel {
        id: dndModel
        ListElement { imagePath: "images/1.jpg" }
        //Еще 15 картинок
    }

    GridView {
        id: dndGrid
        anchors.fill: parent
        anchors.margins: 10
        cellWidth: 100
        cellHeight: 100
        model: dndModel
        delegate: dndDelegate
    }
}

После запуска в qmlviewer мы увидим примерно следующую картину


Шаг 2. Добавляем Drag'nDrop


Теперь нам нужно добавить в этот грид возможность перетаскивания картинок.

Для упрощения задачи отключим возможность прокрутки GridView. Можно обойтись и без этого (используя долгое нажатие вместо обычного нажатия для D'n'D и манипулируя свойством interactive (которое отвечает за то, будет ли прокручиваться GridView) в соответствующих коллбеках MouseArea), но это усложнит пример.

Также добавим элемент MouseArea размером во весь GridView, который и будет отлавливать нажатие мыши при начале драга, а также перемещать элемент на нужную позицию в модели при отпускании мыши. Плюс к этому, в GridView поместим еще дополнительный элемент dndContainer, о котором поговорим позже

Последним штрихом добавим в наш GridView свойство для хранения текущего перемещаемого элемента (а точнее, его индекса в модели).
property int draggedItemIndex: -1
interactive: false
Item {
    id: dndContainer
    anchors.fill: parent
}

MouseArea {
    id: coords
    anchors.fill: parent
    onReleased: {
        if (dndGrid.draggedItemIndex != -1) {
            var draggedIndex = dndGrid.draggedItemIndex
            dndGrid.draggedItemIndex = -1
            dndModel.move(draggedIndex,dndGrid.indexAt(mouseX, mouseY),1)
        }
    }
    onPressed: {
        dndGrid.draggedItemIndex = dndGrid.indexAt(mouseX, mouseY)
    }
}

В делегат добавим состояние inDrag, которое будет активироваться, когда данный элемент является перемещаемым. Вот тут нам и понадобится dndContainer. К нему мы будем цеплять (а если быть более точным, то менять родителя на этот контейнер) наш перемещаемый элемент. Кроме смены родителя, мы также отвязываем якоря у элемента (чтобы он мог перемещаться) и выставляем x и y соответственно координатам мышки (при чем, благодаря биндингу, положение отвязанной картинки будет меняться с перемещением курсора мыши). Когда состояние станет неактивным, все эти изменения откатятся.
states: [
    State {
        name: "inDrag"
        when: index == dndGrid.draggedItemIndex
        PropertyChanges { target: itemImage; parent: dndContainer }
        PropertyChanges { target: itemImage; anchors.centerIn: undefined }
        PropertyChanges { target: itemImage; x: coords.mouseX - itemImage.width / 2 }
        PropertyChanges { target: itemImage; y: coords.mouseY - itemImage.height / 2 }
    }
]

Запустив, увидим примерно вот такую картину. Теперь, мы в нашем гриде можем спокойно перемещать элементы.


Шаг 3. Визуализируем перемещаемый элемент


Ок, мы научились делать то, ради чего была написана эта статья (все просто, не правда ли?). Но перемещаемый элемент почти не заметен, надо его немного выделить на фоне остальных. Добавим белую рамку вокруг перетаскиваемой картинки.
Вставим вот такой код в наш itemImage.
Rectangle {
    id: imageBorder
    anchors.fill: parent
    radius: 5
    color: "transparent"
    border.color: "#ffffff"
    border.width: 6
    opacity: 0
}

Плюс к рамочке неплохо бы как-то помечать место, откуда был взят перетаскиваемый элемент. Добавим белый кружок в центре этого места. Этот код помещается рядом с нашим itemImage
Rectangle {
    id: circlePlaceholder
    width: 30; height: 30; radius: 30
    smooth: true
    anchors.centerIn: parent
    color: "#cecece"
    opacity: 0
}

Ну и добавим их отображение при начале перетаскивания в состояние inDrag делегата.
State {
    name: "inDrag"
    when: index == dndGrid.draggedItemIndex
    PropertyChanges { target: circlePlaceholder; opacity: 1 }
    PropertyChanges { target: imageBorder; opacity: 1 }
    PropertyChanges { target: itemImage; parent: dndContainer }
    PropertyChanges { target: itemImage; anchors.centerIn: undefined }
    PropertyChanges { target: itemImage; x: coords.mouseX - itemImage.width / 2 }
    PropertyChanges { target: itemImage; y: coords.mouseY - itemImage.height / 2 }
}

Теперь наш пример выглядит уже вот так.


Шаг 4. Добавляем анимацию


Ну, основа нашего грида с Drag'n'Drop-ом готова. Добавим свистелок. А если более конкретно, то добавим анимацию.

Для начала в наш многострадальный itemImage добавим два состояния:
  • greyedOut — затеняет картинку. Используется когда drag'n'drop активен, но не для данного элемента
  • inactive — используется либо когда drag'n'drop неактивен, либо для перетаскиваемого элемента

Таким образом, мы слегка затеняем все элементы, кроме перетаскиваемого, что дает нам большую видимость последнего на фоне остальных.

Также добавим анимированные изменения для прозрачности, ширины и высоты картинки.
state: "inactive"
states: [
    State {
        name: "greyedOut"
        when: (dndGrid.draggedItemIndex != -1) && (dndGrid.draggedItemIndex != index)
        PropertyChanges { target: itemImage; opacity: 0.8}
    },
    State {
        name: "inactive"
        when: (dndGrid.draggedItemIndex == -1) || (dndGrid.draggedItemIndex == index)
        PropertyChanges { target: itemImage; opacity: 1.0}
    }
]

Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }
Behavior on height { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }
Behavior on opacity {NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }

В состояние inDrag добавим еще изменение высоты и ширины картинки и переход из этого состояния в любое другое (то есть переход из активного drag'n'drop в обычный режим). В этом переходе сделаем анимацию масштаба.
states: [
    State {
        name: "inDrag"
        when: index == dndGrid.draggedItemIndex
        PropertyChanges { target: circlePlaceholder; opacity: 1 }
        PropertyChanges { target: imageBorder; opacity: 1 }
        PropertyChanges { target: itemImage; parent: dndContainer }
        PropertyChanges { target: itemImage; width: 80 }
        PropertyChanges { target: itemImage; height: 80 }
        PropertyChanges { target: itemImage; anchors.centerIn: undefined }
        PropertyChanges { target: itemImage; x: coords.mouseX - itemImage.width / 2 }
        PropertyChanges { target: itemImage; y: coords.mouseY - itemImage.height / 2 }
    }
]
transitions: [
    Transition {
        from: "inDrag"
        to: "*"
        PropertyAnimation {
            target: itemImage
            properties: "scale, opacity"
            easing.overshoot: 1.5
            easing.type: "OutBack"
            from: 0.0
            to: 1.0
            duration: 750
        }
    }
]

Также добавим анимации изменения прозрачности к рамке вокруг перетаскиваемого элемента и к кружку на пустом месте.
Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } }

В итоге получилась уже вот такая картинка.


А это заснято в промежуточный момент, когда отрабатывает анимация.


Шаг 5. Последний штрих


И, в качестве завершения, добавим индикатор позиции, куда будет перемещен элемент. Отобразим его в виде вертикальной полоски слева от этого элемента.

Родителем этого элемента будет наш GridView, все действия с ним будут происходить там же.

Для начала добавим в GridView три новых свойства: индекс целевого элемента (possibleDropIndex) и текущие координаты мыши (xCoordinateInPossibleDrop и yCoordinateInPossibleDrop).

Плюс добавим сам элемент индикатора. Это обычная картинка 6x1 пикселей с градиентом, растиражированная по вертикали. У индикатора есть два состояния: невидим (по умолчанию) и shown. Во втором состоянии элемент индикатора помещается в промежуток между двумя картинками, слева от цели. Положение элемента рассчитывается на основе двух последних свойств, а не по индексу в модели, тем самым мы не зависим от текущего количества столбцов в таблице.
property int possibleDropIndex: -1
property int xCoordinateInPossibleDrop: -1
property int yCoordinateInPossibleDrop: -1

Item {
    id: dropPosIndicator
    visible: false
    height: dndGrid.cellHeight
    width: 10

    Image {
        visible: parent.visible
        anchors.centerIn: parent
        height: parent.height-10
        source: "drop-indicator.png"
    }

    states: [
        State {
            name: "shown"
            when: dndGrid.possibleDropIndex != -1
            PropertyChanges {
                target: dropPosIndicator
                visible: true
                x: Math.floor(dndGrid.xCoordinateInPossibleDrop/dndGrid.cellWidth) *
                    dndGrid.cellWidth - 5
                y: Math.floor(dndGrid.yCoordinateInPossibleDrop/dndGrid.cellHeight) *
                    dndGrid.cellHeight
            }
        }
    ]
}


Также добавим еще один обработчик в MouseArea. Тут нам понадобится свойство с индексом места дропа, чтобы не обновлять каждый раз координаты мышки, а менять их только при смене целевого элемента.
onPositionChanged: {
    var newPos = dndGrid.indexAt(mouseX, mouseY)
    if (newPos != dndGrid.possibleDropIndex) {
        dndGrid.possibleDropIndex = newPos
        dndGrid.xCoordinateInPossibleDrop = mouseX
        dndGrid.yCoordinateInPossibleDrop = mouseY
    }
}

В итоге, получим вот такое приложение. Такое же, как на видео в начале поста :)


Ну и да, скачать полные исходники (с отдельными .qml файлами для каждого шага) можно здесь
Теги:
Хабы:
+33
Комментарии 16
Комментарии Комментарии 16

Публикации

Истории

Работа

QT разработчик
6 вакансий

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн