Как я могу изменить все точки в XYSeries в qml или PySide2?

Я новичок в PySide2 и QML, и мне действительно нужен способ заменить все точки в XYSeries сразу. Поскольку элемент QML не имеет функции, которая это делает, я подумал, что мне нужно создать собственный класс (который наследуется от QtCharts.QXYSeries), реализовать нужную мне функцию, а затем зарегистрировать новый тип с помощью PySide2.QtQml.qmlRegisterType, но я не знаю, как это сделать, и я не смог найти ответ в Интернете (или, по крайней мере, тот, который я мог бы понять).

Итак, короче говоря, мне нужно знать, есть ли способ изменить все точки XYSeries и как это можно сделать (например, создать собственный класс и зарегистрировать его, получить доступ к объявленному элементу в файле . qml из python и изменение его свойств и т. д.).
Я знаю, что мой вопрос действительно расплывчатый, но я не знаю, где искать и что делать...

ИЗМЕНИТЬ

У меня есть класс Python, который получает данные от инструментов и генерирует массив точек X и Y. Поскольку эти массивы состоят как минимум из 1000 точек и так как мне нужно иметь частоту обновления не менее 1 Гц, то невозможно сделать это, добавляя по одной точке за раз (у меня есть сигнал, который отправляет весь массив в интерфейс qml и там, на данный момент, я просто очищаю серию и добавляю по одной паре XY за раз. Это работает, но чертовски медленно).


person Lando1784    schedule 20.03.2019    source источник


Ответы (2)


Одним из возможных решений является создание класса, который разрешает доступ к объекту QML из Python. В этом случае я создаю вспомогательный класс, который экспортирую в QML через setContextProperty, связывая серию с 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
    }
}
person eyllanesc    schedule 20.03.2019
comment
Это работает, мне все еще нужно найти способ интегрировать его в свой код (он выдает мне это сообщение: Ошибка: невозможно назначить QObject* незарегистрированному типу), но я как-нибудь справлюсь. - person Lando1784; 23.03.2019

Я создал проект Spectrum Analyzer Python, который полностью работает, и я надеюсь, что он может быть полезен для некоторых из вас.

(В реальной жизни функция createerie может содержать команды SCPI, которые будут считывать фактические данные с любого анализатора спектра, осциллографов...)

В этом примере показано, как использовать QtQuick/QML, QtCharts и QThread вместе.

После нажатия кнопки START Qthread запускается и входит в бесконечный цикл (цикл можно прервать, нажав кнопку STOP).

В каждом цикле генерируются некоторые фиктивные случайные данные (в основном QXYSeries из 1000 точек), и график обновляется (на самом деле это очень быстро).

Я использую QThread, чтобы графический интерфейс всегда оставался отзывчивым.

Я хочу поделиться этим примером, потому что мне потребовалось много времени, чтобы написать его, и было не так просто найти хорошую информацию о QML в Интернете.

Основной.ру:

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

Наилучшие пожелания. Оливье.

person Olivier Pelhatre    schedule 20.01.2021