4

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.

Benjamin Audren
  • 374
  • 2
  • 16

1 Answers1

9

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 }
        }
    }
}
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks for your answer - it works as expected. I tried to adapt it to my particular use case (with a more generic `MyModel`, that tries to abstract the names of the keys to support any dict), and so far I failed. I'll try again tonight, and if it works, I'll accept the answer. Notably, I want to filter the list based on a selection from qml (so I send a value to a slot), perform the filter, and I thought that repushing the whole model was the way to go. But maybe I should just update it, like you're doing here. – Benjamin Audren Feb 18 '19 at 09:42
  • @BenjaminAudren If you want to filter data, the simplest and most recommendable is to use a QSortFilterProxyModel. On the other hand if you provide a [mcve] that is the one of your real problem I could help you to adapt my solution. Maybe you have some error in the part that you do not show so my answer will not help you since I only rely on what you provide in your question, unfortunately I am not a fortune teller :-( – eyllanesc Feb 18 '19 at 09:47
  • I tried to isolate what I though would be the issue - how to expose the model as a property. Thanks to your answer, I now understand that this was not the issue I was really facing. With my code and your code, as long as I declare my list to be constant, it displays. If I try to recreate it when the filtering changed, it's empty. Considering this, I will accept your answer, as it perfectly answers the problem I thought I had, and if I struggle with QSortFilterProxyModel, I will ask something more specific. In any case, thanks a lot for your help! – Benjamin Audren Feb 18 '19 at 13:13