Я создал проект 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