QAbstractListModel in QML

This example shows how to add, remove and move items inside a QML ListView, but showing and editing the data via roles using a QAbstractListModel from Python.

You can add new elements and reset the view using the two top buttons, remove elements by ‘middle click’ the element, and move the elements with a ‘left click’ plus dragging the item around.

QAbstractListModel/ListView Screenshot
from PySide6.QtCore import (QAbstractListModel, QByteArray, QModelIndex, Qt,
                            Slot)
from PySide6.QtGui import QColor
from PySide6.QtQml import QmlElement

# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "BaseModel"
QML_IMPORT_MAJOR_VERSION = 1


@QmlElement
class BaseModel(QAbstractListModel):

    RatioRole = Qt.UserRole + 1

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.db = []

    def rowCount(self, parent=QModelIndex()):
        return len(self.db)

    def roleNames(self):
        default = super().roleNames()
        default[self.RatioRole] = QByteArray(b"ratio")
        default[Qt.BackgroundRole] = QByteArray(b"backgroundColor")
        return default

    def data(self, index, role: int):
        if not self.db:
            ret = None
        elif not index.isValid():
            ret = None
        elif role == Qt.DisplayRole:
            ret = self.db[index.row()]["text"]
        elif role == Qt.BackgroundRole:
            ret = self.db[index.row()]["bgColor"]
        elif role == self.RatioRole:
            ret = self.db[index.row()]["ratio"]
        else:
            ret = None
        return ret

    def setData(self, index, value, role):
        if not index.isValid():
            return False
        if role == Qt.EditRole:
            self.db[index.row()]["text"] = value
        return True

    @Slot(result=bool)
    def append(self):
        """Slot to append a row at the end"""
        return self.insertRow(self.rowCount())

    def insertRow(self, row):
        """Insert a single row at row"""
        return self.insertRows(row, 0)

    def insertRows(self, row: int, count, index=QModelIndex()):
        """Insert n rows (n = 1 + count)  at row"""

        self.beginInsertRows(QModelIndex(), row, row + count)

        # start database work
        if len(self.db):
            newid = max(x["id"] for x in self.db) + 1
        else:
            newid = 1
        for i in range(count + 1):  # at least one row
            self.db.insert(
                row, {"id": newid, "text": "new", "bgColor": QColor("purple"), "ratio": 0.2}
            )
        # end database work
        self.endInsertRows()
        return True

    @Slot(int, int, result=bool)
    def move(self, source: int, target: int):
        """Slot to move a single row from source to target"""
        return self.moveRow(QModelIndex(), source, QModelIndex(), target)

    def moveRow(self, sourceParent, sourceRow, dstParent, dstChild):
        """Move a single row"""
        return self.moveRows(sourceParent, sourceRow, 0, dstParent, dstChild)

    def moveRows(self, sourceParent, sourceRow, count, dstParent, dstChild):
        """Move n rows (n=1+ count)  from sourceRow to dstChild"""

        if sourceRow == dstChild:
            return False

        elif sourceRow > dstChild:
            end = dstChild

        else:
            end = dstChild + 1

        self.beginMoveRows(QModelIndex(), sourceRow, sourceRow + count, QModelIndex(), end)

        # start database work
        pops = self.db[sourceRow : sourceRow + count + 1]
        if sourceRow > dstChild:
            self.db = (
                self.db[:dstChild]
                + pops
                + self.db[dstChild:sourceRow]
                + self.db[sourceRow + count + 1 :]
            )
        else:
            start = self.db[:sourceRow]
            middle = self.db[dstChild : dstChild + 1]
            endlist = self.db[dstChild + count + 1 :]
            self.db = start + middle + pops + endlist
        # end database work

        self.endMoveRows()
        return True

    @Slot(int, result=bool)
    def remove(self, row: int):
        """Slot to remove one row"""
        return self.removeRow(row)

    def removeRow(self, row, parent=QModelIndex()):
        """Remove one row at index row"""
        return self.removeRows(row, 0, parent)

    def removeRows(self, row: int, count: int, parent=QModelIndex()):
        """Remove n rows (n=1+count) starting at row"""
        self.beginRemoveRows(QModelIndex(), row, row + count)

        # start database work
        self.db = self.db[:row] + self.db[row + count + 1 :]
        # end database work

        self.endRemoveRows()
        return True

    @Slot(result=bool)
    def reset(self):
        self.beginResetModel()
        self.resetInternalData()  # should work without calling it ?
        self.endResetModel()
        return True

    def resetInternalData(self):
        self.db = [
            {"id": 3, "bgColor": QColor("red"), "ratio": 0.15, "text": "first"},
            {"id": 1, "bgColor": QColor("blue"), "ratio": 0.1, "text": "second"},
            {"id": 2, "bgColor": QColor("green"), "ratio": 0.2, "text": "third"},
        ]
import QtQuick
import QtQuick.Controls
import QtQuick.Window
import BaseModel

Window {
    title: "Moving Rectangle"
    width: 800
    height: 480
    visible: true
    id: mainWindow

    Column {
        spacing: 20
        anchors.fill: parent
        id: mainColumn
        Text {
            padding: 20
            font.pointSize: 10
            width: 600
            wrapMode: Text.Wrap
            text: "This example shows how to add, remove and move items inside a QML ListView.\n
It shows and edits data via roles using QAbstractListModel on the Python side.\n
Use the 'Middle click' on top of a rectangle to remove an item.\n
'Left click' and drag to move the items."
        }

        Button {
            anchors {
                left: mainColumn.left
                right: mainColumn.right
                margins: 30
            }
            text: "Reset view"
            onClicked: lv.model.reset()
        }

        Button {
            anchors {
                left: mainColumn.left
                right: mainColumn.right
                margins: 30
            }
            text: "Add element"
            onClicked: lv.model.append()
        }

        ListView {
            id: lv
            anchors {
                left: mainColumn.left
                right: mainColumn.right
                margins: 30
            }

            height: 200
            model: BaseModel {}
            orientation: ListView.Horizontal
            displaced: Transition {
                NumberAnimation {
                    properties: "x,y"
                    easing.type: Easing.OutQuad
                }
            }
            delegate: DropArea {
                id: droparea
                width: ratio * lv.width
                height: lv.height

                onEntered: function (drag) {
                    let dragindex = drag.source.modelIndex
                    if (index === dragindex)
                        return
                    lv.model.move(dragindex, index)
                }

                MovingRectangle {
                    modelIndex: index
                    dragParent: lv
                    sizeParent: droparea
                }
            }

            MouseArea {
                id: lvMousearea
                anchors.fill: lv
                z: -1
            }
            Rectangle {
                id: lvBackground
                anchors.fill: lv
                anchors.margins: -border.width
                color: "white"
                border.color: "black"
                border.width: 5
                z: -1
            }
            Component.onCompleted: {
                lv.model.reset()
            }
        }
    }
}
import sys
from pathlib import Path

from PySide6.QtCore import QUrl
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine

from model import BaseModel

if __name__ == "__main__":
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    qml_file = Path(__file__).parent / "main.qml"
    engine.load(QUrl.fromLocalFile(qml_file))

    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec())
import QtQuick
import QtQuick.Controls

Rectangle {
    id: root
    property int modelIndex
    property Item dragParent
    property Item sizeParent
    property alias text: zone.text
    property alias bgColor: root.color

    anchors {
        horizontalCenter: parent.horizontalCenter
        verticalCenter: parent.verticalCenter
    }
    color:  backgroundColor
    anchors.fill: sizeParent
    border.color: "yellow"
    border.width: 0
    TextArea {
        id: zone
        anchors.centerIn: parent
        text: display
        onTextChanged: model.edit = text
    }

    MouseArea {
        id: zoneMouseArea
        anchors.fill: parent

        acceptedButtons: Qt.MiddleButton
        onClicked: function(mouse) {
            if (mouse.button == Qt.MiddleButton)
                lv.model.remove(index)
            else
                mouse.accepted = false
        }
    }
    DragHandler {
        id: dragHandler
        xAxis {

            enabled: true
            minimum: 0
            maximum: lv.width - droparea.width
        }
        yAxis.enabled: false
        acceptedButtons: Qt.LeftButton
    }
    Drag.active: dragHandler.active
    Drag.source: root
    Drag.hotSpot.x: width / 2

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

            AnchorChanges {
                target: root
                anchors.horizontalCenter: undefined
                anchors.verticalCenter: undefined
            }
            PropertyChanges {
                target: root
                opacity: 0.6
                border.width: 3
            }
        }
    ]
}