Drag and Drop Reorder List in QML Qt

on April 18th, 2011 by Wade Shull in Development - QML - Qt
| Share

With QML being such a young language in Qt there are still a lot of questions and tweaks that are happening.  There are also a lot of features that people want, but aren’t included.  From going through the different forums one of the options that I have seen brought up multiple times is to have the ability to drag and drop list delegates.  By just saying it, it makes sense, but since it isn’t a built in option it is not super easy to implement.  Now I love a good challenge as much as the next person, so I took it upon myself to get this done.  It is a lot of code for something that sounds simple.  There are two things currently “flawed” with this, but I thought I would at least get it out there so others could see how I did it.  Maybe they can make it better and complete it.  Here is the full code.  It is nasty long, for that I apologize.  Click on the name of the code boxes to see the code inside.  First I will post the whole code then I will break it down to explain what I have done in each section.

QML Drag and Drop Reorder List from Mu Studios on Vimeo.

Full code   
Rectangle {
    id: wholeBody
    width: 360
    height: 360
    ListModel {
        id: starwarsModel
        ListElement {
            number: "IV"
            title: "A New Hope"
        }
        ListElement {
            number: "V"
            title: "The Empire Strikes Back"
        }
        ListElement {
            number: "VI"
            title: "Return of the Jedi"
        }
        ListElement {
            number: "I"
            title: "The Phantom Menace"
        }
        ListElement {
            number: "II"
            title: "Attack of the Clones"
        }
        ListElement {
            number: "III"
            title: "Revenge of the Sith"
        }
        ListElement {
            number: "VII"
            title: "The Force of the Jedi"
        }
        ListElement {
            number: "VIII"
            title: "The New Republic Challenged"
        }
        ListElement {
            number: "IX"
            title: "The Force Combined"
        }
        ListElement {
            number: "X"
            title: "The Council Rebuilt"
        }
        ListElement {
            number: "XI"
            title: "Jedi Outnumbered"
        }
        ListElement {
            number: "XII"
            title: "The Ultimate Force"
        }
    }
    Component {
        id: starwarsDelegate
        Rectangle {
            id: starwarsDelegateBorder
            border.color: "black"
            width: wholeBody.width
            height: starwarsNumberText.height
            Row {
                spacing: 10
                Text { id: starwarsNumberText; text: number }
                Text { text: title }
                Text { text: index }
            }
            MouseArea {
                id: dragArea
                anchors.fill: parent
                property int positionStarted: 0
                property int positionEnded: 0
                property int positionsMoved: Math.floor((positionEnded - positionStarted)/starwarsNumberText.height)
                property int newPosition: index + positionsMoved
                property bool held: false
                drag.axis: Drag.YAxis
                onPressAndHold: {
                    starwarsDelegateBorder.z = 2,
                    positionStarted = starwarsDelegateBorder.y,
                    dragArea.drag.target = starwarsDelegateBorder,
                    starwarsDelegateBorder.opacity = 0.5,
                    starwarsList.interactive = false,
                    held = true
                    drag.maximumY = (wholeBody.height - starwarsNumberText.height - 1 + starwarsList.contentY),
                    drag.minimumY = 0
                }
                onPositionChanged: {
                    positionEnded = starwarsDelegateBorder.y;
                }
                onReleased: {
                    if (Math.abs(positionsMoved) < 1 && held == true) {
                        starwarsDelegateBorder.y = positionStarted,
                        starwarsDelegateBorder.opacity = 1,
                        starwarsList.interactive = true,
                        dragArea.drag.target = null,
                        held = false
                    } else {
                        if (held == true) {
                            if (newPosition < 1) {
                                starwarsDelegateBorder.z = 1,
                                starwarsModel.move(index,0,1),
                                starwarsDelegateBorder.opacity = 1,
                                starwarsList.interactive = true,
                                dragArea.drag.target = null,
                                held = false
                            } else if (newPosition > starwarsList.count - 1) {
                                starwarsDelegateBorder.z = 1,
                                starwarsModel.move(index,starwarsList.count - 1,1),
                                starwarsDelegateBorder.opacity = 1,
                                starwarsList.interactive = true,
                                dragArea.drag.target = null,
                                held = false
                            }
                            else {
                                starwarsDelegateBorder.z = 1,
                                starwarsModel.move(index,newPosition,1),
                                starwarsDelegateBorder.opacity = 1,
                                starwarsList.interactive = true,
                                dragArea.drag.target = null,
                                held = false
                            }
                        }
                    }
                }
            }
        }
    }
    ListView {
        id: starwarsList
        anchors.fill: parent
        model: starwarsModel
        delegate: starwarsDelegate
    }
}

