Neste tutorial, veremos como observar um sinal de tempo em forma de gráfico com o PyQt usando o PyQtGraph. Se estiver criando interfaces gráficas, pode ser uma boa ideia exibi-las na forma de curvas, como em um osciloscópio, em vez de números rolantes.
Instalação
- PyQt (ou PySide)
pip install PyQt5
ou
pip install pyside6
- PyQtGraph
pip install pyqtgraph
Código para exibir uma curva simples usando PyQtGraph
import cv2
import sys
#from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap
from pyqtgraph import PlotWidget, plot
from PySide6.QtWidgets import QMainWindow, QWidget, QLabel, QApplication, QVBoxLayout
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal #convert pyqt to pyside
pyqtSlot = Slot
class SignalContainer(QWidget):
def __init__(self):
super().__init__()
self.title = 'Signal'
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
#self.resize(1200, 800)
layout = QVBoxLayout()
self.setLayout(layout)
# create widget
self.graphWidget = PlotWidget()
layout.addWidget(self.graphWidget)
#plot data
time = [1,2,3,4,5,6,7,8,9,10]
data = [30,32,34,32,33,31,29,32,35,45]
self.graphWidget.plot(time, data)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = SignalContainer()
ex.show()
sys.exit(app.exec())
Código para criar um sinal horário
Para tornar esta curva viva, vamos criar um objeto QThread de modo a não bloquear a aplicação, o que nos permitirá criar um sinal sinusoidal que evoluirá ao longo do tempo. Em cada iteração, enviaremos uma atualização utilizando o sinal changeData.
class Thread(QThread): changeData = pyqtSignal(float,float) def run(self): self.isRunning=True self.time = 0 self.data = 0 f = 1. w = 2. * np.pi * f while self.isRunning: self.time+=0.01 self.data=2*np.sin(w*self.time) self.changeData.emit(self.time,self.data) time.sleep(0.01) def stop(self): self.isRunning=False self.quit() self.terminate()
Código da aplicação PyQt
Para apresentar a curva numa aplicação, criaremos um gráfico PlotWidget num QWidget, que instanciará o QThread e traçará a curva. A curva será actualizada sempre que o sinal changeData for recebido, utilizando a função setData.
- função setData
@pyqtSlot(float,float)
def setData(self, t,d):
#append data
self.time.append(t)
self.data.append(d)
#remove first item
self.time.pop(0)
self.data.pop(0)
#update graph
self.graphWidget.clear()
self.graphWidget.plot(self.time, self.data)
- sinal changeData
self.th.changeData.connect(self.setData)
Código de visualização do sinal de hora completa
import cv2
import sys
#from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap
from pyqtgraph import PlotWidget, plot
from PySide6.QtWidgets import QMainWindow, QWidget, QLabel, QApplication, QVBoxLayout
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal
pyqtSlot = Slot
import numpy as np
import time
class Thread(QThread):
changeData = pyqtSignal(float,float)
def run(self):
self.isRunning=True
self.time = 0
self.data = 0
f = 1.
w = 2. * np.pi * f
while self.isRunning:
self.time+=0.01
self.data=2*np.sin(w*self.time)
self.changeData.emit(self.time,self.data)
time.sleep(0.01)
def stop(self):
self.isRunning=False
self.quit()
self.terminate()
class SignalContainer(QWidget):
def __init__(self):
super().__init__()
self.title = 'Signal'
self.time = [0]*100
self.data = [0]*100
self.initUI()
@pyqtSlot(float,float)
def setData(self, t,d):
#append data
self.time.append(t)
self.data.append(d)
#remove first item
self.time.pop(0)
self.data.pop(0)
#update graph
self.graphWidget.clear()
self.graphWidget.plot(self.time, self.data)
def initUI(self):
self.setWindowTitle(self.title)
#self.resize(1200, 800)
layout = QVBoxLayout()
self.setLayout(layout)
# create widget
self.graphWidget = PlotWidget()
layout.addWidget(self.graphWidget)
#plot data
#self.time = [1,2,3,4,5,6,7,8,9,10]
#self.data = [30,32,34,32,33,31,29,32,35,45]
self.graphWidget.plot(self.time, self.data)
self.th = Thread(self)
self.th.changeData.connect(self.setData)
self.th.start()
import signal #close signal with Ctrl+C
signal.signal(signal.SIGINT, signal.SIG_DFL)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = SignalContainer()
ex.show()
app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window
sys.exit(app.exec())
Graças ao PyQtGraph, podemos ver uma janela aparecer com o sinal a deslocar-se através da interface PyQt, tal como num osciloscópio.
Configurar o estilo do PlotWidget
Existem inúmeras opções para configurar o estilo do gráfico (cor, legenda, etiqueta, etc.).
- estilo da curva self.pen = mkPen()
- definir a cor de fundo self.graphWidget.setBackground
- adicionar um título self.graphWidget.setTitle
- adicionar etiquetas aos eixos self.graphWidget.setLabel
- mostrar a grelha self.graphWidget.showGrid
- adicionar uma legenda self.graphWidget.addLegend
#tune plots
self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine) #line style
self.graphWidget.setBackground((50,50,50,220)) # RGBA #background
self.graphWidget.setTitle("Signal(t)", color="w", size="20pt") #add title
styles = {'color':'r', 'font-size':'20px'} #add label style
self.graphWidget.setLabel('left', 'signal [SI]', **styles) #add ylabel
self.graphWidget.setLabel('bottom', 'time [s]', **styles) #add xlabel
self.graphWidget.showGrid(x=True, y=True) #add grid
self.graphWidget.addLegend() #add grid
self.graphWidget.setYRange(-2, 2, padding=0.1)
#plot data
self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
Bónus: Configurar o sinal a partir de QThread
É possível modificar os parâmetros do sinal gerido pela QThread diretamente a partir da interface. Neste exemplo, vamos modificar a frequência, a amplitude e a amostragem do sinal.
Para o efeito, vamos criar três campos e um botão que nos permitirão configurar o sinal
Nota: cada campo está ligado à função setParam pelo sinal returnPressed, que detecta a tecla “enter”.
#create param
self.amplbl = QLabel("Ampl")
self.amp=QLineEdit("2")
self.amp.returnPressed.connect(self.setParam)
self.freqlbl = QLabel("Freq")
self.freq=QLineEdit("1")
self.freq.returnPressed.connect(self.setParam)
self.samplbl = QLabel("Ts")
self.samp=QLineEdit("0.02")
self.samp.returnPressed.connect(self.setParam)
self.conf = QPushButton("Configure")
self.conf.clicked.connect(self.setParam)
Quando uma configuração é alterada, a função setParam é executada. A função envia o sinal changeParam com um dicionário como argumento
def setParam(self):
if self.amp.text()!='' and self.freq.text()!='' and self.samp.text()!='':
if float(self.samp.text())>0:
d={"amp":float(self.amp.text()),"freq":float(self.freq.text()),"samp":float(self.samp.text())}
self.changeParam.emit(d)
O sinal changeParam liga-se à função setParam do QThread() na definição de SignalContainer
self.th.changeData.connect(self.setData) #reception self.changeParam.connect(self.th.setParam) #emission
No objeto QThread, adicionamos uma função setParam que actualiza os parâmetros do sinal
@pyqtSlot(dict)
def setParam(self,param):
self.amp=param["amp"]
self.freq=param["freq"]
self.samp=max(0.0001,param["samp"])
Podemos então modificar o sinal a partir da interface PyQt e exibi-lo usando PyQtGraph
Código completo
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import sys
#from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap
from pyqtgraph import PlotWidget, mkPen
from PySide6.QtWidgets import QMainWindow, QWidget, QLabel, QApplication, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal
pyqtSlot = Slot
import numpy as np
import time
class Thread(QThread):
changeData = pyqtSignal(float,float)
def __init__(self,a):
super(Thread,self).__init__()
self.amp=2
self.freq=1
self.samp=0.02
self.time = 0
self.data = 0
def run(self):
self.isRunning=True
while self.isRunning:
self.time+=self.samp
self.data=self.amp*np.sin(2. * np.pi * self.freq *self.time)
self.changeData.emit(self.time,self.data)
time.sleep(0.1)
def stop(self):
self.isRunning=False
self.quit()
self.terminate()
@pyqtSlot(dict)
def setParam(self,param):
self.amp=param["amp"]
self.freq=param["freq"]
self.samp=max(0.0001,param["samp"])
class SignalContainer(QWidget):
changeParam = pyqtSignal(dict)
def __init__(self):
super().__init__()
self.title = 'Signal'
self.span=10
self.time = [0]*1000
self.data = [0]*1000
self.initUI()
@pyqtSlot(float,float)
def setData(self, t,d):
#append data
self.time.append(t)
self.data.append(d)
#remove first item
self.time.pop(0)
self.data.pop(0)
#update graph
self.graphWidget.clear()
self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
if self.time[-1]>self.span:
self.graphWidget.setXRange(self.time[-1]-self.span, self.time[-1], padding=0)
self.graphWidget.setYRange(min(-2,min(self.data)), max(2,max(self.data)), padding=0.1)
def initUI(self):
self.setWindowTitle(self.title)
self.resize(800, 400)
layout = QVBoxLayout()
self.setLayout(layout)
#create param
self.amplbl = QLabel("Ampl")
self.amp=QLineEdit("2")
self.amp.returnPressed.connect(self.setParam)
self.freqlbl = QLabel("Freq")
self.freq=QLineEdit("1")
self.freq.returnPressed.connect(self.setParam)
self.samplbl = QLabel("Ts")
self.samp=QLineEdit("0.02")
self.samp.returnPressed.connect(self.setParam)
self.conf = QPushButton("Configure")
self.conf.clicked.connect(self.setParam)
hlayo = QHBoxLayout()
hlayo.addWidget(self.amplbl)
hlayo.addWidget(self.amp)
hlayo.addWidget(self.freqlbl)
hlayo.addWidget(self.freq)
hlayo.addWidget(self.samplbl)
hlayo.addWidget(self.samp)
hlayo.addWidget(self.conf)
layout.addLayout(hlayo)
# create widget
self.graphWidget = PlotWidget()
layout.addWidget(self.graphWidget)
#tune plots
self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine) #line style
self.graphWidget.setBackground((50,50,50,220)) # RGBA #background
self.graphWidget.setTitle("Signal(t)", color="w", size="20pt") #add title
styles = {'color':'r', 'font-size':'20px'} #add label style
self.graphWidget.setLabel('left', 'signal [SI]', **styles) #add ylabel
self.graphWidget.setLabel('bottom', 'time [s]', **styles) #add xlabel
self.graphWidget.showGrid(x=True, y=True) #add grid
self.graphWidget.addLegend() #add grid
self.graphWidget.setXRange(0, self.span, padding=0)
self.graphWidget.setYRange(-2, 2, padding=0.1)
#plot data
self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
#manage thread
self.th = Thread(self)
self.amp.setText(str(self.th.amp))
self.freq.setText(str(self.th.freq))
self.samp.setText(str(self.th.samp))
self.th.changeData.connect(self.setData) #reception
self.changeParam.connect(self.th.setParam) #emission
self.th.start()
def setParam(self):
if self.amp.text()!='' and self.freq.text()!='' and self.samp.text()!='':
if float(self.samp.text())>0:
d={"amp":float(self.amp.text()),"freq":float(self.freq.text()),"samp":float(self.samp.text())}
self.changeParam.emit(d)
import signal #close signal with Ctrl+C
signal.signal(signal.SIGINT, signal.SIG_DFL)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = SignalContainer()
ex.show()
app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window
sys.exit(app.exec())
