Site icon AranaCorp

Créer une liste d’objets défilante avec PyQt

Nous allons voir comment développer un objet graphique permettant d’afficher une liste d’objets défilante et sélectionnable. Grâce à cet objet vous pourrez créer des interfaces graphiques modulables.

Création d’un objet QScrollArea

Pour créer une liste d’objets défilante, nous allons utiliser l’objet QScrollArea qui permet comme son nom l’indique de créer une zone avec des barres de défilement.

Nous créons donc un objet qui hérite de QScrollArea et qui intègre une liste de boîtes à cocher (QCheckBox). Nous utilisons un layout vertical pour agencer la liste d’objet.

class ListContainer(QScrollArea):
	changeItem=pyqtSignal(list)
	def __init__(self,items=None, parent=None):
		super(ListContainer, self).__init__(parent)
		self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
		self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
		self.setWidgetResizable(True)

		self.listItem=items
		if self.listItem==None:
			self.listItem=[0]*20
		self.listState=[False]*len(self.listItem)
		self.itemChk=[]
		self.initUI()
		
	def initUI(self):
		container=QWidget()
		self.setWidget(container)
		layout = QVBoxLayout(container)
		
		for i,s in enumerate(self.listItem):
			self.itemChk.append(QCheckBox("Objet"+str(i)))
			self.itemChk[i].setChecked(False)
			layout.addWidget(self.itemChk[i])

N.B.: Nous utilisons dans cet exemple des objets QCheckBox mais vous pouvez utiliser n’importe quel QWidget.

Pour récupérer l’état des CheckBox, nous ajoutons une fonction changeChk que nous connectons au signal stateChanged de chaque CheckBox

Nous pouvons ensuite ajouter ce code directement dans une application Qt. Voici le code pour tester votre objet ListContainer

#from PyQt6.QtWidgets import (QWidget, QSlider, QLineEdit, QLabel, QPushButton, QScrollArea,QApplication,
#                             QHBoxLayout, QVBoxLayout, QMainWindow)
#from PyQt6.QtCore import Qt, QSize
#from PyQt6 import QtWidgets, uic

from PySide6.QtWidgets import (QWidget, QSlider, QLineEdit, QLabel, QPushButton, QScrollArea,QApplication,
                             QHBoxLayout, QVBoxLayout, QMainWindow, QFrame, QCheckBox)
from PySide6.QtCore import Qt, QSize, Signal, Slot
pyqtSignal = Signal
pyqtSlot = Slot

import sys


class ListContainer(QScrollArea):
	changeItem=pyqtSignal(list)
	def __init__(self,items=None, parent=None):
		super(ListContainer, self).__init__(parent)
		self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
		self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
		self.setWidgetResizable(True)
		self.listItem=items
		if self.listItem==None:
			self.listItem=[0]*20
		self.listState=[False]*len(self.listItem)
		self.itemChk=[]
		self.initUI()
		
	def initUI(self):
		container=QWidget()
		self.setWidget(container)
		layout = QVBoxLayout(container)
		
		for i,s in enumerate(self.listItem):
			self.itemChk.append(QCheckBox("Objet"+str(i)))
			self.itemChk[i].setChecked(False)
			self.itemChk[i].stateChanged.connect(self.changeChk)
			layout.addWidget(self.itemChk[i])
	
	def changeChk(self,state):
		print("{} : {}".format(self.sender().text(),True if state>0 else False))
		for i,s in enumerate(self.itemChk):
			if s.text() == self.sender().text():
				self.listState[i]=True if state>0 else False
		self.changeItem.emit(self.listState)
				

def main():
    app = QApplication(sys.argv)
    main = ListContainer()
    main.show()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()

Exemple d’utilisation de la liste d’objets défilante

A titre d’exemple, nous allons reprendre le projet Afficher un signal avec PyQtGraph, dans lequel nous allons intégrer l’objet ListContainer. La liste d’objets QCheckBox va nous permettre de sélectionner les signaux à afficher sur le graphique.

Dans l’objet SignalContainer, nous créons différents signaux temporels et style aléatoirement à l’aide du paquet numpy.random.

		self.time = np.linspace(0,1,1000)*10
		for i,d in enumerate(self.data):
			x = np.random.choice([-1, 1]) #random sign
			s = np.random.rand()*0.2 #random scale
			fun=np.random.choice([np.sin, np.cos]) #random function
			self.data[i]=x*fun(self.time) + np.random.normal(scale=s, size=len(self.time))
			r=np.random.randint(255) #random color
			g=np.random.randint(255)
			b=np.random.randint(255)
			style = np.random.choice([Qt.DashLine, Qt.SolidLine, Qt.DotLine, Qt.DashDotLine])
			symbol = np.random.choice(['','+','o','x'])
			
			self.pen.append(mkPen(color=(r, g, b), width=3, style=Qt.DashLine))	#line style

