Nous allons voir dans ce tutoriel comment observer un signal temporel sous forme de graphique avec PyQt grâce à PyQtGraph. Si vous créer des interfaces graphiques, il peut être intéressant plutôt que d’afficher des nombres qui défilent de les afficher sous forme de courbes comme sur un oscilloscope.
Installation
- PyQt (ou PySide)
pip install PyQt5
ou
pip install pyside6
- PyQtGraph
pip install pyqtgraph
Code pour afficher une simple courbe avec PyQtGraph
Pour afficher une courbe avec PyQtGraph, il suffit de rajouter un objet PlotWidget dans un objet PyQT QWidget.
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())

Code pour créer un signal temporel
Pour faire vivre cette courbe, nous allons créer un objet QThread pour ne pas bloquer l’application qui va nous permettre de créer un signal sinusoïdale qui va évoluer dans le temps. A chaque itération, nous allons envoyer une mise à jour à l’aide du signal 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()
Code de l’application PyQt
Pour afficher la courbe dans une application, nous allons créer un graphique PlotWidget dans un QWidget qui va instancier le QThread et tracer la courbe. La courbe sera mise à jour à chaque réception du signal changeData grâce à la fonction setData
- fonction 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)
- signal changeData
self.th.changeData.connect(self.setData)
Code complet d’affichage d’un signal temporel
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())
Grâce à PyQtGraph, nous pouvons voir une fenêtre s’afficher avec le signal défiler dans l’interface PyQt comme sur un oscilloscope.

Configuration du style de PlotWidget
Il y a de nombreuse option pour configurer le style du graphique (couleur, legend, label, etc.)
- style de la courbe self.pen = mkPen()
- définir la couleur du fond self.graphWidget.setBackground
- ajouter un titre self.graphWidget.setTitle
- ajouter les labels sur les axes self.graphWidget.setLabel
- afficher la grille self.graphWidget.showGrid
- ajouter une légende 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')

Bonus: Configurer le signal provenant de QThread
Il est possible de modifier les paramètres du signal géré par le QThread directement depuis l’interface. Dans cet exemple, nous allons modifier la fréquence, l’amplitude et l’échantillonnage du signal.
Pour cela nous allons créer trois champs et un bouton qui vont nous permettre de configurer le signal
N.B.: notez que chaque champs est connecté à la fonction setParam par le signal returnPressed qui détecte la touche « entrée »
#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)
Lors d’un changement de configuration, la fonction setParam est exécutée. La fonction émet le signal changeParam avec un dictionnaire en argument
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)
Le signal changeParam se connecte à la fonction setParam du QThread() dans la définiton de SignalContainer
self.th.changeData.connect(self.setData) #reception self.changeParam.connect(self.th.setParam) #emission
Du côté de l’objet QThread, nous ajoutons une fonction setParam qui va mettre à jour les paramètres du signal
@pyqtSlot(dict)
def setParam(self,param):
self.amp=param["amp"]
self.freq=param["freq"]
self.samp=max(0.0001,param["samp"])
Nous pouvons ainsi modifier le signal depuis l’interface PyQt et l’afficher avec PyQtGraph

Code complet
#!/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())