Most of the room in that code is for making the list.  I had to get a little creative to make the list bigger, I hope George doesn’t mind.  Obviously all right are reserved for the titles by George Lucas.  Anyway.  Let’s start breaking this down.

So let us take a look at the outside Rectangle of the delegate.

Rectangle   
Rectangle {
     id: starwarsDelegateBorder
     border.color: "black"
     width: wholeBody.width
     height: starwarsNumberText.height
}

This is pretty straight forward.  I made this to get a nice black border around the text.  I also made it the height of the text inside of it.  Now on to the text from the list.

Row {
 spacing: 10
     Text { id: starwarsNumberText; text: number }
     Text { text: title }
     Text { text: index }
}

If you haven’t used the Row item before it is pretty nice.  It is just 1 row with the items inside spaced by what you indicate.  Here I have listed the “number” and “title” from the list made earlier and also the “index” or position it is in the list.  I added this to verify that when we drag and drop it is actually moving inside the list, and not just graphically on the screen.  Now lets head into the the real meat and potatoes of the code – MouseArea.  All the real work is done here.  I will break this down step by step.  First let’s just look at the objects declared in the MouseArea.

MouseArea   
MouseArea {
     id: dragArea
     anchors.fill: parent
     property int positionStarted: 0
     property int positionEnded: 0
     property int positionsMoved: Math.floor((positionEnded - positionStarted)/starwarsNumberText.height)
     property int newPosition: index + positionsMoved
     property bool held: false
     drag.axis: Drag.YAxis
}

First off I made the MouseArea fill the Rectangle created earlier.  I then made a variable called positionStarted and set it equal to 0.  Next I made a variable called positionEnded and also set it equal to 0.  The positionsMoved variable that is declared next has some math involved.  I bet you can see where I am going with this, but we will reference this variable later.  I set the variable of “held” as false and told it it can only be dragged along the Y axis.  Next let us look at the onPressAndHold area next.

onPressAndHold: {
     starwarsDelegateBorder.z = 2,
     positionStarted = starwarsDelegateBorder.y,
     dragArea.drag.target = starwarsDelegateBorder,
     starwarsDelegateBorder.opacity = 0.5,
     starwarsList.interactive = false,
     held = true
     drag.maximumY = (wholeBody.height - starwarsNumberText.height - 1 + starwarsList.contentY),
     drag.minimumY = 0
}

This has some good stuff in it here.  First off I and to use onPressAndHold because lists are inherited from Flickable.  That is what gives lists the ability to scroll up and down.  So I made the draggable option only available if you press and hold on an item.  The first move was to make the Rectangle from the delegate z = 2.  This was because if you tried to drag it down it would go behind the list.  Setting the z = 2 took care of that problem.  Next I set the positionStarted = to the y of the Rectangle.  Then I set the target of the draggable item to the Rectangle of the delegate.  Next I set the opacity of the item to 0.5 so you would know that you have initiated the dragging command.  It instantly sets it apart from the rest of the list.  Next I set the list(starwarsList) interactive to false.  Now what this does is turn the Flickable off for the list.  I ran into issues of even though I wanted to drag the delegate item I ended up moving the list when I tried dragging it up or down.  Lastly in this section I set the max that you could drag the delegate.  You can take it to the top of the screen or to the bottom of the screen.  If you don’t do this you can drag the item right off the screen.  Now let’s see what goes on when the mouse changes.

