3
votes

I am implementing a QAbstractListModel in class in python for use in QML. I've defined two custom roles in the model. In the model I've also implemented a Slot function 'get' to return data from a specified index and role. When I pass the role back to the 'get' function, I receive an integer different than what I defined for the role in my model.

I've tried passing the roles and index back from QML to a 'get' function defined in my Model. The index works as expected, but the roles return values different than what I've defined in my model.

gui.py

main()

def main():
    # create the application instance
    app = QApplication(sys.argv)
    # create a QML engine
    engine = PoaGUI()

    # instantiate Route class
    # route = Route()

    # add example routes for routemodel
    routelist = [
        {'stringinput' : 'Route 1', 'selected' : True},
        {'stringinput' : 'Route 2', 'selected' : False},
        {'stringinput' : 'Route 3', 'selected' : True},
        {'stringinput' : 'Route 4', 'selected' : False}
    ]

    # instantiate ListModel class
    routemodel = ListModel(routelist)

    # register the python type bindings to QML engine
    engine.rootContext().setContextProperty('RouteModel', routemodel)

    # load main QML file and start app engine
    engine.load('view.qml')

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

QAbstractListModel

class ListModel(QAbstractListModel):

    # create new user roles as class variables
    StringInput = Qt.UserRole + 0
    Selected = Qt.UserRole + 1
    # ADD USERROLE FOR SELECTED TO COMPLETE

    def __init__(self, datain=[], parent=None):
        super().__init__(parent)

        self._list = datain

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

        return len(self._list)

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

        #store QModelIndex passed to data
        row = index.row()

        if index.isValid() and role == self.StringInput:
            return self._list[row]['stringinput']
        if index.isValid() and role == self.Selected:
            return self._list[row]['selected']
        else:
            return None

    def roleNames(self):

        return {
            self.StringInput: b'stringinput',
            self.Selected: b'selected'
        }

    @Slot(int, int, result='QVariant')
    def get(self, row, role):
        # show difference between role supplied 
        # from QML vs definition in model
        print(role)
        print('Selected: ' + str(self.Selected))
        print('StringInput: ' + str(self.StringInput))
        if role == self.StringInput:
            print('stringinput')
            return self._list[row]['stringinput']
        elif role == self.Selected:
            print('selected')
            return self._list[row]['selected']
        else:
            return None


    @Slot(int, bool)
    def modSelected(self, row, selval):
        # set index of the row changes
        ix = self.index(row, 0)
        self._list[row]['selected'] = selval
        # communicate that changes were made
        self.dataChanged.emit(ix, ix, self.roleNames())

view.qml

ListView Implementation

CustomComp.CustomList {
       id: routelist
       model: RouteModel
       keyNavigationWraps: true

       listwidth: 300
       listheight: 600
       delegate: CustomComp.SingleListDelegate {}

       Layout.alignment: Qt.AlignCenter
    }

Delegate for ListView

import QtQuick 2.12
import QtQuick.Controls 2.12

Rectangle {
    id: singleItemList

    property int delegateIndex: index
    property bool altcolor: false
    property string altcolorcode: "#242526"
    property string highlightcolorcode: "#242526"

    width: parent.width; height: 20
    color: (singleItemList.altcolor) ? ((index % 2 == 0) ? altcolorcode:"#000000") : "#000000"
    border.color: "#ffcc00"

    Text {
        id: listText
        anchors.verticalCenter: parent.verticalCenter
        anchors.horizontalCenter: parent.horizontalCenter
        font.pixelSize: 14
        color: "#ffcc00"
        // delegate directly uses provided string
        text: stringinput
    }

    MouseArea {
        id: mousearea

        anchors.fill: parent
        onClicked: {
            singleItemList.ListView.view.currentIndex = index
            console.log(singleItemList.ListView.view.currentIndex)
            singleItemList.delegateSelect()
        }
    }

    function delegateSelect() {
        if (singleItemList.ListView.view.model.get(index, selected)) {
            singleItemList.color = "#000000"
            singleItemList.ListView.view.model.modSelected(index, false)
            // singleItemList.ListView.view.model.set(index, {"selected": false})
        }
        else {
            singleItemList.color = highlightcolorcode
            singleItemList.ListView.view.model.modSelected(index, true)
            // singleItemList.ListView.view.model.set(index, {"selected": true})
        }
        // console.log(singleItemList.ListView.view.model.get(index, selected))
    }
}

