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.
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
}
}
]
}
© 2022 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.