Skip to main content

Raymii.org Raymii.org Logo

Quis custodiet ipsos custodes?
Home | About | All pages | Cluster Status | RSS Feed | Gopher

QML Drag and Drop including reordering the C++ model

Published: 21-01-2022 | Author: Remy van Elst | Text only version of this article


Table of Contents


This guide shows you how to implement drag and drop in Qml including how to reorder the backing C++ (QAbstractListModel derived) data model. Most QML Drag and Drop examples you find online, including the Qt official example, use a ListModel in the same Qml file which has the data, but no example I found actually reordered a C++ model. This example has a simple MVVM (model-view-viewmodel) C++ structure and a QML file with a drag and drop grid. The dragable example items come from the C++ model, which is derived from QAbstractListModel.

I'm developing a desktop monitoring app, Leaf Node Monitoring, open source, but paid. For Windows, Linux & Android, go check it out.

Consider sponsoring me on Github. It means the world to me if you show your appreciation and you'll help pay the server costs.

You can also sponsor me by getting a Digital Ocean VPS. With this referral link you'll get $100 credit for 60 days.

This guide assumes you're familiair with Qml and have read through the Drag and DropArea documentation and the official drag and drop example.

Drag and Drop in Qml

Qml has the concept of drag and drop built in, you define a DropArea somewhere and make something Drag-able, that's basically it. Really neat and quick to set up, including official examples

The second official example shows a grid of tiles which you can reorder by dragging and dropping them. It uses a DelegateModel, a special Qml repeater-like control which has both the model and the delegate, to move a delegate item to the position of another item it is dragged over.

The example also states, quite clearly:

The GridView Example adds drag and drop to a GridView, allowing you to visually reorder the delegates without changing the underlying ListModel.

In my case, also changing the underlying listmodel (and backing C++ model) is exactly what I want to do. It turned out to be a bit convoluted, due to how a DelegateModel acts as a proxy, it has a ListModel which you can manipulate, but that is more like a copy of the original model:. You must explicitly propagate changes back to your C++ code.

Basic MVVM Qt setup

drag and drop

A recording of the demo application

The example application follows an MVVM-like pattern. It has a C++ class named Thingie. In this case a Thingie has two properties, a name and color, but imagine it to be a more complex class, maybe an image of some kind.

There is a ThingieListModel, your basic Qt QAbstractListModel derived list, with a backing QList<Thingie> and one extra special method (move).

Finally there is a ThingieModel, the class that houses all business logic. In an actual MVVM application there would also be a ViewModel, but for this example that would be too much.

The ThingieModel is exposed to QML and constructs the list of Thingies, which is also exposed to Qml as a property, via the model.

You can find the code here on my Github, but for the sake of convinience, the code is also at the bottom of this article.

QML Drag & Drop

My example has a grid of squares that you can drag and drop to re-order. The grid is in a sepeate file named ThingGrid and houses a GridView with a DelegateModel. The delegate of this model is another control, a ThingTile. This ThingTile has most of the Drag logic (rectangle with mousearea) and the tiles on the ThingGrid have most of the Drop logic (DropArea). Inside the ThingTile you define your own element, which in the case of the example is a Text, but could be anything.

Where my example differs from the Qt example is that my code has an explicit MouseArea in the dragable tile, mostly to send signals back up above to the grid, the most important one being parent.Drag.drop(). If you're wondering why, well, let me explain.

The Drag 'thing', does not send a drop event / signal when you release it. It only generates entered events when entering a DropArea. You must explicitly call the drop() method on the Drag object.

The example has a DragHandler and no MouseArea, so it took me a while to figure out how to send that drop() event.

But why do we need a drop() event? The official example already re-orders stuff once you drop, you might ask.

The official example does not re-order when you drop, it re-orders when you enter. This means, when you start dragging a square over another square (each square can be dragged, but is also a drop area), it is already re-ordering the visual model. You can see this because the animation starts (displacing the other square).

What we want to do however, is re-order the backing C++ model. Remember that the DelegateModel acts as a kind of proxy between your actual ListModel. You're modifying the visual representation of that model, not the actual model itself.

Inside our DropArea control in the ThingGrid, this is the code that handles the visual changes. Every square is both draggable as well as its own drop area, so when one square starts being dragged, once it enters another square, this code triggers a visual change (and corresponding animation):

onEntered: function (drag) {
    var from = (drag.source as Example.ThingTile).visualIndex
    var to = thingTile.visualIndex
    visualModel.items.move(from, to)
}

