pyside6 pass custom ListModel to ListView
-
For a project i have to create an Application with pyside6 (Qt 6.4) and python 3.11. I've done very little Qt C++ programs with widgets but I'm fully new to the QtQick QML programming.
I've done some Java/ Kotlin applications so in principle i think i understood MVC patterns.I have some Data in a 'Produkte.db' file which i want to show in a list. it contains two columns: firstly an int which is an ID and a name.
i created a python file which reads in the filecontent, splits the data and passes strings to my ListModel.
Some of the code comes from https://doc.qt.io/qtforpython-6/overviews/model-view-programming.html .import sys import typing import PySide6 from PySide6 import QtCore from PySide6 import QtGui class ProductListModel (QtCore.QAbstractListModel): def __init__(self, products, parent = None): super().__init__(parent) self.products = products def data(self, index, role): """Returns an appropriate value for the requested data. If the view requests an invalid index, an invalid variant is returned. Any valid index that corresponds to a string in the list causes that string to be returned.""" row = index.row() if not index.isValid() or row >= len(self.products): return None if role != QtCore.Qt.DisplayRole and role != QtCore.Qt.EditRole: return None return self.products[row] def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): """Returns the appropriate header string depending on the orientation of the header and the section. If anything other than the display role is requested, we return an invalid variant.""" if role != QtCore.Qt.DisplayRole: return None if orientation == QtCore.Qt.Horizontal: return f"Column {section}" return f"Row {section}" def rowCount(self, parent: typing.Union[PySide6.QtCore.QModelIndex, PySide6.QtCore.QPersistentModelIndex] = ...) -> int: # return length of productList return len(self.products) class Product: def __init__(self, id:int, name:str): self.id = id self.name = name def getProducts(): FILE= "Ressources/data/Produkte.db" productList = [] try: # Open product file and read lines to list. # Avoid u\ufeff prefix in data by set encoding to utf8-8-sig (source: stackoverflow) with open(FILE, 'r', encoding='utf-8-sig') as file: list = file.readlines() for line in list: # Split the line by ';' to get the id and name product_data = line.strip().split(';') product_id = int(product_data[0]) product_name = str(product_data[1]) productList.append(Product(id=product_id, name=product_name)) # Print the list of Product objects for product in productList: print(f"{product.id}: {product.name}") except FileNotFoundError: print("Error: could't find product list file 'Produkte.db'") except FileExistsError: print("Error: file 'Produkte.db' doesn't exist") return productList
in my main.py I pass the Model to my QQmlApplicationEngine:
import sys from pathlib import Path from PySide6.QtGui import QGuiApplication from PySide6.QtQml import QQmlApplicationEngine from Ressources.model import produktListModel if __name__ == "__main__": app = QGuiApplication(sys.argv) engine = QQmlApplicationEngine() productListModel = produktListModel.ProductListModel(produktListModel.getProducts()) engine.rootContext().setContextProperty("productListModel", productListModel) qml_file = Path(__file__).resolve().parent / "Ressources" / "qml" / "main.qml" engine.load(qml_file) if not engine.rootObjects(): sys.exit(-1) sys.exit(app.exec())
and here is my qml file:
import QtQuick.Controls 2.5 import QtQuick.Controls.Material import QtQml.Models 2.15 Rectangle { id: window radius: 10 color : "white" border.color: "#546E7A" border.width: 2 property bool expanded : true property int ex_height : 125 width: 400 height: 140 Image { id: arrow source: "../assets/angle-small-up.png" height: 15 fillMode: Image.PreserveAspectFit anchors.left: parent.left anchors.top: parent.top anchors.margins: 5 MouseArea { anchors.fill: parent onClicked: { arrow.source = window.expanded ? "../assets/angle-small-down.png" : "../assets/angle-small-up.png" window.expanded = window.expanded ? false : true window.height = window.expanded? window.ex_height : 25 seperator01.visible = seperator01.visible ? false: true seperator02.visible = seperator02.visible ? false : true productlist.visible = productlist.visible ? false : true } } } Text { id: title height: 15 anchors.left: arrow. right anchors.top : parent.top anchors.right: parent.right anchors.margins : 5 text: "Product List" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } Rectangle { id: seperator01 visible: true width : window.width - 10 height: 1 color: "#546E7A" anchors.top : arrow.bottom anchors.left: window.left anchors.margins : 5 Behavior on visible { PropertyAnimation{} } } Rectangle { id: seperator02 visible: true width : window.width - 10 height: 1 color: "#546E7A" anchors.bottom : parent.bottom anchors.left: window.left anchors.margins : 5 Behavior on visible { PropertyAnimation{ duration: 50; easing.type: Easing.OutCubic} } } ListView { id: productlist anchors.top: seperator01.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: seperator02.top model: productListModel clip: true delegate: Component{ id: namedelegate Text { text: model.name } } } }
When I execute the app I get an error for each line of my data: Unable to assign [undefined] to QString.
The last two days I tried to understand the issue and read some of the Qt docs. I also tried to find any answer here in the forum without success.In the console i can see that the file is read in correctly. I can also debug may.py and see that the engine.rootContext has the Model with correct data.
So i guessed the error might be in the .qml file?!? But googling and reading didn't bring me to a solution.Can anybody can explain me my mistake?
-
This is a wild guess, because there is a lot of code for a forum question, but I think the problem is
text: model.name
. With Qt 5 and PyQt or PySide, QML doesn't know anything about python (or C++) symbols. I'm presuming this continues to be true with the Qt 6 equivalents. As such, <python object>.<member> isn't usable.In Models and Views in Qt Quick, the
Object Instances as Models
andC++ Data Models
sections offer a few choices. With an otherwise functional model, implementing QAbstractItemModel::roleNames() is probably the easiest route.data()
will need to be updated to use the corresponding role numbers. -
Thank You for the explanation!
As you said I had to implement the roleNames() Function and adjusted the data().
The code below now works
import sys import typing import PySide6 from PySide6 import QtCore from PySide6 import QtGui from PySide6.QtCore import QByteArray class ProductListModel (QtCore.QAbstractListModel): def __init__(self, products, parent = None): super().__init__(parent) self.products = products def data(self, index, role): """Returns an appropriate value for the requested data. If the view requests an invalid index, an invalid variant is returned. Any valid index that corresponds to a string in the list causes that string to be returned.""" row = index.row() if not index.isValid() or row >= len(self.products): return None product = self.products[row] if role == QtCore.Qt.DisplayRole: return product.name elif role == QtCore.Qt.EditRole: return product.name elif role == QtCore.Qt.UserRole + 1: return product.id elif role == QtCore.Qt.UserRole + 2: return product.name return None def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): """Returns the appropriate header string depending on the orientation of the header and the section. If anything other than the display role is requested, we return an invalid variant.""" if role != QtCore.Qt.DisplayRole: return None if orientation == QtCore.Qt.Horizontal: return f"Column {section}" return f"Row {section}" def rowCount(self, parent: typing.Union[PySide6.QtCore.QModelIndex, PySide6.QtCore.QPersistentModelIndex] = ...) -> int: # return length of productList return len(self.products) def roleNames(self): roles = { QtCore.Qt.UserRole + 1: b'id', QtCore.Qt.UserRole + 2: b'name', } return roles class Product: def __init__(self, id:int, name:str): self.id = id self.name = name```
-