简体   繁体   中英

How can I change all the points in an XYSeries in qml or PySide2?

I'm kinda new to PySide2 and QML and I really need a way to replace all the points in an XYSeries at once. Since the QML item does not have a function that does so, I thought I had to create a custom class (that would inherits from QtCharts.QXYSeries), implement the function I need and then register the new type with PySide2.QtQml.qmlRegisterType, but I don't know how that should be done and I haven't been able to find an answer online (or at least one that I could understand).

So, to cut a long story short, what I need to know is if there's a way to change all the points of an XYSeries and how can it be done (eg creating a custom class and registering it, accessing the Item declarend in the .qml file from python and chaning its properties, etc...).
I know my question is really vague but I do not know where to look and what to do...

EDIT

I have a python class that acquires data from an instruments and generates an array of X and Y points. Since this arrays are made from at least 1000 points and since I need to have a refresh rate of at least 1Hz, it's impossible to do it adding one point at a time (I have an signal that sends the whole array to the qml interface and there, for the moment, I simply clear the series and add one XY couple at a time. It works but it's too damn slow).

One possible solution is to create a class that allows access to a QML object from Python, in this case I create the helper class that I export to QML through setContextProperty by linking the series with a qproperty.

main.py

import random
from PySide2 import QtCore, QtWidgets, QtQml
from PySide2.QtCharts import QtCharts

class Helper(QtCore.QObject):
    serieChanged = QtCore.Signal()

    def __init__(self, parent=None):
        super(Helper, self).__init__(parent)
        self._serie = None

    def serie(self):
        return self._serie

    def setSerie(self, serie):
        if self._serie == serie:
            return
        self._serie = serie
        self.serieChanged.emit()

    serie = QtCore.Property(QtCharts.QXYSeries, fget=serie, fset=setSerie, notify=serieChanged)

    @QtCore.Slot(list)
    def replace_points(self, points):
        if self._serie is not None:
            self._serie.replace(points)

class Provider(QtCore.QObject):
    pointsChanged = QtCore.Signal(list)

    def __init__(self, parent=None):
        super(Provider, self).__init__(parent)
        timer = QtCore.QTimer(
            self, 
            interval=100,
            timeout=self.generate_points
        )
        timer.start()

    @QtCore.Slot()
    def generate_points(self):
        points = []
        for i in range(101):
            point = QtCore.QPointF(i, random.uniform(-10, 10))
            points.append(point)
        self.pointsChanged.emit(points)

if __name__ == '__main__':
    import os
    import sys
    app = QtWidgets.QApplication(sys.argv)
    helper = Helper()
    provider = Provider()
    provider.pointsChanged.connect(helper.replace_points)
    engine = QtQml.QQmlApplicationEngine()
    engine.rootContext().setContextProperty("helper", helper)
    file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "main.qml")
    engine.load(QtCore.QUrl.fromLocalFile(file))
    if not engine.rootObjects():
        sys.exit(-1)
    sys.exit(app.exec_())

main.qml

import QtQuick 2.9
import QtQuick.Window 2.2
import QtCharts 2.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")
    ChartView{
        anchors.fill: parent
        LineSeries{
            id: serie
            axisX: axisX
            axisY: axisY
        }
        ValueAxis {
            id: axisX
            min: 0
            max: 100
        }

        ValueAxis {
            id: axisY
            min: -10
            max: 10
        }
        Component.onCompleted: helper.serie = serie
    }
}

i have created a "Spectrum Analyser" Python project that is fully working, i hope it may be useful for some of you.

(In the real-life the "createserie" function may contain SCPI commands that would read actual data from any Spectrum Analyser, Oscilloscopes...)

This example demonstrates how to use QtQuick/QML, QtCharts and QThread together.

After clicking the "START" button the Qthread starts and enters a infinite loop (the loop can be terminated by clicking the "STOP" button).

