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

时间:2019-03-20 09:11:36

标签: python qml pyside2

我是PySide2和QML的新手,我真的需要一种方法来一次替换XYSeries中的所有点。由于QML项没有执行此操作的功能,因此我认为必须创建一个自定义类(该类将从QtCharts.QXYSeries继承),实现所需的功能,然后向PySide2.QtQml.qmlRegisterType注册新类型,但我不知道该怎么做,也无法在线找到答案(或至少我能理解的答案)。

因此,为了简而言之,我需要知道的是是否有办法更改XYSeries的所有点以及如何完成(例如,创建自定义类并注册它,访问Item声明对象)在python的.qml文件中更改其属性等)。
我知道我的问题确实很模糊,但我不知道在哪里看以及该怎么做...

编辑

我有一个python类,该类从仪器获取数据并生成X和Y点的数组。由于此阵列是由至少1000个点组成的,并且由于我必须具有至少1Hz的刷新率,所以不可能一次添加一个点(我有一个信号将整个阵列发送到qml接口,在那儿,我暂时清除序列并一次添加一对XY对。它虽然有效,但是实在太慢了。

2 个答案:

答案 0 :(得分:1)

一种可能的解决方案是创建一个类,该类允许从Python访问QML对象,在这种情况下,我通过将序列与qproperty链接来创建通过setContextProperty导出到QML的帮助器类。

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
    }
}

答案 1 :(得分:1)

我已经创建了一个完全有效的“频谱分析仪”Python 项目,希望对你们中的一些人有用。

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

这个例子演示了如何一起使用 QtQuick/QML、QtCharts 和 QThread。

点击“START”按钮后Qthread启动并进入无限循环(点击“STOP”按钮可以终止循环)。

在每个循环中都会生成一些“虚拟”随机数据(基本上是 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);
        }
    }
}

最好的问候。 奥利维尔。