Having two custom roles, when I pass a role back to my model from QML, I receive integers on the order of 0 and 1. I used Qt.UserRole to define my role numbers for the model I generated in Python. Therefore the integers for those roles as defined in my model are 256 and 257. QML seems to handle the model fine because when I provide my model to a ListView and reference one of my custom roles in the delegate to display as text, the list populates as expected. Why are the integers for my two custom roles different in Python than they are in QML? That being the case, how can I successfully return those roles for use by another function in my model?

My ultimate goal is to create functions analogous to 'get' and 'set' in a QML ListView, but do so for a model I've built in Python.

1

1 Answers

2
votes

You have an XY problem, your real goal is to edit the model values from QML. So the solution is to implement the setData() method and edit the data with selected = foo_value, in your particular case: selected != selected

TL;DR;

You are misunderstanding the concept of "selected", "selected" in QML is the value of the model associated with the Selected role, that is what it means:

def roleNames(self):
    return {
        ListModel.StringInput: b"stringinput",
        ListModel.Selected: b"selected",
    }

That is, selected in QML is not the role but the value associated with the role and the index, that is, it is equivalent to:

selected = model.data(index, ListModel.Selected)

Therefore you get 1 and 0 which are the conversion of booleans true and false, respectively.

This is done to make an abstraction of the roles.


So you understand when you use the following:

QML code               Python Code
foo_value = selected ~ foo_value = model.data(index, ListModel.Selected)

But when you use:

QML code               Python Code
selected = foo_value ~ model.setData(index, foo_value, ListModel.Selected)

Solution

So in your case the solution is:

class ListModel(QAbstractListModel):

    StringInput = Qt.UserRole + 0
    Selected = Qt.UserRole + 1

    def __init__(self, datain=[], parent=None):
        super().__init__(parent)
        self._list = datain

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

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return QVariant()
        row = index.row()
        if 0 <= row < self.rowCount():
            if role == ListModel.StringInput:
                return self._list[row]["stringinput"]
            elif role == ListModel.Selected:
                return self._list[row]["selected"]
        return QVariant()

    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False
        row = index.row()
        result = False
        if 0 <= row < self.rowCount():
            if role == ListModel.StringInput:
                self._list[row]["stringinput"] = value
                result = True
            elif role == ListModel.Selected:
                self._list[row]["selected"] = value
                result = True
        if result:
            self.dataChanged.emit(index, index, (role,))
        return result

    def roleNames(self):
        return {
            ListModel.StringInput: b"stringinput",
            ListModel.Selected: b"selected",
        }

SingleListDelegate.qml

import QtQuick 2.12
import QtQuick.Controls 2.12

Rectangle {
    id: singleItemList

    property int delegateIndex: index
    property color highlightcolor: "#242526"
    property color defaultcolor : "#000000"
    width: parent.width; height: 20
    color: selected ? highlightcolor : defaultcolor
    border.color: "#ffcc00"

    Text {
        id: listText
        anchors.verticalCenter: parent.verticalCenter
        anchors.horizontalCenter: parent.horizontalCenter
        font.pixelSize: 14
        color: "#ffcc00"
        text: stringinput
    }

    MouseArea {
        id: mousearea
        anchors.fill: parent
        onClicked: {
            singleItemList.ListView.view.currentIndex = index
            console.log(singleItemList.ListView.view.currentIndex)
            // invert the boolean
            selected = !selected
        }
    }
}