如何在 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);
}
}
}
此致。
奥利维尔
我对 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);
}
}
}
此致。 奥利维尔