Search code examples
pythonqmlpyside2

Expose a looped changing python variable to QML


I am trying to take a python variable and use it within QML. My implementation is creating a button, which when clicked, will take data generated from my program and append the float values to x and y of a line series. Any suggestions?

Here is a little code from my program that will replicate my issue:

# ========================================================================  #
from math import exp


class ErrorFunction:
    def firstPoint(self, param):
        if param < 0:
            return -self.firstPoint(-param)
        else:
            p = 0.47047
            a = [0.3480242, -0.0958798, 0.7478556]
            t = 1.0 / (1.0 + p * param)
            return 1 - t * (a[0] + t * (a[1] + t * a[2])) * exp(-(param ** 2))

    def successivePoint(self, param):
        return 1 - self.firstPoint(param)
# ========================================================================  #
# ========================================================================  #
import os
import sys

from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtWidgets import QApplication

from ErrorFunction import ErrorFunction


class ErrorFunctionRunnable:
    def __init__(self):
        errorfunction = ErrorFunction()
        n = 100
        errorPoints = [(0, 0)] * n
        for i in range(n):
            x = i * 0.1 - 5.0
            errorPoints[i] = (errorfunction.firstPoint(x), errorfunction.successivePoint(x))
            print(errorPoints[i])


if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
    app = QApplication(sys.argv)

    engine = QQmlApplicationEngine()

    engine.load(os.path.join(os.path.dirname(__file__), "gui.qml"))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())
# ========================================================================  #
// ========================================================================  #
import QtQuick 2.0
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.3
import QtCharts 2.3

ApplicationWindow {
    id: applicationWindow
    visible: true
    width: 720
    height: 780
    title: qsTr("QML Test")
    Material.background: Material.color(Material.Grey, Material.Shade900)

    GroupBox {
        id: groupBox
        width: 114
        anchors.top: parent.top
        anchors.topMargin: 0
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 0
        anchors.left: parent.left
        anchors.leftMargin: 0

        Button {
            id: button
            x: 8
            y: 0
            width: 74
            height: 48
            text: qsTr("E_r")
            font.pointSize: 10
            Material.foreground: Material.color(Material.Grey, Material.Shade100)
            Material.background: Material.color(Material.Grey, Material.Shade800)
            onClicked: {
                line.removeAllSeries()
                /*
                Run the "ErrorFunctionRunnable" init function to generate "errorPoints"
                */
            }
        }
    }

    GroupBox {
        id: groupBox2
        anchors.right: parent.right
        anchors.rightMargin: 0
        anchors.top: parent.top
        anchors.topMargin: 0
        anchors.left: parent.left
        anchors.leftMargin: 114
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 75

        ChartView {
            id: line
            dropShadowEnabled: false
            anchors.left: parent.left
            anchors.leftMargin: 0
            anchors.top: parent.top
            anchors.topMargin: 0
            anchors.right: parent.right
            anchors.rightMargin: 0
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 0
            theme: ChartView.ChartThemeDark
            antialiasing: true

            backgroundColor: Material.color(Material.Grey, Material.Shade900)
            LineSeries {
                name: "Marshak Wave"
                /*
                Update the line series with the points from "errorPoints"
                */
            }
        }
    }

    GroupBox {
        id: groupBox1
        y: 704
        height: 76
        anchors.right: parent.right
        anchors.rightMargin: 0
        anchors.left: parent.left
        anchors.leftMargin: 114
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 0


    }

}
// ========================================================================  #


