如何在 qml 或 PySide2 中更改 XYSeries 中的所有点?

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

我对 PySide2 和 QML 有点陌生,我真的需要一种方法来一次替换 XYSeries 中的所有点。由于 QML 项目没有这样做的功能,我想我必须创建一个自定义 class(将从 QtCharts.QXYSeries 继承),实现我需要的功能,然后注册新类型PySide2.QtQml.qmlRegisterType,但我不知道该怎么做,而且我无法在网上找到答案(或者至少是我能理解的答案)。

所以,长话短说,我需要知道的是是否有办法更改 XYSeries 的所有点以及如何完成(例如创建自定义 class 并注册它,从 python 访问 .qml 文件中的 Item declarend 并更改其属性等...)。
我知道我的问题真的很模糊,但我不知道去哪里看和做什么...

编辑

我有一个 python class 从仪器获取数据并生成 X 和 Y 点数组。由于这个数组至少由 1000 个点组成,并且我需要至少 1Hz 的刷新率,所以不可能一次添加一个点(我有一个信号将整个数组发送到 qml 接口并且在那里,目前,我只是简单地清除系列并一次添加一对 XY 对。它有效但太慢了)。

一个可能的解决方案是创建一个 class 允许从 Python 访问 QML 对象,在这种情况下,我创建了通过 setContextProperty 导出到 QML 的助手 class通过将系列与 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
    }
}

我已经创建了一个“频谱分析仪”Python 项目,它可以正常工作,我希望它对你们中的一些人有用。

(在现实生活中,“createserie”函数可能包含 SCPI 命令,可以从任何频谱分析仪、示波器读取实际数据...)

此示例演示如何一起使用 QtQuick/QML、QtCharts 和 QThread。

单击“开始”按钮后,Qthread 启动并进入无限循环(可通过单击“停止”按钮终止循环)。

在每个循环中生成一些“虚拟”随机数据(基本上是 1000 点的“QXYSeries”)并更新绘图(实际上非​​常快)。

我正在使用 QThread,以便 GUI 保持随时响应。

我想分享这个例子,因为我花了很多时间写它,而且在网上找到一些好的 QML 资料也不是那么容易。

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_())

和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);
        }
    }
}

此致。 奥利维尔