Search code examples
pythonqmlpysidepyside6

Python dynamically instantiate QML Components


I want to use Python to dynamically add custom components onto my view.qml, but I am not sure about my method because I can't see the Button.qml component in the resulting window. Ideally, I hope to be able to instantiate several rows of buttons into the ColumnLayout. By the way, Button.qml custom quick example/demo button whose's source code I have included too below. It is not QtQuick Button.qml from the PySide6 library

I thought I could just call functions from the view.qml but apparently not? I have seen another method that involves using a separate Javascript file, but I would like to avoid doing that if possible.

Main.py

import os
from pathlib import Path
import sys
from PySide6.QtCore import  QUrl, QObject
from PySide6.QtGui import QGuiApplication
from PySide6.QtQuick import QQuickView

class CreateWidgets(QObject):
    
    def instantiate_widgets(self, root, widgetsNeeded):
        #for i in widgetsNeeded:
            root.doSomething
    
if __name__ == '__main__':
    app = QGuiApplication(sys.argv)
    view = QQuickView()
    view.setResizeMode(QQuickView.SizeRootObjectToView);

    qml_file = os.fspath(Path(__file__).resolve().parent / 'view.qml')
    view.setSource(QUrl.fromLocalFile(qml_file))
    if view.status() == QQuickView.Error:
        sys.exit(-1)
    
    root = view.rootObject()
    widgetCreator = CreateWidgets()
    widgetCreator.instantiate_widgets(root, 6)
    
    view.show()
    res = app.exec()
    # Deleting the view before it goes out of scope is required to make sure all child QML instances
    # are destroyed in the correct order.
    del view
    sys.exit(res)

view.qml

import QtQuick 2.0
import QtQuick.Layouts 1.12

Item{
    function doSomething(){
        var component = Qt.createComponent("Button.qml");
        if (component.status === Component.Ready) {
            var button = component.createObject(colLayout);
            button.color = "red";
        }
        console.log("Button created");
    }
    ColumnLayout{
        id: colLayout
        Rectangle {
            id: page           
            width: 500; height: 200
            color: "lightgray"  
         }   
    }
}

Button.qml

import QtQuick 2.0

Rectangle { width: 80; height: 50; color: "red"; anchors.fill: parent}

(Code reference for questions in comments section)

Main.py
import os
import random
import sys
from pathlib import Path

from PySide6.QtCore import Property, QUrl, QObject, Qt
from PySide6.QtGui import QColor, QGuiApplication, QStandardItem, QStandardItemModel
from PySide6.QtQuick import QQuickView

ColorRole = Qt.UserRole
BorderRole = Qt.UserRole

class Manager(QObject):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._model = QStandardItemModel()
        self._model.setItemRoleNames({Qt.DisplayRole: b"display", ColorRole: b"custom", BorderRole: b"custom2"})

    @Property(QObject, constant=True)
    def model(self):
        return self._model

    def add_button(self, text, color, bColor):
        item = QStandardItem(text)
        item.setData(color, ColorRole)
        item.setData(bColor, BorderRole)
        self._model.appendRow(item)


if __name__ == "__main__":
    app = QGuiApplication(sys.argv)

    manager = Manager()

    view = QQuickView()
    view.rootContext().setContextProperty("manager", manager)
    view.setResizeMode(QQuickView.SizeRootObjectToView)

    qml_file = os.fspath(Path(__file__).resolve().parent / "view.qml")
    view.setSource(QUrl.fromLocalFile(qml_file))
    if view.status() == QQuickView.Error:
        sys.exit(-1)

    for i in range(6):
        color = QColor(*random.sample(range(0, 255), 3))
        border = QColor(*random.sample(range(0, 255), 3))
        manager.add_button(f"button-{i}", color, border)

    view.show()
    res = app.exec()
    sys.exit(res)

View.qml

import QtQuick 2.0
import QtQuick.Layouts 1.12

Item {
    ColumnLayout {
        id: colLayout
        anchors.fill: parent
        Repeater{
            model: manager.model
            Button{
                color: model.custom
                text: model.display
                border.color: model.custom2
            }
        }
    }
}

Button.qml

import QtQuick 2.0

Rectangle {
    id: root

    property alias text: txt.text
    width: 80
    height: 50
    color: "red"
    border.color: "black"

    Text{
        id: txt
        anchors.centerIn: parent
    }
}

Solution

  • The idea is that Python (or C++) provide the information to QML to create the items for example using a model and a Repeater.

    On the other hand, if an item is going to be a child of a ColumnLayout then it should not use anchors since there will be conflicts since they both handle the geometry of the item.

    Considering the above I have added more elements such as variable text, variable color, etc. to demonstrate the logic.

    import os
    import random
    import sys
    from pathlib import Path
    
    from PySide6.QtCore import Property, QUrl, QObject, Qt
    from PySide6.QtGui import QColor, QGuiApplication, QStandardItem, QStandardItemModel
    from PySide6.QtQuick import QQuickView
    
    ColorRole = Qt.UserRole
    
    
    class Manager(QObject):
        def __init__(self, parent=None):
            super().__init__(parent)
            self._model = QStandardItemModel()
            self._model.setItemRoleNames({Qt.DisplayRole: b"display", ColorRole: b"custom"})
    
        @Property(QObject, constant=True)
        def model(self):
            return self._model
    
        def add_button(self, text, color):
            item = QStandardItem(text)
            item.setData(color, ColorRole)
            self._model.appendRow(item)
    
    
    if __name__ == "__main__":
        app = QGuiApplication(sys.argv)
    
        manager = Manager()
    
        view = QQuickView()
        view.rootContext().setContextProperty("manager", manager)
        view.setResizeMode(QQuickView.SizeRootObjectToView)
    
        qml_file = os.fspath(Path(__file__).resolve().parent / "view.qml")
        view.setSource(QUrl.fromLocalFile(qml_file))
        if view.status() == QQuickView.Error:
            sys.exit(-1)
    
        for i in range(6):
            color = QColor(*random.sample(range(0, 255), 3))
            manager.add_button(f"button-{i}", color)
    
        view.show()
        res = app.exec()
        sys.exit(res)
    
    import QtQuick 2.0
    import QtQuick.Layouts 1.12
    
    Item {
        ColumnLayout {
            id: colLayout
            anchors.fill: parent
            Repeater{
                model: manager.model
                Button{
                    color: model.custom
                    text: model.display
                }
            }
        }
    }
    
    import QtQuick 2.0
    
    Rectangle {
        id: root
    
        property alias text: txt.text
        width: 80
        height: 50
        color: "red"
    
        Text{
            id: txt
            anchors.centerIn: parent
        }
    }
    

    Update:

    Each role must have a different numerical value since otherwise Qt cannot identify it, in your case you could change:

    BorderRole = Qt.UserRole + 1