Do note that this is all on the visual model side, not the actual model. Once you drop an item inside a DropArea, the following code triggers, handling the actual backend model change:

onDropped: function (drag) {
    var from = modelIndex
    var to = (drag.source as Example.ThingTile).visualIndex
    ThingModel.listOfThingies.move(from, to)
}

The C++ ThingModel has a Q_PROPERTY names listOfThingies, which is the QAbstractListModel derived class. QML calls the move() method directly on that listmodel. For the observant readers among you, you might be wondering what modelIndex is in the latter method. The DropArea has a property visualIndex, which is the actual index in the visual model:

property int visualIndex: DelegateModel.itemsIndex

This property changes once we enter another droparea, via the onEntered method. But, we need to keep that old index to move the C++ model. If we would use the visual index, then that would already be updated once a drop occurs. Therefore I added a variable just below the visualIndex, named modelIndex. It is set once you press the tile, but not via a property binding (otherwise it would update the same as the visualIndex), but via a JavaScript statement:

Example.ThingTile {
  [...]
  onPressed: delegateRoot.modelIndex = visualIndex

This way, once you start dragging the square, the visual index updates and other squares are displaced. Only when you drop, the actual C++ code is called with the old index and the new index.

Reordering the C++ model

The basic C++ (read only) listmodel derived from QAbstractListModel for your own data structures must subclass rowCount, data and roleNames(last one for QML). At work we have a few more convinience methods, for example to update a listmodel from a vector. Most model data comes from the C++ backend and the listmodels are only used to display stuff in QML.

In this case, the data should also be re-ordered from QML. Most of the subclassing reference documentation talks about removing data or adding data from the model, not moving stuff around. There is the beginMoveRows and endMoveRows method, but when I used that the visual model was not ordered correctly and there were visual oddities when releasing an item. So, in the end I went with a beginResetModel and endResetModel.

As you saw in the above Qml code, once the draggable tile is actually released (dropped), a C++ method named move() is called. That method is simple, in the backing QList it moves an item (not swapping) and emits the correct signals to notify Qml that the model has changed:

void ThingieListModel::move(int from, int to)
{
    if(from >= 0 && from < rowCount() && to >= 0 && to < rowCount() && from != to) 
    {
        if(from == to - 1) 
        { // Allow item moving to the bottom
            to = from++;
        }

        beginResetModel();
        //beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
        _thingies.move(from, to); // update backing QList
        //endMoveRows();
        endResetModel();
    }
}

I left the moveRows calls in there, if you can figure out why that doesn't work correctly, please let me know.

You could extend this method to emit another signal, which you can handle in the viewmodel or actual model, for example, to send a call to a web api backend to also reorder the data.

Code

The code is also on my github but since it's small, I've posted it here as well.

Generated with a bash loop to automatically intend for markdown:

for i in *.h *.cpp *.qml; do 
  echo '**' $i '**'; 
  echo; 
  sed 's/^/    /' $i; 
  echo; 
  echo; 
done 

ThingModel.h

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

#ifndef THINGMODEL_H
#define THINGMODEL_H

#include <QObject>
#include <ThingieListModel.h>

class Thingie;
class ThingModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(ThingieListModel* listOfThingies READ listOfThingies CONSTANT)
public:
    ThingModel(QObject* parent = nullptr);

    Q_INVOKABLE QString printModel() { return _listOfThingies.print(); }
    ThingieListModel* listOfThingies() { return &_listOfThingies; }

public slots:

signals:

private:
    ThingieListModel _listOfThingies;
};

#endif // THINGMODEL_H

Thingie.h

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

#ifndef THINGIE_H
#define THINGIE_H

#include <QObject>
#include <QColor>

class Thingie : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)

public:
    Thingie(const QString& name, QObject* parent = nullptr);
    const QString &name() const;
    const QColor &color() const;

public slots:
    void setName(const QString &newName);
    void setColor(const QColor &newColor);

signals:
    void nameChanged(const QString &name);
    void colorChanged(const QColor &color);

private:
    QString _name;
    QColor _color = randomColor();

    QColor randomColor();
    QString randomHexString(unsigned int length);
};

#endif // THINGIE_H

ThingieListModel.h

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

#ifndef ThingieLISTMODEL_H
#define ThingieLISTMODEL_H

#include "Thingie.h"

#include <QAbstractListModel>

class ThingieListModel : public QAbstractListModel
{
    Q_OBJECT
public:
    enum ThingieRoles
    {
        NameRole = Qt::UserRole + 1,
        ColorRole,
        ModelIndexRole,
    };
    ThingieListModel(QObject *parent = nullptr);

    void updateFromVector(std::vector<Thingie*> newThingies);
    QHash<int, QByteArray> roleNames() const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;

    Q_INVOKABLE void move(int from, int to);
    Q_INVOKABLE QString print();

private:
    QList<Thingie*> _thingies;
};

#endif // ThingieLISTMODEL_H

ThingModel.cpp

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

#include "ThingModel.h"
#include "Thingie.h"

ThingModel::ThingModel(QObject* parent) : QObject(parent)
{    
    std::vector<Thingie*> tmpV;
    tmpV.push_back(new Thingie("Coffee Bean", this));
    tmpV.push_back(new Thingie("Small Cup", this));
    tmpV.push_back(new Thingie("Remy van Elst", this));
    tmpV.push_back(new Thingie("Fire information", this));
    tmpV.push_back(new Thingie("New Products", this));
    tmpV.push_back(new Thingie("New Videos", this));
    tmpV.push_back(new Thingie("Corona Info", this));
    _listOfThingies.updateFromVector(tmpV);
}

Thingie.cpp

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

#include "Thingie.h"

#include <random>

Thingie::Thingie(const QString& name, QObject* parent) : QObject(parent), _name(name)
{

}

const QString &Thingie::name() const
{
    return _name;
}

void Thingie::setName(const QString &newName)
{
    if (_name == newName)
        return;
    _name = newName;
    emit nameChanged(_name);
}

const QColor &Thingie::color() const
{
    return _color;
}

void Thingie::setColor(const QColor &newColor)
{
    if (_color == newColor)
        return;
    _color = newColor;
    emit colorChanged(_color);
}


QString Thingie::randomHexString(unsigned int length)
{
    QString result;
    static std::mt19937 generator {std::random_device {}()};
    std::string hex_characters = "0123456789abcdef";
    std::uniform_int_distribution<int> dist(0, hex_characters.length() - 1);
    for (unsigned int i = 0; i < length; i++)
    {
        result += hex_characters[dist(generator)];
    }
    return result;
}


QColor Thingie::randomColor()
{
    QString result = "#";
    result.append(randomHexString(6));
    return QColor(result);
}

ThingieListModel.cpp

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */
#include "ThingieListModel.h"

#include <QDebug>

ThingieListModel::ThingieListModel(QObject *parent) :
    QAbstractListModel(parent)
{
}

void ThingieListModel::updateFromVector(std::vector<Thingie*> newThingies)
{
    beginResetModel();
    _thingies.clear();
    for (const auto &item : newThingies)
    {
        _thingies << item;
    }
    endResetModel();
}

QHash<int, QByteArray> ThingieListModel::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[NameRole] = "name";
    roles[ColorRole] = "color";
    roles[ModelIndexRole] = "modelIndex";
    return roles;
}

QVariant ThingieListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
    {
        return QVariant();
    }

    const Thingie *thingie = _thingies[index.row()];
    switch (role)
    {
    case NameRole:
        return thingie->name();

    case ColorRole:
        return thingie->color();

    case ModelIndexRole:
        if (std::find(_thingies.begin(), _thingies.end(), thingie) != _thingies.end()) {
          return std::distance(_thingies.begin(), std::find(_thingies.begin(), _thingies.end(), thingie));
        } else {
          return -1;
        }

    default:
        return QVariant();
    }
}

int ThingieListModel::rowCount(const QModelIndex &) const
{
    return _thingies.count();
}


void ThingieListModel::move(int from, int to)
{
    if(from >= 0 && from < rowCount() && to >= 0 && to < rowCount() && from != to) {
        if(from == to - 1) { // Allow item moving to the bottom
            to = from++;
        }

        beginResetModel();
//        beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
        qInfo() << "model move from: " << from << " to: " << to;
        _thingies.move(from, to);
//        endMoveRows();
        endResetModel();

    }
}

QString ThingieListModel::print()
{
    QString tmp;
    for(int i = 0; i < _thingies.size(); ++i) {
        tmp.append(QString::number(i));
        tmp.append(": ");
        tmp.append(_thingies.at(i)->name());
        tmp.append("; ");
    }
    return tmp;
}

main.cpp

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

#include "ThingModel.h"
#include "Thingie.h"

#include <QGuiApplication>
#include <QQmlApplicationEngine>

int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

    QGuiApplication app(argc, argv);

    qRegisterMetaType<std::vector<Thingie*>>("std::vector<Thingie*>");
    ThingModel* thingModel = new ThingModel;
    qmlRegisterSingletonInstance<ThingModel>("org.raymii.ThingModel", 1, 0, "ThingModel", thingModel);


    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

