Please note that versions of this question have been asked before, but the various comments and proposed solutions have not worked. They either cover C++, for which I have tried replicating the solution without success, or more often than not, they are using older versions of Qt Quick and are methods that are no longer workable in the newer versions.
Problem
I am trying to display very simple 2D data in Qt Quick using a QML TableView
and Qt for Python / PySide6. Here is an example of what I am looking to create:
This table takes me just a few minutes to create in LabVIEW. However, I am completely at a loss for how to properly do this in QML and with Python using the singleton approach.
What I’ve read
QStandardItemModel
for Qt 6.7QStandardItemModel
for Qt for Python 6.7TreeView
QML type for Qt 6.7- Countless StackOverflow and forum posts, where I have tried to replicate supposed solutions to no avail.
A pure QML example that kind of works
The official documentation unfortunately highlights the simplest case in which the model is defined purely in QML.
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import Qt.labs.qmlmodels
import com.simplified
Window {
width: 740
height: 540
visible: true
title: "Python log viewer"
TableView {
id: log
anchors.fill: parent
columnSpacing: 1
rowSpacing: 1
clip: true
model: TableModel {
TableModelColumn {
display: "Timestamp"
}
TableModelColumn {
display: "Logger"
}
TableModelColumn {
display: "Level"
}
TableModelColumn {
display: "Message"
}
rows: [
{
"Timestamp": "today",
"Logger": "root",
"Level": "DEBUG",
"Message": "This is a debug message"
},
{
"Timestamp": "today",
"Logger": "root",
"Level": "INFO",
"Message": "This is an info message"
}
]
}
delegate: Rectangle {
border.width: 1
clip: true
Text {
text: display
anchors.centerIn: parent
}
}
}
}
This looks like:
Of course, the message column needs to be made wider, and I’m not sure how to get header names displayed using this setup, but it at least shows that the rest of the QML is correct and that the issue is with my model.
QML plus Python that doesn’t work
// Main.qml
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Controls
import Qt.labs.qmlmodels
import com.simplified
Window {
width: 740
height: 540
visible: true
title: "Python log viewer"
TableView {
id: log
anchors.fill: parent
columnSpacing: 1
rowSpacing: 1
clip: true
model: Simplified.log
delegate: Rectangle {
border.width: 1
clip: true
Text {
text: display
anchors.centerIn: parent
}
}
}
}
# main.py
QML_IMPORT_NAME = "com.simplified"
QML_IMPORT_MAJOR_VERSION = 1
# Core dependencies
from pathlib import Path
import sys
# Package dependencies
from PySide6.QtCore import QObject, Signal, Property, Qt
from PySide6.QtGui import QGuiApplication, QStandardItemModel, QStandardItem
from PySide6.QtQml import QQmlApplicationEngine, QmlElement, QmlSingleton
LOG_ENTRIES = [
{
"Timestamp": "2024-07-01 19:16:03.326",
"Name": "root.child",
"Level": "DEBUG",
"Message": "This is a debug message",
},
{
"Timestamp": "2024-07-01 19:16:03.326",
"Name": "root.child",
"Level": "INFO",
"Message": "This is an info message",
},
]
FIELD_NAMES = ["Timestamp", "Name", "Level", "Message"]
@QmlElement
@QmlSingleton
class Simplified(QObject):
log_changed = Signal()
def __init__(self) -> None:
super().__init__()
_ = self.log
self.log_changed.emit()
@Property(QStandardItemModel, notify=log_changed)
def log(self):
lines = LOG_ENTRIES
table = QStandardItemModel(len(lines), len(FIELD_NAMES))
table.setHorizontalHeaderLabels(FIELD_NAMES)
for line in lines:
row = [QStandardItem(str(line[key])) for key in FIELD_NAMES]
table.appendRow(row)
return table
if __name__ == "__main__":
application = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
qml_file = Path(__file__).resolve().parent / "qml" / "Main.qml"
engine.load(qml_file)
if not engine.rootObjects():
sys.exit(-1)
engine.singletonInstance("com.simplified", "Simplified")
sys.exit(application.exec())
No matter how I’ve tried building up the items and rows in the QStandardItemModel
, I have been unable to get anything to display. I have also tried all sorts of things in the text
field item inside the Text
component.
Here’s a set of commented out code I’ve tried, amongst many other permutations:
# table.setItemRoleNames(
# {i: FIELD_NAMES[i].encode() for i in range(len(FIELD_NAMES))}
# )
# for row_index, line in enumerate(lines):
# for column_index, key in enumerate(FIELD_NAMES):
# item = QStandardItem(str(line[key]))
# table.setItem(row_index, column_index, item)
# for line in lines:
# row = QStandardItem()
# for role_index, key in enumerate(FIELD_NAMES):
# row.setData(str(line[key]), Qt.ItemDataRole + role_index)
# table.appendRow(row)
# for line in lines:
# row = QStandardItem()
# for role_index, key in enumerate(FIELD_NAMES):
# row.appendColumn([QStandardItem(str(line[key]))])
# table.appendRow(row)
I have also tried using a combination of the two solutions, trying to provide a list model for the rows
of a TableModel
, but that property expects a JSON array. I was not able to convert anything in Python to something accepted by the rows
property.
Questions
-
What is the simplest, most direct way to display 2D data in a QML
TableView
from Python? -
Where should I go to learn more about this? The official documentation is close to useless along these lines.
-
Of course, I’d like to be able to style things better and get header names displaying, especially such that the header names are not the same names as the key names in the dictionaries. For some of this, I know that there is
DelegateChooser
, but I haven’t even gotten there.
Some side issues
I must admit that I am completely struggling here and experiencing a lot of frustration, especially coming from an ecosystem where I could have built this in just a few minutes. In addition to struggling to read about how this should work and get it to work and the absolutely zero feedback Qt is giving me, I am also experiencing the following issues:
-
You cannot used named arguments with Qt for Python. It generates a silent error (due to #2 or possibly always) and doesn’t work. This is clearly a bug.
-
I often get the following bizarre error:
<Unknown File>: qmlRegisterSingletonType(): "Simplified" is not available because the callback function returns a null pointer. Traceback (most recent call last): File "main.py", line 88, in <module> engine.singletonInstance("com.simplified", "Simplified") TypeError: Callback returns 0 value.
Usually this means there’s an error in the class definition such that it can’t instantiate, but Qt is completely eating the error. In the most simple of singleton classes, I cannot reproduce this error (i.e., I actually see the actual cause and report of the Python issue), but in almost all cases, I’m getting this non-error message that leaves me guessing as to what’s wrong.
-
I have received the following error, which also seems like a bug given that it reports “FIXME” for a Qt function:
FIXME qt_isinstance({0: 'Timestamp', 1: 'Name', 2: 'Level', 3: 'Message'}, typing.Dict[int, PySide6.QtCore.QByteArray]): Subscripted generics cannot be used with class and instance checks