3

I am a beginner in Qt and I am creating an Application by:

  1. Using QML for View design,
  2. Using python mainly for the Controller and Model part.

Therefore, QML need to interact with the python objects.

My problem: I have created a QAbstractListModel in python via the following (simplified) code:

class MyList(QAbstractListModel):

    _myCol1 = Qt.UserRole + 1
    _myCol2 = Qt.UserRole + 2


    def __init__(self, parent=None):
        super().__init__(parent)
        self.myData= [
            {
                'id': '01',
                'name': 'test1',
            },
            {
                'id': '02',
                'name': 'test2',
            }
        ]

    def data(self, index, role=Qt.DisplayRole):
        row = index.row()
        if role == MyList._myCol1:
            return self.myData[row]['id']
        if role == MyList._myCol2:
            return self.myData[row]['name']

    def rowCount(self, parent=QModelIndex()):
        return len(self.myData)

    def roleNames(self):
        return {
            MyList._myCol1: b'id',
            MyList._myCol2: b'name'
        }

    def get(self, index):
        # How to implement this?

Above code works fine and exposing the list from python to QML via the QQmlApplicationEngine and the rootContext().setContextProperty(...) works (I used the answer from how to insert/edit QAbstractListModel in python and qml updates automatically? and the Qt for Python docs as orientation).

If using a QML ListModel I could use the object get(index) function as described in the docs https://doc.qt.io/qt-5/qml-qtqml-models-listmodel.html. However:

  1. How can I access a specific element in the instantiated MyList from QML as I would do this element with the get(index) method if it would be a native QML ListModel?
  2. How to implement get(index) method?

I am still searching and expecting a solution referring to python and QML. Thank's for your help!

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
BeCurious
  • 127
  • 1
  • 9

1 Answers1

3

Only some types of variables are exportable to QML, among them are str, int, float, list but in the case of a dictionary it must be exported as a QVariant.

On the other hand if you want to access a method from QML then you must use the @pyqtSlot or @Slot decorator if you are using PyQt5 or PySide2, respectively, indicating the type of input data that in this case is int and the type of output through of the result parameter.

main.py

from PySide2 import QtCore, QtGui, QtQml


class MyList(QtCore.QAbstractListModel):
    col1 = QtCore.Qt.UserRole + 1
    col2 = QtCore.Qt.UserRole + 2

    def __init__(self, parent=None):
        super().__init__(parent)
        self.myData = [{"id": "01", "name": "test1",}, {"id": "02", "name": "test2",}]

    def data(self, index, role=QtCore.Qt.DisplayRole):
        row = index.row()
        if index.isValid() and 0 <= row < self.rowCount():
            if role == MyList.col1:
                return self.myData[row]["id"]
            if role == MyList.col2:
                return self.myData[row]["name"]

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.myData)

    def roleNames(self):
        return {MyList.col1: b"id", MyList.col2: b"name"}

    @QtCore.Slot(int, result='QVariant')
    def get(self, row):
        if 0 <= row < self.rowCount():
            return self.myData[row]


if __name__ == "__main__":
    import os
    import sys

    app = QtGui.QGuiApplication(sys.argv)

    current_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)))
    qml_file = os.path.join(current_dir, "main.qml")

    model = MyList()

    engine = QtQml.QQmlApplicationEngine()
    engine.rootContext().setContextProperty("listmodel", model)
    engine.load(QtCore.QUrl.fromLocalFile(qml_file))

    sys.exit(app.exec_())

main.qml

import QtQuick 2.13
import QtQuick.Controls 2.13

ApplicationWindow{
    id: root
    visible: true
    width: 640
    height: 480
    ListView{
        id: view
        anchors.fill: parent
        model: listmodel
        delegate: Text{
            text: model.id + " " + model.name
        }
    }
    Component.onCompleted: {
        var obj = listmodel.get(0)
        console.log(obj["id"])
        console.log(obj["name"])
    }
}

Output:

qml: 01
qml: test1

Plus:

Only some basic types are accepted directly and that is not the case with dict, in those cases you can use a QVariant and QVariantList (for lists or tuples), but in PySide2 there is no QVariant so You can indicate the types in C++ by passing them as string: "QVariant". What is indicated in the docs:

QVariant

As QVariant was removed, any function expecting it can receive any Python object (None is an invalid QVariant). The same rule is valid when returning something: the returned QVariant will be converted to the its original Python object type.

When a method expects a QVariant::Type the programmer can use a string (the type name) or the type itself.

(emphasis mine)

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you for your quick answer! As you pointed out when using PySide2 I have to use Qt.Core.Slot as the decorator. But my problem is that there is no QVariant type in PySide2. In [link](https://wiki.qt.io/Differences_Between_PySide_and_PyQt) under "PySide only supports PyQt's "API 2" (PSEP 101)" I found that native python types should be used. But if I use ```result=dict``` for the ```get``` function decorator I can't access the role as you did. With your code ```get``` would return a ```QVariant(PySide::PyObjectWrapper, )``` as ```obj``` in qml. – BeCurious Nov 25 '19 at 08:39
  • Ok. I found out, that I have to implement the ```result``` keyword of the ```get``` decorator as follows (at least for PySide2): ```result='QVariant'``` (found it here: [link](https://askvoprosy.com/voprosy/return-python-dict-to-qml-pyside2)) But I am not sure why to do so. I would appreciate some explanation. And by the way, is that documented anywhere in the official "Qt for Python" docs? Until now, I found them to be not very useful. – BeCurious Nov 25 '19 at 09:48
  • @BeCurious As you realize the answer depends on the binding: PyQt5 or PySide2 so for next question you must indicate clearly to avoid confusion. See my update. – eyllanesc Nov 25 '19 at 16:13