Issue
I want to create a Qt data model with a structure like below to use in Python and QML. If any values or keys are changed, added, or deleted in Python or QML I would need the values to be updated on the other side (QML or Python). This would ideally be the model used in a ListView and I would only display certain fields for the ListView. But I would use this data model to store all my information. When a test is performed for memory in Python I would want to write that test log information to this data model and display that in QML. I have read about QAbstractListModel, but I am unsure if I can make nested objects or lists and whether they would update automatically or not.
System: {
Processor: {
name: 'Processor',
description: 'Intel i7 6600k',
iconSource: '/resources/images/chip.svg',
progressValue: 100,
pageSource: '/resources/qml/Processor.qml',
details: {
"badge": "Intel® Core i5 processor",
"cache": "6144 KB",
"clock": "4200000"
}
testLog: [
'Starting Cpu Test',
'Detected Intel CPU',
'Performing intense calculations',
'Processing calculations still',
'Cleaning up',
'Test Passed'
]
}
Memory: {
name: 'Memory',
description: 'Kingston 16GB DDR3',
iconSource: '/resources/images/ram.svg',
progressValue: 50,
pageSource: '/resources/qml/Processor.qml',
details: {
"device_locator_string": "ChannelB-DIMM1",
"device_set": 0,
"error_handle": 65534,
"extended_size": 0,
"form_factor": "Unknown"
},
testLog: [
'Starting Memory Test',
'Detected 2 x RAM modules',
'Performing intense calculations',
'Processing calculations still',
'Cleaning up',
'Test Failed'
]
}
}
Solution
There are several options in this case such as:
Create a model based on QAbstractItemModel where you provide the properties through roles.
Create a QObject Device that has the desired properties as qproperties and expose it through a qproperty associated with a signal from another QObject, the QObject Device list and use that list as a model.
Create a model as a QAbstractListModel (or QStandardItemModel) and expose the QObject through a role.
Create a QObject that exposes a list of QObjects Device through ListProperty.
In this case I have chosen the first option for a demo:
main.py
from dataclasses import dataclass
import sys
from typing import Callable
from PySide2.QtCore import (
Property,
QCoreApplication,
QObject,
QVariantAnimation,
Qt,
QUrl,
)
from PySide2.QtGui import QGuiApplication, QStandardItem, QStandardItemModel
from PySide2.QtQml import QQmlApplicationEngine
@dataclass
class item_property:
role: int
function: Callable = None
def __call__(self, function):
self.function = function
return self
class item_property_impl(property):
def __init__(self, role, function):
super().__init__()
self._role = role
self._function = function
def __get__(self, obj, type=None):
if obj is None:
return self
if hasattr(obj, "_initial"):
obj.setData(self._function(obj), self._role)
delattr(obj, "_initial")
return obj.data(self._role)
def __set__(self, obj, value):
obj.setData(value, self._role)
class ItemMeta(type(QStandardItem), type):
def __new__(cls, name, bases, attrs):
for key in attrs.keys():
attr = attrs[key]
if not isinstance(attr, item_property):
continue
new_prop = item_property_impl(attr.role, attr.function)
attrs[key] = new_prop
if not hasattr(cls, "attrs"):
cls._names = []
cls._names.append(key)
obj = super().__new__(cls, name, bases, attrs)
return obj
def __call__(cls, *args, **kw):
obj = super().__call__(*args, **kw)
obj._initial = True
for key in cls._names:
getattr(obj, key)
return obj
class Item(QStandardItem, metaclass=ItemMeta):
pass
keys = (b"name", b"description", b"icon", b"progress", b"source", b"details", b"log")
ROLES = (
NAME_ROLE,
DESCRIPTION_ROLE,
ICON_ROLE,
PROGRESS_ROLE,
SOURCE_ROLE,
DETAILS_ROLE,
LOG_ROLE,
) = [Qt.UserRole + i for i, _ in enumerate(keys)]
class Device(Item):
@item_property(role=NAME_ROLE)
def name(self):
return ""
@item_property(role=DESCRIPTION_ROLE)
def description(self):
return ""
@item_property(role=ICON_ROLE)
def icon(self):
return ""
@item_property(role=PROGRESS_ROLE)
def progress(self):
return 0
@item_property(role=SOURCE_ROLE)
def source(self):
return ""
@item_property(role=DETAILS_ROLE)
def details(self):
return dict()
@item_property(role=LOG_ROLE)
def log(self):
return list()
class DeviceManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._model = QStandardItemModel()
self._model.setItemRoleNames(dict(zip(ROLES, keys)))
def get_model(self):
return self._model
model = Property(QObject, fget=get_model, constant=True)
def add_device(self, *, name, description, icon, progress, source, details, log):
dev = Device()
dev.name = name
dev.description = description
dev.icon = icon
dev.progress = progress
dev.source = source
dev.details = details
dev.log = log
self.model.appendRow(dev)
return dev
def main():
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
manager = DeviceManager()
engine.rootContext().setContextProperty("device_manager", manager)
url = QUrl("main.qml")
def handle_object_created(obj, obj_url):
if obj is None and url == obj_url:
QCoreApplication.exit(-1)
engine.objectCreated.connect(handle_object_created, Qt.QueuedConnection)
engine.load(url)
processor = manager.add_device(
name="Processor",
description="Intel i7 6600k",
icon="/resources/images/chip.svg",
progress=10,
source="resources/qml/Processor.qml",
details={
"badge": "Intel® Core i5 processor",
"cache": "6144 KB",
"clock": "4200000",
},
log=[
"Starting Cpu Test",
"Detected Intel CPU",
"Performing intense calculations",
"Processing calculations still",
"Cleaning up",
"Test Passed",
],
)
memory = manager.add_device(
name="Memory",
description="Kingston 16GB DDR3",
icon="/resources/images/ram.svg",
progress=50,
source="resources/qml/Memory.qml",
details={
"device_locator_string": "ChannelB-DIMM1",
"device_set": 0,
"error_handle": 65534,
"extended_size": 0,
"form_factor": "Unknown",
},
log=[
"Starting Memory Test",
"Detected 2 x RAM modules",
"Performing intense calculations",
"Processing calculations still",
"Cleaning up",
"Test Failed",
],
)
def update_progress(value):
processor.progress = value
animation = QVariantAnimation(
startValue=processor.progress, endValue=100, duration=3 * 1000
)
animation.valueChanged.connect(update_progress)
animation.start()
ret = app.exec_()
sys.exit(ret)
if __name__ == "__main__":
main()
main.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
ApplicationWindow {
id: root
visible: true
width: 400
height: 400
ListView {
id: view
property url currentSource: ""
model: device_manager.model
width: parent.width / 2
height: parent.height
spacing: 10
clip: true
flickableDirection: Flickable.VerticalFlick
boundsBehavior: Flickable.StopAtBounds
currentIndex: -1
ScrollBar.vertical: ScrollBar {
}
highlight: Rectangle {
color: "lightsteelblue"
radius: 5
}
delegate: Rectangle {
id: rect
color: "transparent"
border.color: ListView.isCurrentItem ? "red" : "green"
height: column.height
width: ListView.view.width
Column {
id: column
Text {
text: model.name
}
ProgressBar {
from: 0
to: 100
value: model.progress
}
Label {
text: "Log:"
font.bold: true
font.pointSize: 15
}
Text {
text: model.log.join("\n")
}
}
MouseArea {
anchors.fill: parent
onClicked: {
rect.ListView.view.currentIndex = index;
rect.ListView.view.currentSource = model.source;
}
}
}
}
Rectangle {
x: view.width
width: parent.width / 2
height: parent.height
color: "salmon"
Loader {
anchors.centerIn: parent
source: view.currentSource
}
}
}
Processor.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle{
color: "red"
width: 100
height: 40
Text{
text: "Processor"
anchors.centerIn: parent
}
}
Memory.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle{
color: "blue"
width: 100
height: 40
Text{
text: "Memory"
anchors.centerIn: parent
}
}
├── main.py
├── main.qml
└── resources
└── qml
├── Memory.qml
└── Processor.qml
Answered By - eyllanesc
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.