pythonpyqtqmlpyqt5qabstractlistmodel

how to insert/edit QAbstractListModel in python and qml updates automatically?



i am trying to insert/edit a python list that is subclassed from QAbstractListModel in pyqt5. this python list is read in the model property of ListView element in qml. i have no issues displaying the data in qml. problem arises when i try to append new data into the python list.

the following is what i have done so far:

main.py:

import sys, model2
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtQuick import QQuickView

class MainWindow(QQuickView):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.model = model2.PersonModel()
        self.rootContext().setContextProperty('PersonModel', self.model)
        self.rootContext().setContextProperty('MainWindow', self)
        self.setSource(QUrl('test2.qml'))

myApp = QApplication(sys.argv)
ui = MainWindow()
ui.show()
sys.exit(myApp.exec_())

model2.py

from PyQt5.QtCore import QAbstractListModel, Qt, pyqtSignal, pyqtSlot

class PersonModel(QAbstractListModel):

    Name = Qt.UserRole + 1
    Age = Qt.UserRole + 2

    personChanged = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.persons = [
            {'name': 'jon', 'age': 20},
            {'name': 'jane', 'age': 25}
        ]

    def data(self, QModelIndex, role):
        row = QModelIndex.row()
        if role == self.Name:
            return self.persons[row]["name"]
        if role == self.Age:
            return self.persons[row]["age"]

    def rowCount(self, parent=None):
        return len(self.persons)

    def roleNames(self):
        return {
            Qt.UserRole + 1: b'name',
            Qt.UserRole + 2: b'age'
        }

    @pyqtSlot()
    def addData(self):
        self.beginResetModel()
        self.persons = self.persons.append({'name': 'peter', 'age': 22})
        self.endResetModel()
        print(self.persons)

    @pyqtSlot()
    def editData(self):
        print(self.model.persons)

test2.qml:

import QtQuick 2.6
import QtQuick.Controls 2.2

Rectangle {
    anchors.fill: parent
    color: "lightgrey"

    ListView {
        id: listExample
        anchors.fill: parent
        model: PersonModel
        delegate: Text {
            text: name + " " + age
        }
    }

    Button {
        width: 50
        height: 25
        anchors.bottom: parent.bottom
        text: "add"
        onClicked: {
            console.log("qml adding")
            PersonModel.addData()
        }
    }

    .
    .
    .
}

error occurs when i click the add button which calls the addData method in model2.py. error lies in the rowCount and error message says TypeError: object of type 'NoneType' has no len(). do i have to emit the changes or pass in some index and role value so qml knows what is new/old and only reflect the changes accordingly?

any form of guidance is greatly appreciated!


Solution

  • The error you get is caused by the following line of code:

    self.persons = self.persons.append({'name': 'peter', 'age': 22})
    

    It is caused because the append function does not return anything, so it was meant to assign None to self.persons

    To insert new data you must call beginInsertRows() and endInsertRows() to notify the view of the change.

    the data method must be identical to that shown in the documentation, ie it must have the following format:

    def data(self, index, role=Qt.DisplayRole):
    

    The same with the rowCount method:

    def rowCount(self, parent=QModelIndex()):
    

    I have implemented the methods addPerson, editPerson and deletePerson that adds, edits and deletes a data from the list respectively. Also I added the necessary items to the .qml to be able to test it.

    model2.py

    from PyQt5.QtCore import QAbstractListModel, Qt, pyqtSignal, pyqtSlot, QModelIndex    
    
    class PersonModel(QAbstractListModel):
    
        NameRole = Qt.UserRole + 1
        AgeRole = Qt.UserRole + 2
    
        personChanged = pyqtSignal()
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self.persons = [
                {'name': 'jon', 'age': 20},
                {'name': 'jane', 'age': 25}
            ]
    
        def data(self, index, role=Qt.DisplayRole):
            row = index.row()
            if role == PersonModel.NameRole:
                return self.persons[row]["name"]
            if role == PersonModel.AgeRole:
                return self.persons[row]["age"]
    
        def rowCount(self, parent=QModelIndex()):
            return len(self.persons)
    
        def roleNames(self):
            return {
                PersonModel.NameRole: b'name',
                PersonModel.AgeRole: b'age'
            }
    
        @pyqtSlot(str, int)
        def addPerson(self, name, age):
            self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
            self.persons.append({'name': name, 'age': age})
            self.endInsertRows()
    
        @pyqtSlot(int, str, int)
        def editPerson(self, row, name, age):
            ix = self.index(row, 0)
            self.persons[row] = {'name': name, 'age': age}
            self.dataChanged.emit(ix, ix, self.roleNames())
    
        @pyqtSlot(int)
        def deletePerson(self, row):
            self.beginRemoveColumns(QModelIndex(), row, row)
            del self.persons[row]
            self.endRemoveRows()
    

    test2.qml

    import QtQuick 2.6
    import QtQuick.Controls 2.2
    
    Rectangle {
        anchors.fill: parent
        color: "lightgrey"
    
        ListView {
            id: listExample
            anchors.fill: parent
            model: PersonModel
            delegate:
                Item {
                width: 200
                height: 60
                Row {
                    Text {
                        width: 60
                        text:  name + " " + age
                        horizontalAlignment: Text.AlignHCenter
                        anchors.verticalCenter: parent.verticalCenter
                    }
                    Button{
                        width: 20
                        text: "+"
                        onClicked: PersonModel.editPerson(index, name, age+1)
                    }
                    Button{
                        width: 20
                        text: "-"
                        onClicked: PersonModel.editPerson(index, name, age-1)
                    }
                    Button{
                        width: 20
                        text: "X"
                        onClicked: PersonModel.deletePerson(index)
                    }
                }
            }
        }
    
        Button {
            width: 50
            height: 25
            anchors.bottom: parent.bottom
            anchors.right: parent.right
            text: "add"
            onClicked: {
                console.log("qml adding")
                PersonModel.addPerson("luis", 22)
            }
        }
    }
    

    Edit:

    .py

    @pyqtSlot(int, str, int)
    def insertPerson(self, row, name, age):
        self.beginInsertRows(QModelIndex(), row, row)
        self.persons.insert(row, {'name': name, 'age': age})
        self.endInsertRows()
    

    .qml

     PersonModel.insertPerson(2, "luis", 1111)