ThingGrid.qml

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

import QtQuick 2.14
import QtQml.Models 2.15

import org.raymii.ThingModel 1.0

import "./" as Example

GridView {
    id: root
    width: 600
    height: 600

    cellWidth: 250
    cellHeight: 250

    displaced: Transition {
        NumberAnimation {
            properties: "x,y"
            easing.type: Easing.OutQuad
        }
    }

    model: DelegateModel {
        id: visualModel
        model: ThingModel.listOfThingies

        // each square is both a drag-able item as well as a droparea (to drop items in).
        delegate: DropArea {
            id: delegateRoot
            required property color color
            required property string name

            property int modelIndex

            width: root.cellWidth
            height: root.cellHeight

            onEntered: function (drag) {
                var from = (drag.source as Example.ThingTile).visualIndex
                var to = thingTile.visualIndex
                visualModel.items.move(from, to)
            }

            onDropped: function (drag) {
                var from = modelIndex
                var to = (drag.source as Example.ThingTile).visualIndex
                ThingModel.listOfThingies.move(from, to)
            }

            property int visualIndex: DelegateModel.itemsIndex

            Example.ThingTile {
                id: thingTile
                width: root.cellWidth * 0.8
                height: root.cellHeight * 0.8
                dragParent: root
                visualIndex: delegateRoot.visualIndex
                color: delegateRoot.color
                onPressed: delegateRoot.modelIndex = visualIndex

                // content of the draggable square
                Text {
                    anchors.fill: parent
                    anchors.centerIn: parent
                    horizontalAlignment: Text.AlignHCenter
                    verticalAlignment: Text.AlignVCenter
                    color: "white"
                    anchors.margins: 5
                    fontSizeMode: Text.Fit
                    minimumPixelSize: 10
                    font.pixelSize: 30
                    text: delegateRoot.name
                }
            }
        }
    }
}

ThingTile.qml

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

import QtQuick 2.14

Rectangle {
    id: root
    required property Item dragParent
    signal pressed
    signal released
    signal clicked

    property int visualIndex: 0

    anchors {
        horizontalCenter: parent.horizontalCenter
        verticalCenter: parent.verticalCenter
    }
    radius: 3

    MouseArea {
        id: mouseArea
        anchors.fill: parent
        drag.target: root
        onClicked: root.clicked()
        onPressed: root.pressed()
        onReleased: {
            parent.Drag.drop()
            root.released()
        }
    }

    Drag.active: mouseArea.drag.active
    Drag.source: root
    Drag.hotSpot.x: root.width / 2
    Drag.hotSpot.y: root.height / 2

    states: [
        State {
            when: mouseArea.drag.active
            ParentChange {
                target: root
                parent: root.dragParent
            }

            AnchorChanges {
                target: root
                anchors.horizontalCenter: undefined
                anchors.verticalCenter: undefined
            }
        }
    ]
}

main.qml

/* Author: Remy van Elst, https://raymii.org
 * License: GNU AGPLv3
 */

import QtQuick 2.15
import QtQuick.Layouts 1.12
import QtQuick.Window 2.15
import QtQuick.Controls 2.15

import org.raymii.ThingModel 1.0

import "./" as Example

Window {
    width: 800
    height: 800
    visible: true
    title: qsTr("Drag & Drop")

    Text {
        id: infoText
        anchors.top: parent.top
        anchors.left: parent.left
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignTop
        color: "black"
        anchors.margins: 5
        fontSizeMode: Text.Fit
        minimumPixelSize: 10
        font.pixelSize: 30
        height: 40
        text: "Drag and drop images below to reorder them"
    }

    Button {
        anchors.top: infoText.bottom
        anchors.left: parent.left
        anchors.leftMargin: 5
        id: printButton
        text: "Log C++ Model"
        onClicked: {
            modeltext.text = ThingModel.printModel()
        }
    }
    Text {
        id: modeltext
        anchors.top: printButton.bottom
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.margins: 5
        text: ""
        font.pixelSize: 20
        height: 40
        fontSizeMode: Text.Fit
        wrapMode: Text.WordWrap
        minimumPixelSize: 10
    }

    Example.ThingGrid {
        id: g
        anchors.top: modeltext.bottom
        anchors.margins: 5
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.bottom: parent.bottom
    }
}
Tags: animation , c++ , cpp , drag-and-drop , qabstractlistmodel , qml , qt , qt5 , tutorials