Search code examples
pythonpython-3.xqmlpyside6

Why isn't dataChanged emitted after beginInsertRows() and endInsertRows() in a subclass of QAbstractListModel?


I was messing around with subclassing QAbstractListModel, and I don't think I understand how to properly add data to the model. Here's my script and the QML:

import QtQuick
import QtQuick.Controls

ApplicationWindow
{
    id: mainWindow
    visible: true
    title: qsTr("Sample Qt Quick application")
    width: 400
    height: 400
    color: "whitesmoke"

    Component.onCompleted: console.log("Component completed")

    Connections
    {
        target: main.custom_model
        function onDataChanged(topLeft, bottomRight, roles)
        {
            console.log("Custom model data changed")
        }
    }
    
}  // ApplicationWindow

import sys

from random import randint
from pathlib import Path
from PySide6.QtCore import Qt, QObject, QTimer, Property, QAbstractListModel, QModelIndex
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine


class CustomModel(QAbstractListModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._my_items = []
        
        self._role1 = Qt.UserRole + 1
        self._role2 = Qt.UserRole + 2
        
        self._roles = {
            self._role1: b"role1",
            self._role2: b"role2"
        }
        
    def append_item(self, arg1, arg2):
        row_count = self.rowCount()
        self.beginInsertRows(QModelIndex(), row_count, row_count)

        self._my_items.append({
            self._roles[self._role1]: arg1,
            self._roles[self._role2]: arg2
        })

        self.endInsertRows()

    def rowCount(self, parent=QModelIndex()):
        """
        Required for subclasses of QAbstractListModels
        """
        return len(self._my_items)

    def data(self, index, role=Qt.DisplayRole):
        """
        Required for subclasses of QAbstractListModels
        """
        try:
            the_item = self._my_items[index.row()]
        except IndexError:
            return QVariant()

        if role in self._roles:
            key = self._roles[role]
            return the_item[key]

        return QVariant()

    def roleNames(self):
        return self._roles


class MainContextClass(QObject):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._model = CustomModel()

    def add_an_item(self):
        thing1 = randint(0, 9)
        thing2 = randint(0, 9)
        print("Adding {0} and {1}".format(thing1, thing2))
        self._model.append_item(thing1, thing2)

    @Property(QObject, constant=True)
    def custom_model(self):
        return self._model


def main():
    app = QGuiApplication(sys.argv)
    qml_app_engine = QQmlApplicationEngine()
    qml_context = qml_app_engine.rootContext()

    main_context = MainContextClass(parent=app)
    qml_context.setContextProperty("main", main_context)
    
    this_file_path = Path(__file__)
    main_qml_path = this_file_path.parent / 'example.qml'
    qml_app_engine.load(str(main_qml_path))

    timer = QTimer()
    timer.setInterval(1000)
    timer.setSingleShot(False)
    timer.timeout.connect(main_context.add_an_item)
    timer.start()

    sys.exit(app.exec())

    
if __name__ == '__main__':
    main()

My expectation is that, every time the timer times out, a new row gets added to the listmodel, and therefore, the listmodel's dataChanged signal should get emitted. The Connections object's signal handler should then print a message. But it seems like it never executes:

$ python example.py
qml: Component completed
Adding 7 and 0
Adding 8 and 5
Adding 4 and 0
...

If I explicitly add self.dataChanged.emit() after endInsertRows(), then obviously the signal handler executes. But in all the documentation and example code I see, this isn't done. So, what's the proper approach?


Solution

  • dataChanged is emitted (explicitly) when the information of an item changes, when inserting rows then you should listen for the rowsAboutToBeInserted or rowsInserted signals:

    Connections {
        target: main.custom_model
    
        function onRowsAboutToBeInserted(parent, first, last) {
            console.log("before", parent, first, last);
        }
    
        function onRowsInserted(parent, first, last) {
            console.log("after", parent, first, last);
            /* print data
            let index = main.custom_model.index(first, 0, parent);
            let data1 = main.custom_model.data(index, Qt.UserRole + 1);
            let data2 = main.custom_model.data(index, Qt.UserRole + 2);
            console.log(data1, data2);*/
        }
    }
    

    On the other hand, in PySide there is no QVariant, instead you must return python objects, in the case of null QVariant you must return None (or nothing that is the same):

    def data(self, index, role=Qt.DisplayRole):
        """
        Required for subclasses of QAbstractListModels
        """
        try:
            the_item = self._my_items[index.row()]
        except IndexError:
            return
    
        if role in self._roles:
            key = self._roles[role]
            return the_item[key]