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