In each loop some "dummy" random data (basically a "QXYSeries" of 1000 points) are generated and the plot is updated (it's actually very fast).

I am using a QThread so that the GUI remains anytime responsive.

I want to share this example because it took me a lot of time to write it and it was not that easy to find some good QML information online.

Main.py:

import sys
import os
# import time
import random
from PySide2.QtCore import Qt, QUrl, QThread, QPoint, QPointF, Slot, Signal, QObject, QProcess, Property, qInstallMessageHandler, QtInfoMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg
from PySide2.QtQuick import QQuickView
from PySide2.QtWidgets import QApplication, QMainWindow, QMessageBox
# from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCharts import QtCharts
# import pdb

print(chr(27) + "[2J")

def qt_message_handler(mode, context, message):
    if mode == QtInfoMsg:
        mode = 'Info'
    elif mode == QtWarningMsg:
        mode = 'Warning'
    elif mode == QtCriticalMsg:
        mode = 'critical'
    elif mode == QtFatalMsg:
        mode = 'fatal'
    else:
        mode = 'Debug'
    print("%s: %s (%s:%d, %s)" % (mode, message, context.file, context.line, context.file))
    

class Worker1(QObject):
    set_val = Signal(QtCharts.QXYSeries)
    finished = Signal()
    
    def __init__(self, serie, parent=None):
        QObject.__init__(self, parent)
        self._serie = serie
        self._isRunning = True 
        
    def run(self):
        measure(self)    
        
    def stop(self):
        self._isRunning = False
        
        
def measure(self): # Called inside Thread1
    while 1:
        if self._isRunning == True:
            createserie(self)
            self.set_val.emit(self._serie)
            # time.sleep(0.002)
        else:
            print("QUITING LOOP")
            break
    self.finished.emit()
    return


def createserie(self):
    points = []
    for i in range(1001):
        points.append(QPointF(i/1000, random.random()))
    self._serie.replace(points)
    

class Backend(QObject):
    setval = Signal(QtCharts.QXYSeries)  
    
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self._serie = None
    
    @Slot(QtCharts.QXYSeries) # expose QML serie to Python
    def exposeserie(self, serie):
        self._serie = serie
        print(serie)
        print("QML serie exposed to Python")
        
    @Slot(str)
    def startthread(self, text):
        self.WorkerThread = QThread()
        self.worker = Worker1(self._serie)
        self.WorkerThread.started.connect(self.worker.run)
        self.worker.finished.connect(self.end)
        self.worker.set_val.connect(self.setval)
        self.worker.moveToThread(self.WorkerThread)  # Move the Worker object to the Thread object
        self.WorkerThread.start()
        
    @Slot(str)     
    def stopthread(self, text):
        self.worker.stop()
        print("CLOSING THREAD")
               
    def end(self):
        self.WorkerThread.quit()
        self.WorkerThread.wait()
        msgBox = QMessageBox() 
        msgBox.setText("THREAD CLOSED")
        msgBox.exec()
        

class MainWindow(QObject):
    def __init__(self, parent = None):
        # Initialization of the superclass
        super(MainWindow, self).__init__(parent)
        
        qInstallMessageHandler(qt_message_handler)
        
        self.backend = Backend()

        # Expose the Python object to QML
        self.engine = QQmlApplicationEngine()
                
        self.context = self.engine.rootContext()
        self.context.setContextProperty("backend", self.backend)
        
        # Load the GUI
        self.engine.load(os.path.join(os.path.dirname(__file__), "SpecPXA_QML.qml"))
        if not self.engine.rootObjects():
            sys.exit(-1)
        
        self.win = self.engine.rootObjects()[0]
        
        # Execute a function if "Start" button clicked
        startbutton = self.win.findChild(QObject, "startbutton")
        startbutton.startclicked.connect(self.startclicked)
        
        # Execute a function if "Stop" button clicked
        stopbutton = self.win.findChild(QObject, "stopbutton")
        stopbutton.stopclicked.connect(self.stopclicked)
        
    def startclicked(self):
        print("START")
        self.backend.startthread("test")
        
    def stopclicked(self):
        print("STOP")
        self.backend.stopthread("test")

        
if __name__ == "__main__":
    
    if not QApplication.instance():
        app = QApplication(sys.argv)
    else:
        app = QApplication.instance()
    app.setStyle('Fusion') # 'Breeze', 'Oxygen', 'QtCurve', 'Windows', 'Fusion'
    w = MainWindow()
    sys.exit(app.exec_())

and SpecPXA_QML.qml:

import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15
import QtQuick.Dialogs 1.2
import QtCharts 2.3


ApplicationWindow {
    width: 1200
    height: 700
    visible: true
    title: qsTr("Hello World")
    
    property var xySeries;   

//    MessageDialog {
//        id: messageDialogQuit
//        title: "Question:"
//        icon: StandardIcon.Question
//        text: "Quit program?"
//        standardButtons: StandardButton.Yes |StandardButton.No
//        //        Component.onCompleted: visible = true
//        onYes: {
//            Qt.quit()
//            close.accepted = true
//        }
//        onNo: {
//            close.accepted = false
//        }
//     }
//    onClosing: {
//        close.accepted = true
//        onTriggered: messageDialogQuit.open()
//    }

    MenuBar {
        id: menuBar
        width: Window.width

        Menu {
            title: qsTr("&File")
            Action { text: qsTr("&New...") }
            Action { text: qsTr("&Open...") }
            Action { text: qsTr("&Save") }
            Action { text: qsTr("Save &As...") }
            MenuSeparator { }
            Action { text: qsTr("&Quit") }
        }
        Menu {
            title: qsTr("&Edit")
            Action { text: qsTr("Cu&t") }
            Action { text: qsTr("&Copy") }
            Action { text: qsTr("&Paste") }
        }
        Menu {
            title: qsTr("&Help")
            Action { text: qsTr("&About") }
        }
    }

    SplitView {
        id: splitView
        y: menuBar.height
        width: Window.width
        height: Window.height-(menuBar.height+infoBar.height)
        orientation: Qt.Horizontal
        Rectangle {
            id: leftitem
            height: Window.height
            implicitWidth: 200
            color: "red"
            anchors.left: parent.left
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            anchors.leftMargin: 0
            anchors.bottomMargin: 0
            anchors.topMargin: 0

            Button {
                //id: startbutton
                signal startclicked
                objectName: "startbutton"
                y: 40
                height: 40
                text: qsTr("Start")
                anchors.left: parent.left
                anchors.right: parent.right
                checkable: false
                anchors.rightMargin: 30
                anchors.leftMargin: 30
                onClicked: startclicked("START")
                //onClicked: backend.text = "Button was pressed"
            }

            Button {
                //id: stopbutton
                signal stopclicked
                objectName: "stopbutton"
                y: 100
                height: 40
                text: qsTr("Stop")
                anchors.left: parent.left
                anchors.right: parent.right
                checked: false
                checkable: false
                anchors.rightMargin: 30
                anchors.leftMargin: 30
                onClicked: stopclicked("STOP")
            }

        }
        Rectangle {
            id: rightitem
            height: Window.height
            color: "green"
            anchors.right: parent.right
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            anchors.topMargin: 0
            anchors.rightMargin: 0
            anchors.bottomMargin: 0

            Rectangle {
                id: rectangle
                color: "#ffffff"
                anchors.fill: parent
                anchors.rightMargin: 30
                anchors.leftMargin: 30
                anchors.bottomMargin: 30
                anchors.topMargin: 30

                ChartView {
                    id: line
                    anchors.fill: parent
                    
                    ValueAxis {
                        id: axisX
                        min: 0
                        max: 1
                    }

                    ValueAxis {
                        id: axisY
                        min: 0
                        max: 1
                    }

//                    LineSeries {
//                       id: xySeries
//                       name: "my_Serie"
//                       axisX: axisX
//                       axisY: axisY
//                       useOpenGL: true
//                       XYPoint { x: 0.0; y: 0.0 }
//                       XYPoint { x: 1.1; y: 2.1 }
//                       XYPoint { x: 1.9; y: 3.3 }
//                       XYPoint { x: 2.1; y: 2.1 }
//                       XYPoint { x: 2.9; y: 4.9 }
//                       XYPoint { x: 3.4; y: 3.0 }
//                       XYPoint { x: 4.1; y: 3.3 }
//                    }
                    
                    Component.onCompleted: {
                        xySeries = line.createSeries(ChartView.SeriesTypeLine, "my_plot", axisX, axisY);  
                        xySeries.useOpenGL = true                    
                        backend.exposeserie(xySeries) // expose the serie to Python (QML to Python)
                    }
                    
                }
            }
        }
    }

    MenuBar {
        id: infoBar
        x: 0
        y: 440
        width: Window.width
        height: 30
        anchors.bottom: parent.bottom
        anchors.bottomMargin: 0
    }
    
   
    
    Connections {
        target: backend
        
        function onSetval(serie) {  // "serie" is calculated in python (Python to QML)
            xySeries = serie;       // progressbar.value = val  
//            console.log(serie);
        }
    }
}

Best Regards. Olivier.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM