[Solved] QML and Drag and Drop



  • So i am trying to find a way to rearrange dashboard widgets for my application. I would to give the user the ability to do this by just pressing and holding on a widget and then dragging it to a new location. Here is an implementation that allows the user to tap the widget he or she wants to change and then move it by tapping a new location. It works but I would love to add drag and drop to this. Please let me know if you have any ideas:

    @import Qt 4.7

    Rectangle {
    width: 640
    height: 480
    color: "#111111"

    Component {
    
    
        id: widgetdelegate
        Item {
            width: grid.cellWidth; height: grid.cellHeight
            Image {
                source: portrait;
                anchors.horizontalCenter: parent.horizontalCenter
                width: grid.cellWidth; height: grid.cellHeight
                fillMode: Image.PreserveAspectFit
            }
    
    
            MouseArea {
                id: mouse_area1
                anchors.fill: parent
                onClicked: {
                    if (grid.firstIndexDrag== 0) {
                        grid.firstIndexDrag=index
                    }else {
                        widgetmodel.move(grid.firstIndexDrag,index,1)
                        grid.firstIndexDrag= 0
                    }
    
                }
            }
        }
    }
    
    
    ListModel {
        id: widgetmodel
        ListElement {
            portrait: "Images/widget1.png"
        }
        ListElement {
            portrait: "Images/widget2.png"
        }
        ListElement {
            portrait: "Images/widget3.png"
        }
        ListElement {
             portrait: "Images/widget4.png"
        }
        ListElement {
             portrait: "Images/widget5.png"
        }
        ListElement {
             portrait: "Images/widget6.png"
        }
        ListElement {
             portrait: "Images/widget7.png"
        }
        ListElement {
             portrait: "Images/widget8.png"
        }
        ListElement {
             portrait: "Images/widget9.png"
        }
    
    
    
    }
    
    GridView {
    
        property int firstIndexDrag: 0
    
        id: grid
        x: 0
        y: 0
        anchors.rightMargin: 200
        anchors.bottomMargin: 100
        anchors.leftMargin: 200
        anchors.topMargin: 100
        width: 640
        height: 480
        anchors.fill: parent
        cellWidth: 80; cellHeight: 80
        flow: GridView.LeftToRight
    
    
        model: widgetmodel
        delegate: widgetdelegate
        //highlight: Rectangle { color: "white"; radius: 5 ; z: 1 }
        focus: true
    
    
    }
    

    }
    @



  • kyleplattner, is it related with "this other thread":http://developer.qt.nokia.com/forums/viewthread/2388?



  • Same in concept, different in approach to implementation.



  • Hey Kyle. Here is drag and drop for you:

    For the sake of brevity, I have separated your widgetmodel in to another QML file which I won't repeat in future.

    WidgetModel.qml
    @
    import QtQuick 1.0

    ListModel {
    ListElement {
    portrait: "Images/widget1.png"
    }
    ListElement {
    portrait: "Images/widget2.png"
    }
    ListElement {
    portrait: "Images/widget3.png"
    }
    ListElement {
    portrait: "Images/widget4.png"
    }
    ListElement {
    portrait: "Images/widget5.png"
    }
    ListElement {
    portrait: "Images/widget6.png"
    }
    ListElement {
    portrait: "Images/widget7.png"
    }
    ListElement {
    portrait: "Images/widget8.png"
    }
    ListElement {
    portrait: "Images/widget9.png"
    }
    }@

    Main.qml
    @
    import QtQuick 1.0

    Rectangle {
    width: 640
    height: 480
    color: "#111111"
    Component {
    id: widgetdelegate
    Item {
    width: grid.cellWidth; height: grid.cellHeight
    Image {
    source: portrait;
    anchors.horizontalCenter: parent.horizontalCenter
    width: grid.cellWidth; height: grid.cellHeight
    fillMode: Image.PreserveAspectFit
    }
    Rectangle {
    width: parent.width; height: parent.height; radius: 5
    border.color: "white"; color: "transparent"; border.width: 6;
    visible: index == grid.firstIndexDrag
    }
    }
    }

    GridView {
        property int firstIndexDrag: -1
    
        id: grid
        x: 0; y: 0
        interactive: false
    
        anchors.rightMargin: 200
        anchors.bottomMargin: 100
        anchors.leftMargin: 200
        anchors.topMargin: 100
        width: 640
        height: 480
        anchors.fill: parent
        cellWidth: 80; cellHeight: 80
    
        model: WidgetModel { id: widgetmodel }
        delegate: widgetdelegate
        MouseArea {
            anchors.fill: parent
            onReleased: {
                if (grid.firstIndexDrag != -1)
                    widgetmodel.move(grid.firstIndexDrag,grid.indexAt(mouseX, mouseY),1)
                grid.firstIndexDrag = -1
            }
            onPressed: grid.firstIndexDrag=grid.indexAt(mouseX, mouseY)
        }
    }
    

    }@

    You probably want to animate the drag and drop too. But didn't we cover this in the other topic? Please tell me if you desire code for this too.



  • I am interested in the code for animating it, thanks. I will try this tonight.

    I really appreciate your help.

    Kyle



  • Ok, I used re-parenting to animate the drag:

    @
    import QtQuick 1.0

    Rectangle {
    width: 640
    height: 480
    color: "#222222"
    Component {
    id: widgetdelegate
    Item {
    width: grid.cellWidth; height: grid.cellHeight
    Image {
    id: im
    source: portrait;
    anchors.centerIn: parent
    width: grid.cellWidth - 10; height: grid.cellHeight - 10
    smooth: true
    fillMode: Image.PreserveAspectFit
    Rectangle {
    id: imRect
    anchors.fill: parent; radius: 5
    anchors.centerIn: parent
    border.color: "#326487"; color: "transparent"; border.width: 6;
    opacity: 0
    }
    }
    Rectangle {
    id: iWasHere
    width: 20; height: 20; radius: 20
    smooth: true
    anchors.centerIn: parent
    color: "white";
    opacity: 0
    }
    states: [
    State {
    name: "inDrag"
    when: index == grid.firstIndexDrag
    PropertyChanges { target: iWasHere; opacity: 1 }
    PropertyChanges { target: imRect; opacity: 1 }
    PropertyChanges { target: im; parent: container }
    PropertyChanges { target: im; width: (grid.cellWidth - 10) / 2 }
    PropertyChanges { target: im; height: (grid.cellHeight - 10) / 2 }
    PropertyChanges { target: im; anchors.centerIn: undefined }
    PropertyChanges { target: im; x: coords.mouseX - im.width/2 }
    PropertyChanges { target: im; y: coords.mouseY - im.height/2 }
    }
    ]
    transitions: [
    Transition { NumberAnimation { properties: "width, height, opacity"; duration: 300; easing.type: Easing.InOutQuad } }
    ]
    }
    }

    GridView {
        property int firstIndexDrag: -1
    
        id: grid
        x: 0; y: 0
        interactive: false
    
        anchors.rightMargin: 200
        anchors.bottomMargin: 100
        anchors.leftMargin: 200
        anchors.topMargin: 100
        anchors.fill: parent
        cellWidth: 80; cellHeight: 80;
    
        model: WidgetModel { id: widgetmodel }
        delegate: widgetdelegate
        Item {
            id: container
            anchors.fill: parent
        }
        MouseArea {
            id: coords
            anchors.fill: parent
    
            onReleased: {
                if (grid.firstIndexDrag != -1)
                    widgetmodel.move(grid.firstIndexDrag,grid.indexAt(mouseX, mouseY),1)
                grid.firstIndexDrag = -1
            }
            onPressed: {
                grid.firstIndexDrag=grid.indexAt(mouseX, mouseY)
            }
        }
    }
    

    }@

    Enjoy!



  • Amazing, very well done. Thanks so much! I owe you a lot.



  • Oh by the way, I read your previous thread and remembered you wanted to have that 'squiggle' thing when the user does an 'onPressAndHold' -- like iOS does. So, I did this too. I hope you learn from the code:

    Main.qml
    @
    import QtQuick 1.0

    Rectangle {
    width: 640
    height: 480
    color: "#222222"
    Component {
    id: widgetdelegate
    Item {
    width: grid.cellWidth; height: grid.cellHeight
    Image {
    id: im
    state: "inactive"
    source: portrait;
    anchors.centerIn: parent
    width: grid.cellWidth - 10; height: grid.cellHeight - 10
    smooth: true
    fillMode: Image.PreserveAspectFit
    SequentialAnimation on rotation {
    NumberAnimation { to: 20; duration: 200 }
    NumberAnimation { to: -20; duration: 400 }
    NumberAnimation { to: 0; duration: 200 }
    running: im.state == "squiggle"
    loops: Animation.Infinite
    }
    Rectangle {
    id: imRect
    anchors.fill: parent; radius: 5
    anchors.centerIn: parent
    border.color: "#326487"; color: "transparent"; border.width: 6;
    opacity: 0
    }
    states: [
    State {
    name: "squiggle";
    when: (grid.firstIndexDrag != -1) && (grid.firstIndexDrag != index)
    },
    State {
    name: "inactive";
    when: (grid.firstIndexDrag == -1) || (grid.firstIndexDrag == index)
    PropertyChanges { target: im; rotation: 0}
    }
    ]
    }
    Rectangle {
    id: iWasHere
    width: 20; height: 20; radius: 20
    smooth: true
    anchors.centerIn: parent
    color: "white";
    opacity: 0
    }
    states: [
    State {
    name: "inDrag"
    when: index == grid.firstIndexDrag
    PropertyChanges { target: iWasHere; opacity: 1 }
    PropertyChanges { target: imRect; opacity: 1 }
    PropertyChanges { target: im; parent: container }
    PropertyChanges { target: im; width: (grid.cellWidth - 10) / 2 }
    PropertyChanges { target: im; height: (grid.cellHeight - 10) / 2 }
    PropertyChanges { target: im; anchors.centerIn: undefined }
    PropertyChanges { target: im; x: coords.mouseX - im.width/2 }
    PropertyChanges { target: im; y: coords.mouseY - im.height/2 }
    }
    ]
    transitions: [
    Transition { NumberAnimation { properties: "width, height, opacity"; duration: 300; easing.type: Easing.InOutQuad } }
    ]
    }
    }

    GridView {
        property int firstIndexDrag: -1
    
        id: grid
        x: 0; y: 0
        interactive: false
    
        anchors.rightMargin: 200
        anchors.bottomMargin: 100
        anchors.leftMargin: 200
        anchors.topMargin: 100
        anchors.fill: parent
        cellWidth: 80; cellHeight: 80;
    
        model: WidgetModel { id: widgetmodel }
        delegate: widgetdelegate
        Item {
            id: container
            anchors.fill: parent
        }
        MouseArea {
            id: coords
            anchors.fill: parent
            onReleased: {
                if (grid.firstIndexDrag != -1)
                    widgetmodel.move(grid.firstIndexDrag,grid.indexAt(mouseX, mouseY),1)
                grid.firstIndexDrag = -1
            }
            onPressAndHold: {
                grid.firstIndexDrag=grid.indexAt(mouseX, mouseY)
            }
        }
    }
    

    }@

    This has been made in to a Wiki Entry:
    http://developer.qt.nokia.com/wiki/Drag_and_Drop_within_a_GridView



  • Hi Sacha,

    The demo of drag and drop code looks cool!

    I tried the code(Main2.qml) of second version in the wiki but failed with following error: "ReferenceError: Can't find variable: gridId"

    Is there anything I can do to avoid this?

    THanks



  • Well, I didn't write the second version.
    I tested my (first) version and provided a video of that exact code running.

    I can review the second one for you to discover the issue. It seems a lot better than my version so I hope you can get it working.



  • I'm responsible for the second version. I tried to update this post with a "I’ve updated the above wiki entry with an alternate implementation." like I did on the "other thread":http://developer.qt.nokia.com/forums/viewreply/14354/, but it failed for some reason.

    As it turns out, the second version requires Qt 4.7.1. It gives that error with Qt 4.7.0. Thanks for pointing it out. I wasn't aware of this difference in the versions. I've updated the wiki page with a notation about this.



  • By the way, I just fixed in a critical issue with the first version in the wiki. It was to do with the moving code.
    Problem was discovered and fixed in this thread: http://developer.qt.nokia.com/forums/viewthread/2460/P15/

    The issue was that if you dragged items forward, the call to 'move' would push the indices backwards (to -1, -2 and so on) and you would no longer be able to drag the '-1' index and other items would become out of whack.

    It is most noticeable on a 3-item grid.

    I think the issue is only if you don't have animation onMove. The second example uses such an animation so it should be fine. Just be careful if you disable the animation.



  • Thank you guys. I finally get the second version working.

    It turns out that "gridId" should be defined in WidgetModel.qml, and it must starts with 0. For example:

    @
    ListElement { icon: "images/widget1.png"; gridId: 0 }
    ListElement { icon: "images/widget2.png"; gridId: 1 }
    ListElement { icon: "images/widget3.png"; gridId: 2 }
    @



  • Still returning:

    @file:///Users/kp/Desktop/Precision Work/Screen Design/FinalScreens/QMLFinal/Rearrange4.qml:22: TypeError: Result of expression 'grid.items' [undefined] is not an object.@



  • I have made a cleaner version of the 'second version'
    Halved the lines of code, removed quite a lot of redundancy and doesn't require Qt4.7.1.
    Personally, I think the grids list is a bit silly when it can be done with the model/delegate. So I have completely removed the IconItem and grids.
    No loss of functionality.
    I replaced the first version with it. You can check it out in the wiki.



  • The grids list was made because when I added behaviors in the delegate, it wasn't working. I don't know why it wasn't working for me, but what you've got is working. I'll remove the second version now that it is obsolete.



  • Yeah, I know, you can't animate a GridList.
    I discussed this "in another thread.":http://developer.qt.nokia.com/forums/viewthread/2327/#13163

    Basically, you need to reparent the items within the delegate. Then you have complete freedom of movement and animation. I reparented the images to the MouseArea.



  • Looks like in the new code the grid can't be resized, it always be 3*3.

    For example if I drag the size of its parent to a long list, it should be automatically rearranged to a 1*9 grid, as the GridView will do.



  • When you drag the window?
    Oh I had removed that as I was running it on a mobile device. I'll add that back in, thanks.



  • Just get it works by simply set width and height of GridView equal to its parent. :)



  • Yeah it already does that.
    Anchor is fill to parent and parent width is whatever the host operating system gives it. 640x480 is just for reference.
    Try it out now?
    I think the qmlviewer on Windows won't allow you to resize the window smaller than 640x480 by the way. Might need a custom qmlviewer for that.



  • As I can see, the size of qmlViewer will be the same as the top item in the qml file when it starts.

    Now I'm thinking of storing position of these icons.

    I guess the position should be stored in the model(or somewhere else?). How to show icons in GridView according to the values stored in the model?



  • Well you actually cannot change the positions of items inside a GridView.

    What I have done is created items within the GridView and then reparented them to a MouseArea (outside of GridView) so that I can move them freely.
    Then I assign the x,y values of the GridView locations.

    If you were to create entirely new locations and not use a GridView at all, you may as well use a Flow element.

    If you're just using GridView/ListView for the model/delegate, then I will answer your question:

    In your model, create an 'x' and 'y' property. For example 'myx', 'myy'. Then in the image, just set:
    @x=myx; y=my@



  • Basically I will have a serialized icons provided by C++ code via a model, and showed out as a Gridview.

    These icons then can be rearranged by user, and when user reopen the program they should be seeing the same order as they set last time.

    I'm really new to QML and not sure which is the best way, Flow or GridView?



  • Had tried setting x,y in model for image to display, but looks like it is not working.

    I guess the reason is in this case Grid index is not matching correct image, say a 2*3 grid:

    ID of GridView
    0 1 2
    3 4 5

    Images displayed in self-defined position
    4 5 1
    0 2 3

    When clicking at the top-left item, we'd like to pick image 4, but grid.at(index) will return 0 for us.



  • Oh, I see what you mean.

    Well the code I have in the wiki does exactly this.

    All you need to do is save the model in to either:

    1. QML file (overwrite the existing one on the fly)
    2. QSettings file (do the model positions inside QSettings object).

    The gridIDs are irrelevant. You can use gridId=843423 if you want. It's just a reference point.
    The only thing that matters is the order of the ListElements inside the ListModel.

    However, in saying this, if you keep the gridIDs as is and save the final gridIDs in to a QSettings object (uses less space/quicker to load), then you can simply use 'move' commands at runtime to sort them in to the correct order. It's another option.

    Since you said you are loading the model via a C++ backend, it only makes sense to load the model in the order you want. This solves your problem.



  • Thanks. I will try it in my C++ code.

    Another concern is about flickable. Sometimes user would like to flick the page when they have a lot of icons that displayed out of the screen. GridView have this feature by setting 'interactive' to true. However if we reparent items out of the GridView, these items will lost this feature.

    Actually I tried to comment out reparenting code, IconItem.qml like this:
    @
    Component {
    // Rectangle {
    // id: main
    // width: grid.cellWidth; height: grid.cellHeight
    // opacity: 0.5
    Image {
    id: item//; parent: loc
    width: grid.cellWidth; height: grid.cellHeight
    @

    In this code icon can be rearranged inside the GridView somehow, but grid failed to find correct index underneath cursor(don't know why)

    Is it possible to add flickable back to the GridView?



  • I only set interactive to false because it makes it easier to demo and you won't accidently flick the grid. It was unnecessary with only a 3x3 grid.

    If you want to set interactive to true, just remember to use contentX and/or (depending on flickable direction) contentY to get the position within the flickable (GridView) item.

    Example:
    @y: main.y - grid.contentY + 5@

    Note: You'll also have to make sure the grid doesn't flick while you are moving an icon.
    @interactive: loc.currentId == -1@

    With these two lines, it is working perfect with flickable here.

    Edit: You may get some extraordinary lagg and weird effects with flickable. You need to modify Behavior on x and y, with this:
    @ enabled: loc.currentId != -1 && item.state != "active"@



  • Gosh! It works perfectly.
    Thank you Sacha.



  • Hi Sacha, just found an issue about reparenting item. If beginResetModel()/endResetModel() called, icons will failed to be erased when moving.

    Sometimes icons could be updated with new data, so I need to reset model to refresh icons. New images can be displayed out but they can't be erased anymore.

    Any ideas how to fix this? Thanks in advance!



  • Yeah, if you re-parent the icons, any action on the gridView won't affect them any more (even destroying the grid view!).
    So what you need to do is:

    1. Re-parent back to gridView
    2. beginResetModel()/endResetModel() or other actions
    3. Re-parent back to loc/screen

    You can think of this as two states. Its parent should be 'loc' (or screen) when it is in a moving state because you want animations. Its parent should be the gridView when it is in a modification state (reseting model/destroying gridView or whatever else you need to do).



  • Cool! I solved it by simply moving "parent: loc" into the state "active".

    Thanks a lot!



  • Just a followup question: How can I persist the drag and dropped location so that when the user navigates away from the page and returns the arrangement that the user chose is still present.



  • I guess you need to store the locations in the model.



  • I do need to keep the locations of the dragged items. What would be the best way to accomplish this?



  • I'm using C++ data model and store items in a list.

    When drag-and-drop finishes, I will not only move items in the GridView, but also move them inside the list in C++ code at the same time.



  • this can work in one screen,but when i scroll the screen,this does not work,how to change it



  • Hi sfjam. Can you elaborate on your issue?

    Are you scrolling to a second GridView or scrolling within the GridView?

    My solution for scrolling within a GridView is on the previous page (fourth comment from bottom).

    If you want multiple GridViews, the code gets more complex. Post back if this is what you desire (multi-grid draggable icon layout).



  • Wow, thanks xsacha for putting this together! It's amazing how little code is involved to get such nice complex-looking behavior. :)

    I suggest a small tweak to the code: it looks like if an item at a lower index is in the active state and is moved towards an item of a higher index in the model, the active item is drawn behind the item with the higher model index. Setting the z value of the active item in the state PropertyChanges definition should do the trick:

    @PropertyChanges { target: item; x: loc.mouseX - width/2; y: loc.mouseY - height/2; scale: 0.5; z: 500 }@



  • Thanks, I'll add that :)


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.