📜 ⬆️ ⬇️

Drag'n'Drop in QML is easy! Or 5 steps to the goal

This post participates in the competition " Smart phones for smart posts ".

Drag'n'Drop is an undeniably important element of user interaction and graphical environment. Unfortunately, in QML there is no built-in Drag'n'Drop mechanism for View. Therefore, I wrote a small example based on a GridView with 16 images.

This example of Drag'n'Drop doesn’t claim perfection (there are several other implementations that are visually more perfect), but the aim is to show that QML is a very flexible and simple development tool.
')
For a start, a small video, and under the cat 5 simple steps to get a similar result.



Step 1. Create a GridView


Let's start by creating a GridView and a small model. As test data, I took standard images from the Nokia N8.
Let's make our grid size 4x4 images.
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 } } 

After running in qmlviewer, we will see something like the following picture.


Step 2. Add Drag'nDrop


Now we need to add the ability to drag and drop images into this grid.

To simplify the task, disable the ability to scroll the GridView. You can do without it (using a long press instead of the usual pressing for D'n'D and manipulating the interactive property (which is responsible for whether the GridView will scroll) in the corresponding MouseArea callbacks), but this complicates the example.

We will also add a MouseArea element the size of the entire GridView, which will catch the mouse click at the start of the dredge, as well as move the element to the desired position in the model when the mouse is released. Plus, in the GridView we will add another additional dndContainer element, which we'll talk about later

The final touch is to add a property to our GridView to store the current floating element (or rather, its index in the model).
 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) } } 

In the delegate, add the inDrag state, which will be activated when this element is relocatable. This is where we need dndContainer . To it we will cling (or to be more precise, then change the parent to this container) our moving element. In addition to changing the parent, we also untie the anchors on the element (so that it can move) and expose x and y, respectively, to the coordinates of the mouse (and thanks to the binding, the position of the untied image will change with the mouse cursor). When the state becomes inactive, all these changes will be rolled back.
 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 } } ] 

Running, we will see something like this. Now, we in our grid can safely move elements.


Step 3. Render the item to be moved.


Ok, we learned to do something for which this article was written (it's simple, isn't it?). But the moved element is almost not noticeable, it is necessary to slightly distinguish it from the rest. Add a white frame around the dragged image.
Paste this code into our itemImage .
 Rectangle { id: imageBorder anchors.fill: parent radius: 5 color: "transparent" border.color: "#ffffff" border.width: 6 opacity: 0 } 

Plus to the frame, it would be nice to somehow mark the place where the drag element was taken from. Add a white circle in the center of this place. This code is placed next to our itemImage.
 Rectangle { id: circlePlaceholder width: 30; height: 30; radius: 30 smooth: true anchors.centerIn: parent color: "#cecece" opacity: 0 } 

Well, add their display when you start dragging to the inDrag delegate state.
 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 } } 

Now our example looks like this.


Step 4. Add animation


Well, the basis of our grid with Drag'n'Drop is ready. Add a whistle. And more specifically, add animation.

To begin with, we add two states to our long-suffering itemImage :

Thus, we slightly shade all the elements except the one being dragged, which gives us a greater visibility of the latter against the rest.

Also add animated changes for transparency, width and height of the image.
 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 } } 

In the inDrag state , add another change in the height and width of the image and the transition from this state to any other state (that is, the transition from the active drag'n'drop to the normal mode). In this transition we will make a scale animation.
 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 } } ] 

We will also add transparency animations to the frame around the drag element and to the circle on an empty spot.
 Behavior on opacity { NumberAnimation { duration: 300; easing.type: Easing.InOutQuad } } 

The result was already such a picture.


And this is filmed at the intermediate time when the animation is working.


Step 5. The final touch


And, as a conclusion, we add a position indicator to which the item will be moved. Display it as a vertical bar to the left of this element.

The parent of this element will be our GridView, all actions with it will occur in the same place.

To begin with, we will add three new properties to the GridView: the target element index ( possibleDropIndex ) and the current mouse coordinates ( xCoordinateInPossibleDrop and yCoordinateInPossibleDrop ).

Plus, add the indicator element itself. This is a regular 6x1 pixel image with a gradient spread out vertically. The indicator has two states: invisible (default) and shown . In the second state, the indicator element is placed in the gap between the two pictures, to the left of the target. The position of the element is calculated based on the last two properties, and not by the index in the model, thus we are not dependent on the current number of columns in the table.
 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 } } ] } 


Also add another handler to MouseArea. Here we need a property with a drop location index, so as not to update the mouse coordinates each time, but to change them only when the target element changes.
 onPositionChanged: { var newPos = dndGrid.indexAt(mouseX, mouseY) if (newPos != dndGrid.possibleDropIndex) { dndGrid.possibleDropIndex = newPos dndGrid.xCoordinateInPossibleDrop = mouseX dndGrid.yCoordinateInPossibleDrop = mouseY } } 

As a result, we get this application. Same as in the video at the beginning of the post :)


Well, yes, download the full source (with separate .qml files for each step) here

Source: https://habr.com/ru/post/134377/


All Articles