Nous créons ensuite une fonction setSignal qui prend en argument la liste des états et met à jour le graphique en fonction des signaux à afficher.

	@pyqtSlot(list)			
	def setSignal(self,states):
		print(states)
		self.sigstate=states
		
		#update graph
		self.graphWidget.clear()
		#self.graphWidget.plot(self.time, self.data, name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
		for i,data in enumerate(self.data):
			if self.sigstate[i]: #display signal
				self.graphWidget.plot(self.time, self.data[i],name = "signal"+str(i),pen=self.pen[i])

Nous connectons la fonction setSignal au signal changeItem de l’objet ListContainer afin que la fonction soit appelée à chaque mise à jour des états des CHeckBoxs.

		self.select.changeItem.connect(self.setSignal)

Voici le code complet d’une application PyQt avec une liste d’objets défilante permettant de sélectionner les signaux à afficher sur PyQTGraph

#!/usr/bin/env python
# -*- coding: utf-8 -*-

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, QCheckBox, QScrollArea, QSplitter
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal #convert pyqt to pyside
pyqtSlot = Slot

import numpy as np
import time

		
class ListContainer(QScrollArea):
	changeItem=pyqtSignal(list)
	def __init__(self,items=None, parent=None):
		super(ListContainer, self).__init__(parent)
		self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
		self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
		self.setWidgetResizable(True)
		self.listItem=items
		self.listState=[False]*len(self.listItem)
		
		if self.listItem==None:
			self.listItem=[0]*50
		self.itemChk=[]
		self.initUI()
		
	def initUI(self):
		container=QWidget()
		self.setWidget(container)
		layout = QVBoxLayout(container)
		
		for i,s in enumerate(self.listItem):
			self.itemChk.append(QCheckBox("Signal"+str(i)))
			self.itemChk[i].setChecked(False)
			self.itemChk[i].stateChanged.connect(self.changeChk)
			layout.addWidget(self.itemChk[i])
	
	def changeChk(self,state):
		print("{} : {}".format(self.sender().text(),True if state>0 else False))
		for i,s in enumerate(self.itemChk):
			if s.text() == self.sender().text():
				self.listState[i]=True if state>0 else False
		self.changeItem.emit(self.listState)
				
		

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]*20#[[0]*1000,[0]*1000,[0]*1000]
		self.pen=[]
		
		self.time = np.linspace(0,1, 1000)*10
		for i,d in enumerate(self.data):
			x = np.random.choice([-1, 1]) #random sign
			s = np.random.rand()*0.2 #random scale
			fun=np.random.choice([np.sin, np.cos]) #random function
			self.data[i]=x*fun(self.time) + np.random.normal(scale=s, size=len(self.time))
			r=np.random.randint(255) #random color
			g=np.random.randint(255)
			b=np.random.randint(255)
			style = np.random.choice([Qt.DashLine, Qt.SolidLine, Qt.DotLine, Qt.DashDotLine])
			symbol = np.random.choice(['','+','o','x'])
			
			self.pen.append(mkPen(color=(r, g, b), width=3, style=Qt.DashLine))	#line style

		self.sigstate=[False,False,False]
		self.initUI()
		
	def initUI(self):
		self.setWindowTitle(self.title)
		self.resize(800, 400)
		self.mainLayout = QHBoxLayout()
		self.setLayout(self.mainLayout)
		self.splitter=QSplitter()
		self.mainLayout.addWidget(self.splitter)
		
		self.select=ListContainer(["signal{}".format(i) for i in range(len(self.data))])
		self.select.changeItem.connect(self.setSignal)
		self.splitter.addWidget(self.select)
		
		self.signalLayout=QVBoxLayout()
		
		# create widget
		self.graphWidget = PlotWidget()
		self.signalLayout.addWidget(self.graphWidget)
		#self.mainLayout.addLayout(self.signalLayout)
		self.splitter.addWidget(self.graphWidget)
        
		#tune plots
		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[0],name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
		
	@pyqtSlot(list)			
	def setSignal(self,states):
		print(states)
		self.sigstate=states
		
		#update graph
		self.graphWidget.clear()
		#self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
		for i,data in enumerate(self.data):
			if self.sigstate[i]: #display signal
				self.graphWidget.plot(self.time, self.data[i],name = "signal"+str(i),pen=self.pen[i])
		
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()
		
		sys.exit(app.exec())

Sources

Quitter la version mobile