Search code examples
pythonqtqmlpyside2

Declaring a QAbstractListModel as a property in Pyside2


I am using Pyside2 with QML, and try to keep a good organisation of my code. I want to expose a subclass MyModel of QAbstractListModel from Python to QML, to use in a ListView. The code works perfectly if I declare the MyModel instance directly inside the engine:

...
engine = QQmlApplicationEngine()
myModel = MyModel(some_dict)
engine.rootContext().setContextProperty("myModel ", myModel)
...

that I can then use so:

ListView {
    model: myModel
    delegate: Row {
        Text { text: name }
        Text { text: type }
    }
}

However, when I try to define this element as a Property of a class, to keep things tidy and not registering models all over the place, I can't seem to make it work. I fail to recover good debugging information from qml, which also does not help.

I tried to declare the following

class ModelProvider(QObject):
    modelChanged = Signal()
    _entries: List[Dict[str, Any]]

    def __init__(self, entries, parent=None):
        QObject.__init__(self, parent)
        self._entries = entries

    def _model(self):
        return MyModel(self._entries)

    myModel = Property(list, _model, notify=modelChanged)
    myQVariantModel = Property('QVariantList', _model, notify=modelChanged)

...
modelProvider = ModelProvider(some_dict)
engine.rootContext().setContextProperty("modelProvider", modelProvider )

and then use it so in qml

ListView {
    model: modelProvider.myModel
    // or model: modelProvider.myQVariantModel 
    delegate: Row {
        Text { text: name }
        Text { text: type }
    }
}

The result is a blank screen.

I found out there that one potential reason could be that QAbstractListModel is a QObject, which would make it non copyable, and in c++ they propose to pass a pointer to it instead. But I thought that this would be the case automatically in Python.

What do I do wrong in this case? And if possible, how could I find out why is the ListView not rendering anything (a debug output, maybe)? Is it not possible at all to organize my code in this way?


For the context, I try to follow the Bloc pattern, that I enjoy a lot using with dart and flutter, in which you have one (or more) central Bloc class that expose the model and the methods to act on this model for the view.


Solution

  • You have to point out that the Property is a QObject, not a QVariantList or a list. On the other hand I do not think that you change the model so you should use constant property and without signals. Also, you do not believe in the function the Model since each time you invoke _model a different object was created.

    main.py

    import os
    import sys
    from functools import partial
    from PySide2 import QtCore, QtGui, QtQml
    
    class MyModel(QtCore.QAbstractListModel):
        NameRole = QtCore.Qt.UserRole + 1000
        TypeRole = QtCore.Qt.UserRole + 1001
    
        def __init__(self, entries, parent=None):
            super(MyModel, self).__init__(parent)
            self._entries = entries
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            if parent.isValid(): return 0
            return len(self._entries)
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if 0 <= index.row() < self.rowCount() and index.isValid():
                item = self._entries[index.row()]
                if role == MyModel.NameRole:
                    return item["name"]
                elif role == MyModel.TypeRole:
                    return item["type"]
    
        def roleNames(self):
            roles = dict()
            roles[MyModel.NameRole] = b"name"
            roles[MyModel.TypeRole] = b"type"
            return roles
    
        def appendRow(self, n, t):
            self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
            self._entries.append(dict(name=n, type=t))
            self.endInsertRows()
    
    class ModelProvider(QtCore.QObject):
        def __init__(self, entries, parent=None):
            super(ModelProvider, self).__init__(parent)
            self._model = MyModel(entries)
    
        @QtCore.Property(QtCore.QObject, constant=True)
        def model(self):
            return self._model
    
    def test(model):
        n = "name{}".format(model.rowCount())
        t = "type{}".format(model.rowCount())
        model.appendRow(n, t)
    
    def main():
        app = QtGui.QGuiApplication(sys.argv)
        entries = [
            {"name": "name0", "type": "type0"},
            {"name": "name1", "type": "type1"},
            {"name": "name2", "type": "type2"},
            {"name": "name3", "type": "type3"},
            {"name": "name4", "type": "type4"},
        ]
        provider = ModelProvider(entries)
        engine = QtQml.QQmlApplicationEngine()
        engine.rootContext().setContextProperty("provider", provider)
        directory = os.path.dirname(os.path.abspath(__file__))
        engine.load(QtCore.QUrl.fromLocalFile(os.path.join(directory, 'main.qml')))
        if not engine.rootObjects():
            return -1
        timer = QtCore.QTimer(interval=500)
        timer.timeout.connect(partial(test, provider.model))
        timer.start()
        return app.exec_()
    
    if __name__ == '__main__':
        sys.exit(main())
    

    main.qml

    import QtQuick 2.11
    import QtQuick.Window 2.2
    import QtQuick.Controls 2.2
    
    ApplicationWindow {    
        visible: true
        width: 640
        height: 480
        ListView {
            model: provider.model
            anchors.fill: parent
            delegate: Row {
                Text { text: name }
                Text { text: type }
            }
        }
    }