onPositionChanged: {
     positionEnded = starwarsDelegateBorder.y;
}

This is very simple.  All it does is set the positionEnded as whatever the current y is of the item that is moving around.  Ok so we pressed on it for a long time, we moved it around while we were still pressing on it.  To wrap it all up we “release” the mouse when we have dragged it to its desired position.  This is the big one.

onRelease   
onReleased: {
     if (Math.abs(positionsMoved) < 1 && held == true) {
          starwarsDelegateBorder.y = positionStarted,
          starwarsDelegateBorder.opacity = 1,
          starwarsList.interactive = true,
          dragArea.drag.target = null,
          held = false
     } else {
          if (held == true) {
               if (newPosition < 1) {
                    starwarsDelegateBorder.z = 1,
                    starwarsModel.move(index,0,1),
                    starwarsDelegateBorder.opacity = 1,
                    starwarsList.interactive = true,
                    dragArea.drag.target = null,
                    held = false
               } else if (newPosition > starwarsList.count - 1) {
                    starwarsDelegateBorder.z = 1,
                    starwarsModel.move(index,starwarsList.count - 1,1),
                    starwarsDelegateBorder.opacity = 1,
                    starwarsList.interactive = true,
                    dragArea.drag.target = null,
                    held = false
               } else {
                    starwarsDelegateBorder.z = 1,
                    starwarsModel.move(index,newPosition,1),
                    starwarsDelegateBorder.opacity = 1,
                    starwarsList.interactive = true,
                    dragArea.drag.target = null,
                    held = false
               }
          }
     }
}

Now for the good stuff.  First off I test to see if the delegate is being moved by calling held == true, also I am checking to see if it has moved more than one position.  To do that I just take the absolute value of the positionsMoved variable.  If it hasn’t gone more than 1 spot it snaps back into the position it was when it was selected.  I set the drag selection as null, set my drag variable to false, make the list flickable again by setting the interactive as true and set the opacity and z back to 1.  If you don’t set the z back to 1 then the next item you select to drag will drag under it.  The next three do a lot of the same things that the first one did and I could probably write the code a little simpler but left it this way to illustrate what was going on in each instance.  Basically for the next three I have it so that if you move it to the first position it will become index 0.  Next if you move it beyond the end of the list it will become the count – 1 or the last instance of the list.  Lastly if it isn’t the first or the last it will just calculate which position it should become.

Now for the flaws.  The two majors flaws are this.  Since I use the delegate height to calculate its new position then every delegate has to be the same height.  If you would make a list with delegates of different heights then the calculations would be off and the drag and drop wouldn’t be exact.  The other flaw is that if you move the delegate to the bottom of the screen with a list that goes off screen it, at the moment, doesn’t scroll the list automatically.  You would have to drag it to the bottom, flick the list upward, then move it again if that is what is needed.

Now for the last bit of warning.  The list that I am using in this example is hard coded.  If you move items around and then exit the program and restart it, the list will return like it was the first time.  To make a change like the drag and drop stick for forever, you would have to pull the list from like a database and make the onReleased change the database to reflect what you have done in the list.  That wasn’t the intention of this example though.

Leave any comments you want.  If you have a better way please share.  If you do use this, how about a little shout out for @wadeshull on twitter :)  If not, no big deal.  I just enjoy QML.

Comments: 4 Tags:

4 Responses to “Drag and Drop Reorder List in QML Qt”

  1. Scorp1us says:

    THANK YOU! I used this. However, could it be made simpler if you use a MouseArea on the ListView? I was able to roll my own that did this minus the visual movement that way. But you could drag and drop and use the ‘index’ property without having to calculate it. Bu like I said, I didn’t get to see my item move on the drag.

  2. fritz_van_tom says:

    very helpfull, thanks! could you provide something similar for a gridview?

  3. Paulo says:

    very helpful information. thank you! just wondering if it is possible to do something similar with gridview

  4. Vishwa says:

    its really nice .. but if you press and hold then releases in same point means it ll move towards the up….

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>