Search code examples
pythonqtpyqtqmlpyside

QT/QML Data Model


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
    

    enter image description here