Solution

  • A possible solution is to create a QObject that exposes the QPointF list through a method to QML, in addition the limits of the axes must be updated as shown below:

    import math
    import os
    import sys
    
    from PySide2.QtCore import QObject, QPointF, Slot
    from PySide2.QtQml import QQmlApplicationEngine
    from PySide2.QtWidgets import QApplication
    
    
    class ErrorFunction:
        def firstPoint(self, param):
            if param < 0:
                return -self.firstPoint(-param)
            else:
                p = 0.47047
                a = [0.3480242, -0.0958798, 0.7478556]
                t = 1.0 / (1.0 + p * param)
                return 1 - t * (a[0] + t * (a[1] + t * a[2])) * math.exp(-(param ** 2))
    
        def successivePoint(self, param):
            return 1 - self.firstPoint(param)
    
    
    class Bridge(QObject):
        @Slot(result="QVariantList")
        def produce(self):
            f = ErrorFunction()
            values = []
            n = 100
            for i in range(n):
                x = i * 0.1 - 5.0
                p = QPointF(f.firstPoint(x), f.successivePoint(x))
                values.append(p)
            return values
    
    
    if __name__ == "__main__":
        os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
        app = QApplication(sys.argv)
    
        bridge = Bridge()
    
        engine = QQmlApplicationEngine()
        engine.rootContext().setContextProperty("bridge", bridge)
    
        engine.load(os.path.join(os.path.dirname(__file__), "gui.qml"))
    
        if not engine.rootObjects():
            sys.exit(-1)
    
        sys.exit(app.exec_())
    
    import QtQuick 2.0
    import QtQuick.Controls 2.5
    import QtQuick.Controls.Material 2.3
    import QtCharts 2.3
    
    ApplicationWindow {
        id: applicationWindow
        visible: true
        width: 720
        height: 780
        title: qsTr("QML Test")
        Material.background: Material.color(Material.Grey, Material.Shade900)
    
        GroupBox {
            id: groupBox
            width: 114
            anchors.top: parent.top
            anchors.topMargin: 0
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 0
            anchors.left: parent.left
            anchors.leftMargin: 0
    
            Button {
                id: button
                x: 8
                y: 0
                width: 74
                height: 48
                text: qsTr("E_r")
                font.pointSize: 10
                Material.foreground: Material.color(Material.Grey, Material.Shade100)
                Material.background: Material.color(Material.Grey, Material.Shade800)
                onClicked: {
                    line.removePoints(0, line.count)
                    var x_min = line.axisX.min
                    var x_max = line.axisX.max
                    var y_min = line.axisY.min
                    var y_max = line.axisY.max
    
                    var values = bridge.produce()
                    for(var i in values){
                        line.append(values[i].x, values[i].y)
                        x_min = Math.min(x_min, values[i].x)
                        x_max = Math.max(x_max, values[i].x)
                        y_min = Math.min(y_min, values[i].y)
                        y_max = Math.max(y_max, values[i].y)
                    }
                    line.axisX.min = x_min
                    line.axisX.max = x_max
                    line.axisY.min = y_min
                    line.axisY.max = y_max
                }
            }
        }
    
        GroupBox {
            id: groupBox2
            anchors.fill: parent
            anchors.rightMargin: 0
            anchors.topMargin: 0
            anchors.leftMargin: 114
            anchors.bottomMargin: 75
    
            ChartView {
                id: view
                dropShadowEnabled: false
                anchors.fill: parent
                anchors.margins: 0
                theme: ChartView.ChartThemeDark
                antialiasing: true
    
                backgroundColor: Material.color(Material.Grey, Material.Shade900)
                LineSeries {
                    id: line
                    name: "Marshak Wave"
                }
            }
        }
        GroupBox {
            id: groupBox1
            y: 704
            height: 76
            anchors.right: parent.right
            anchors.rightMargin: 0
            anchors.left: parent.left
            anchors.leftMargin: 114
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 0
        }
    }
    

    enter image description here

    The disadvantage is the for-loop in QML if the data has many elements, a possible optimization is to use the replace method but it is not accessible in QML so the element has to be exported to python:

    import math
    import os
    import sys
    
    from PySide2.QtCore import QObject, QPointF, Slot
    from PySide2.QtQml import QQmlApplicationEngine
    from PySide2.QtWidgets import QApplication
    from PySide2.QtCharts import QtCharts
    
    
    class ErrorFunction:
        def firstPoint(self, param):
            if param < 0:
                return -self.firstPoint(-param)
            else:
                p = 0.47047
                a = [0.3480242, -0.0958798, 0.7478556]
                t = 1.0 / (1.0 + p * param)
                return 1 - t * (a[0] + t * (a[1] + t * a[2])) * math.exp(-(param ** 2))
    
        def successivePoint(self, param):
            return 1 - self.firstPoint(param)
    
    
    class Bridge(QObject):
        @Slot(QtCharts.QAbstractSeries)
        def fill(self, series):
            axis_x = series.property("axisX")
            axis_y = series.property("axisY")
            xmin = axis_x.min()
            xmax = axis_x.max()
            ymin = axis_y.min()
            ymax = axis_y.max()
            f = ErrorFunction()
            values = []
            n = 100
            for i in range(n):
                x = i * 0.1 - 5.0
                xi = f.firstPoint(x)
                yi = f.successivePoint(x)
                p = QPointF(xi, yi)
                xmin = min(xmin, xi)
                xmax = max(xmax, xi)
                ymin = min(ymin, yi)
                ymax = max(ymax, yi)
                values.append(p)
            series.replace(values)
            axis_x.setMin(xmin)
            axis_x.setMax(xmax)
            axis_y.setMin(ymin)
            axis_y.setMax(ymax)
    
    
    if __name__ == "__main__":
        os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
        app = QApplication(sys.argv)
    
        bridge = Bridge()
    
        engine = QQmlApplicationEngine()
        engine.rootContext().setContextProperty("bridge", bridge)
    
        engine.load(os.path.join(os.path.dirname(__file__), "gui.qml"))
    
        if not engine.rootObjects():
            sys.exit(-1)
    
        sys.exit(app.exec_())
    
    import QtQuick 2.0
    import QtQuick.Controls 2.5
    import QtQuick.Controls.Material 2.3
    import QtCharts 2.3
    
    ApplicationWindow {
        id: applicationWindow
        visible: true
        width: 720
        height: 780
        title: qsTr("QML Test")
        Material.background: Material.color(Material.Grey, Material.Shade900)
    
        GroupBox {
            id: groupBox
            width: 114
            anchors.top: parent.top
            anchors.topMargin: 0
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 0
            anchors.left: parent.left
            anchors.leftMargin: 0
    
            Button {
                id: button
                x: 8
                y: 0
                width: 74
                height: 48
                text: qsTr("E_r")
                font.pointSize: 10
                Material.foreground: Material.color(Material.Grey, Material.Shade100)
                Material.background: Material.color(Material.Grey, Material.Shade800)
                onClicked: {
                    bridge.fill(line)
                }
            }
        }
    
        GroupBox {
            id: groupBox2
            anchors.fill: parent
            anchors.rightMargin: 0
            anchors.topMargin: 0
            anchors.leftMargin: 114
            anchors.bottomMargin: 75
    
            ChartView {
                id: view
                dropShadowEnabled: false
                anchors.fill: parent
                anchors.margins: 0
                theme: ChartView.ChartThemeDark
                antialiasing: true
    
                backgroundColor: Material.color(Material.Grey, Material.Shade900)
                LineSeries {
                    id: line
                    name: "Marshak Wave"
                }
            }
        }
        GroupBox {
            id: groupBox1
            y: 704
            height: 76
            anchors.right: parent.right
            anchors.rightMargin: 0
            anchors.left: parent.left
            anchors.leftMargin: 114
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 0
        }
    }