fbpixel
Exibindo um sinal no PyQt com PyQtGraph

Exibindo um sinal no PyQt com PyQtGraph

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())
		
		

Fontes

Exibindo uma imagem OpenCV em uma interface PyQt

Exibindo uma imagem OpenCV em uma interface PyQt

Para certas aplicações, pode ser útil incorporar o OpenCV numa interface PyQt. Neste tutorial, veremos como integrar e gerenciar corretamente um vídeo capturado pelo OpenCV em um aplicativo PyQt.

N.B.: Utilizamos o Pyside, mas a conversão para PyQt é bastante simples.

Pré-requisitos:

  • Instalar o Python
  • Instalação do OpenCV (pip install opencv-python)
  • PySide ou PyQt (pip install pyside6 ou pip install PyQt5)

Código para capturar um vídeo com OpenCV

Eis o código básico para apresentar um vídeo de webcam com o openCV

import sys
import cv2

def main(args):

	cap = cv2.VideoCapture(0) #default camera

	while(True):
		ret, frame = cap.read()
		if ret:
			frame=cv2.resize(frame, (800, 600)) 
			cv2.imshow("Video",frame)
			
		if cv2.waitKey(1) & 0xFF == ord('q'): #click q to stop capturing
			break

	cap.release()
	cv2.destroyAllWindows()
	return 0

if __name__ == '__main__':
    
    sys.exit(main(sys.argv))

Para o integrar numa aplicação PyQt, vamos criar um objeto QThread que será responsável por reproduzir o vídeo sem bloquear a aplicação.

  • A função run é a função que contém o código openCV que será executado num ciclo quando a função QThread.start for chamada.
  • A função de paragem é utilizada para parar a linha de forma limpa
  • O sinal changePixmap é utilizado para informar a aplicação de que está disponível uma nova imagem.
class Thread(QThread):
	changePixmap = pyqtSignal(QImage)

	def run(self):
		self.isRunning=True
		cap = cv2.VideoCapture(0)
		while self.isRunning:
			ret, frame = cap.read()
			if ret:
				rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
				h, w, ch = rgbImage.shape
				bytesPerLine = ch * w
				convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888)
				p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio)
				self.changePixmap.emit(p)
				
	def stop(self):
		self.isRunning=False
		self.quit()
		self.terminate()

Criar a aplicação PyQt

Para a aplicação, criaremos um QLabel num QWidget simples que conterá a imagem de vídeo e instanciaremos o QThread. O vídeo será atualizado automaticamente utilizando a função setImage, que é chamada quando o sinal changePixmap é recebido.

  • função setImage
	@pyqtSlot(QImage)
	def setImage(self, image):
		#update image	
		self.label.setPixmap(QPixmap.fromImage(image))
  • sinal changePixmap
		self.th.changePixmap.connect(self.setImage)
class VideoContainer(QWidget):
	def __init__(self):
		super().__init__()
		self.title = 'PySide Video'
		self.left = 100
		self.top = 100
		self.fwidth = 640
		self.fheight = 480
		self.initUI()

	@pyqtSlot(QImage)
	def setImage(self, image):
		#update image	
		self.label.setPixmap(QPixmap.fromImage(image)) 
	
	def initUI(self):
		self.setWindowTitle(self.title)
		self.setGeometry(self.left, self.top, self.fwidth, self.fheight)
		self.resize(1200, 800)
		
		# create a label
		self.label = QLabel(self)		
		self.label.resize(640, 480)
		self.th = Thread(self)
		self.th.changePixmap.connect(self.setImage)
		self.th.start()
		self.show()

Código completo para exibir um vídeo em uma janela do PyQt

import cv2
import sys
#from PyQt5.QtWidgets import  QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap

from PySide6.QtWidgets import  QWidget, QLabel, QApplication
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal
pyqtSlot = Slot

class Thread(QThread):
	changePixmap = pyqtSignal(QImage)

	def run(self):
		self.isRunning=True
		cap = cv2.VideoCapture(0)
		while self.isRunning:
			ret, frame = cap.read()
			if ret:
				# https://stackoverflow.com/a/55468544/6622587
				rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
				h, w, ch = rgbImage.shape
				bytesPerLine = ch * w
				convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888)
				p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio)
				self.changePixmap.emit(p)
				
	def stop(self):
		self.isRunning=False
		self.quit()
		self.terminate()

class VideoContainer(QWidget):
	def __init__(self):
		super().__init__()
		self.title = 'Video'
		self.left = 100
		self.top = 100
		self.fwidth = 640
		self.fheight = 480
		self.initUI()

	@pyqtSlot(QImage)
	def setImage(self, image):
		#update image	
		self.label.setPixmap(QPixmap.fromImage(image)) 
	
	def initUI(self):
		self.setWindowTitle(self.title)
		self.setGeometry(self.left, self.top, self.fwidth, self.fheight)
		self.resize(1200, 800)
		
		# create a label
		self.label = QLabel(self)		
		self.label.resize(640, 480)
		self.th = Thread(self)
		self.th.changePixmap.connect(self.setImage)
		self.th.start()
		self.show()

if __name__ == '__main__':
	
		app = QApplication(sys.argv)
		ex = VideoContainer()
		sys.exit(app.exec())

Aparece uma janela “Vídeo” com a imagem da Webcam.

Bónus: interface de fecho melhorada

O código funciona bem e pode ser suficiente, mas existem alguns problemas com esta implementação:

  • A aplicação não pode ser fechada com Ctrl+C (Interrupção do teclado)
  • Quando se fecha a janela, a Qthread não pára
  • Se redimensionar a janela, o tamanho do vídeo não se altera

Para fechar a aplicação com Ctrl+C, pode utilizar o sinal de interrupção adicionando o seguinte código antes de chamar a aplicação (existem métodos mais limpos)

import signal #close signal with Ctrl+C
signal.signal(signal.SIGINT, signal.SIG_DFL)

Para terminar o QThread quando a janela é fechada, pode utilizar o sinal aboutToQuit da aplicação para chamar a função stop do QThread

app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window

Por fim, para redimensionar o vídeo com a janela sempre que esta é actualizada, utilizamos o tamanho da janela para calcular o tamanho da imagem e a posição da etiqueta, de modo a que esta fique centrada e o vídeo mantenha as suas proporções.

	@pyqtSlot(QImage)
	def setImage(self, image):
		#resize image with window and center
		imWidth=self.width()-2*self.padding
		imHeight=self.height()-2*self.padding
		image = image.scaled(imWidth, imHeight, Qt.KeepAspectRatio) # remove Qt.KeepAspectRatio if not needed
		self.label.resize(image.width(), image.height()) #(640, 480)
		self.label.move((self.width()-image.width())/2, (self.height()-image.height())/2)
			
		#update image	
		self.label.setPixmap(QPixmap.fromImage(image)) 

Aqui está o código completo com melhorias

import cv2
import sys
#from PyQt5.QtWidgets import  QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap

from PySide6.QtWidgets import  QWidget, QLabel, QApplication
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal
pyqtSlot = Slot

class Thread(QThread):
	changePixmap = pyqtSignal(QImage)

	def run(self):
		self.isRunning=True
		cap = cv2.VideoCapture(0)
		while self.isRunning:
			ret, frame = cap.read()
			if ret:
				# https://stackoverflow.com/a/55468544/6622587
				rgbImage = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
				h, w, ch = rgbImage.shape
				bytesPerLine = ch * w
				convertToQtFormat = QImage(rgbImage.data, w, h, bytesPerLine, QImage.Format_RGB888)
				p = convertToQtFormat.scaled(640, 480, Qt.KeepAspectRatio)
				self.changePixmap.emit(p)
				
	def stop(self):
		self.isRunning=False
		self.quit()
		self.terminate()

class VideoContainer(QWidget):
	def __init__(self):
		super().__init__()
		self.title = 'PySide Video'
		self.left = 100
		self.top = 100
		self.fwidth = 640
		self.fheight = 480
		self.padding = 10
		self.initUI()

	@pyqtSlot(QImage)
	def setImage(self, image):
		#resize image with window and center
		imWidth=self.width()-2*self.padding
		imHeight=self.height()-2*self.padding
		image = image.scaled(imWidth, imHeight, Qt.KeepAspectRatio) # remove Qt.KeepAspectRatio if not needed
		self.label.resize(image.width(), image.height()) #(640, 480)
		self.label.move((self.width()-image.width())/2, (self.height()-image.height())/2)
			
		#update image	
		self.label.setPixmap(QPixmap.fromImage(image)) 
		
	def initUI(self):
		self.setWindowTitle(self.title)
		self.setGeometry(self.left, self.top, self.fwidth, self.fheight)
		self.resize(1200, 800)
		
		# create a label
		self.label = QLabel(self)		
		self.label.resize(self.width()-2*self.padding,self.height()-2*self.padding) #(640, 480)
		self.th = Thread(self)
		self.th.changePixmap.connect(self.setImage)
		self.th.start()
		self.show()

import signal #close signal with Ctrl+C
signal.signal(signal.SIGINT, signal.SIG_DFL)

if __name__ == '__main__':
	
		app = QApplication(sys.argv)
		ex = VideoContainer()
		app.aboutToQuit.connect(ex.th.stop) #stop qthread when closing window
		
		sys.exit(app.exec())

Fontes

Criar um executável EXE a partir de um script Python

Criar um executável EXE a partir de um script Python

Pode converter um script Python num ficheiro executável (EXE) utilizando a biblioteca PyInstaller. Depois de o seu código Python ter sido testado e validado, pode partilhá-lo como uma aplicação de ambiente de trabalho executável a partir de qualquer computador. Isto é especialmente útil para interfaces gráficas de utilizador (GUIs), como este monitor de série desenvolvido em Python

Estrutura do projeto

Para começar, temos de estruturar corretamente o nosso projeto. Para o fazer, utilizamos a seguinte estrutura em árvore

app/
│
├── resources
|   ├── logo.ico
|   ├── logo.png
|   ├── style.txt
├── app.py
├── LICENSE
├── README.md
├── requirements.txt
├── setup.py
└── tests.py
  •  app.py contém o código principal
  •  logo.ico será o ficheiro de ícone utilizado após a compilação
  • logo.png é utilizado no software
  •  style.txt contém a definição do estilo CSS

Neste tutorial, vamos criar uma interface simples usando PySide2 (você também pode usar PyQt) contendo um botão que modificará as mensagens na barra de depuração.

#!/usr/bin/python
# -*-coding:Utf-8 -*
"""
Created on Thu Nov 17 16:59:13 2022

@author: X.Wiedmer

Dummy app
Define a simple app to test PyInstaller
"""
import sys,os
#from PyQt5.QtWidgets import *
#from PyQt5.QtCore import *
#from PyQt5.QtGui import *
from PySide2.QtWidgets import *
from PySide2.QtCore import *
from PySide2.QtGui import *
pyqtSignal=Signal #translate pyqt to Pyside



# Generic app container     
def resource_path(relative_path):
	""" Get absolute path to resource, works for dev and for PyInstaller """
	try:
		# PyInstaller creates a temp folder and stores path in _MEIPASS
		base_path = sys._MEIPASS
	except Exception:
		base_path = os.path.abspath(".")

	return os.path.join(base_path, relative_path)


"""
App configuration
"""
__title__="DummyApp"
__version__="v0.1"	
style_path = resource_path("resources\\style.txt")
__icon__=resource_path("resources/logo.png")

class AcApp(QMainWindow):

	def __init__(self,title='DummyApp',mainFrame=QFrame):
		super().__init__()
		self.title=title
		self.mainFrame=mainFrame()
		self.initUI()

	def initUI(self):        
		#mainframe
		self.setCentralWidget(self.mainFrame)
		centralLayout=QHBoxLayout(self.mainFrame)
		
		#widgets
		self.button = QPushButton("Start")
		self.button.clicked.connect(self.clicked)
		centralLayout.addWidget(self.button)
	   
		#General configuration
		self.setWindowTitle(self.title)
		self.setGeometry(300, 300, 400, 200)
		self.setWindowIcon(QIcon(__icon__))
	   
		#Debug bar
		self.statusBar()
		self.statusBar().showMessage('Display debug messages')
	   
		self.show()

	def debugMsg(self,val):
		self.statusBar().showMessage(val)
	
	def clicked(self):
		if self.button.text() == "Start":
			self.debugMsg("Process started")
			self.button.setText("Stop") 
		else:
			self.debugMsg("Process stopped")
			self.button.setText("Start")    
  
def main():
	app = QApplication(sys.argv)
	app.setQuitOnLastWindowClosed(True)
	app.setStyleSheet(open(style_path).read()); #set style according to file 
	ex = AcApp(__title__+__version__)
	app.quit()
	sys.exit(app.exec_())
   
if __name__ == '__main__':
	main()

N.B.: Se quiser que ficheiros como o ícone sejam incluídos no .EXE, terá de especificar o caminho absoluto no script Python.

Para gerar caminhos absolutos automaticamente, pode utilizar a seguinte função

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)
	
style_path = resource_path("resources\\ac_style.txt")

Instalando o pacote PyInstaller

Para compilar o projeto, utilizamos a biblioteca PyInstaller, que cria um ficheiro executável contendo todas as dependências necessárias para executar o código.

Antes de instalar o pyinstaller, verifique se C:\Users\ADMIN\AppData\Local\Programs\Python\Python38\Scripts está adicionado ao Path das variáveis de ambiente.

python -m pip install pyinstaller==5.6.2

N.B.: a versão 5.7.0 foi lançada, mas apresenta um erro de permissão

Configurar o ficheiro install_bat

Criamos um ficheiro install_app.bat que irá automatizar o processo de compilação. Nesse arquivo, especificamos o tipo de compilação (onefile, noconsole), as bibliotecas (PySide2) e as pastas a serem incluídas no executável (resources).

python -m PyInstaller --noconfirm --log-level=WARN ^
    --onefile --noconsole ^
    --hidden-import=PySide2 ^
    --hidden-import=shiboken2 ^
    --add-data  ./resources;resources ^
    --icon=./resources/logo_araignee.ico ^
    app.py

O PyInstaller irá criar as pastas build, dist e um ficheiro app.spec

O ficheiro app.spec contém as especificações de compilação e a pasta que contém o ficheiro EXE.

Pode agora partilhar este ficheiro executável com qualquer pessoa (que utilize o mesmo sistema operativo) sem qualquer instalação especial da parte dela.

Limitações

O PyInstaller não permite que você compile para diferentes sistemas operacionais a partir da mesma máquina. Para criar uma aplicação Windows, é necessário usar o PyInstaller numa máquina Windows. Será necessário fazer o mesmo para Linux ou MacOS.

O PyInstaller não contém todas as dependências. De facto, algumas dependências estão contidas no SO. Portanto, é possível que algumas pessoas não consigam executar o seu software.

É possível fazer a compilação cruzada para diferentes sistemas operativos utilizando um docker.

Fontes

Melhores práticas para criar um projeto Python

Melhores práticas para criar um projeto Python

Neste tutorial, veremos as melhores práticas para organizar um projeto Python para que possa ser partilhado e implementado.

Quer esteja a trabalhar num software, numa biblioteca ou numa interface gráfica, como uma aplicação Web ou uma aplicação móvel, se quiser partilhar o seu trabalho, tem de o organizar e documentar corretamente.

É possível aplicar estas práticas ao projeto Serial Monitor

Criar um ambiente virtual

Quando se pretende criar um projeto que se vai partilhar e manter a longo prazo, é essencial integrá-lo num ambiente virtual para isolar as suas dependências (bibliotecas utilizadas, versões, etc.).

No Windows, certifique-se de que a versão do python que está a utilizar pode ser chamada com python.exe

  • python -m pip install python3-venv –> pour installer
  • python -m venv venv –> créer un environnement virtuel
  • source venv/bin/activate –> activer l’environnement
  • deactivate –> désactiver l’environnement

N.B.: No Windows, para abrir a consola onde quiser, pode criar um ficheiro start_console.bat para o qual copia o comando

iniciar cmd.exe

Pode executar o ficheiro fazendo duplo clique para abrir o terminal

Depois de o ambiente ter sido criado, pode adicionar ou remover pacotes

pip install <package_name>
pip install <package_name>==<version>
pip uninstall <package_name> 

E mantenha o registo dos pacotes e versões utilizados no ficheiro requirements.txt

pip freeze > requirements.txt

Arquitetura de ficheiros de um projeto Python

Com base nas recomendações de Kenneth Reitz, eis a estrutura de ficheiros a adotar.

  • Se o seu projeto for constituído por apenas um ficheiro
helloworld/
│
├── .gitignore
├── helloworld.py
├── LICENSE
├── README.md
├── requirements.txt
├── setup.py
└── tests.py
  • Para projectos mais complexos que contenham diferentes módulos
sample
│
├── docs
│   ├── conf.py
│   ├── index.rst
├── sample
│   ├── __init__.py
│   ├── core.py
│   ├── helpers.py
├── tests
│   ├── test_basic.py
│   ├── test_advanced.py
├── LICENSE
├── README.md
├── requirements.txt
├── setup.py

Nota: Para visualizar a árvore de pastas, pode utilizar o comando de árvore

Na prática, tem toda a liberdade para criar a arquitetura das pastas. O importante é que seja funcional, modular e organizada.

Segue-se uma descrição dos ficheiros de arquitetura essenciais

  • sample.py ou sample: contém o código-fonte com um ou mais módulos
  • README: descrição do projeto, guia de instalação, fonte e exemplo. Possíveis desenvolvimentos
  • LICENÇA: contém a licença e os direitos de autor ao abrigo dos quais o código é publicado
  • requirements.txt: contém todas as dependências e versões exigidas pelo script
  • setup.py : gere a distribuição e a construção do projeto Python
  • docs: contém a documentação do projeto
  • tests: contém os guiões de teste do projeto

Uma vez criada esta arquitetura, pode utilizá-la e adaptá-la a todos os seus projectos Python.

Fontes

Programar um Raspberry Pi Pico com MicroPython

Programar um Raspberry Pi Pico com MicroPython

O Raspberry Pi Pico é uma placa de desenvolvimento baseada no RP2040 programável com MicroPython. Tem um grande número de entradas

Hardware

  • Computador
  • Raspberry Pi Pico (standar H ou W)
  • Cabo USB A macho para USB Mini B macho

Instalar os controladores no computador

O Raspberry Pi Pico é reconhecido como um dispositivo de armazenamento USB Fat32. Por isso, não é necessário instalar controladores especiais para o ligar a um computador. Para converter o RPi Pico numa unidade flash USB:

  • Desligue o Pico da sua fonte de alimentação
  • Premir o botão BOOTSEL
  • Ligue o Pico ao computador utilizando o cabo USB
  • Soltar o botão BOOTSEL

Instalando o MicroPython no Pico RPI

Descarregue o ficheiro MicroPython UTF2 correspondente à versão do seu Pico

Em seguida, copie o ficheiro para a janela do Raspberry Pi Pico.

O Pico deve reiniciar e iniciar o MicroPython

Verificar o funcionamento com Putty

Uma vez que o MicroPython tenha sido instalado no seu Pico, pode executar comandos Python através de comunicação série. Para o fazer, pode usar o Putty

Procure o nome da porta utilizada pelo Raspberry Pi no Gestor de Dispositivos.

No software Putty, seleccione Comunicação em série, introduza o nome da porta e a taxa de transmissão é de 9600. Pode introduzir comandos Python no terminal.

Executar um script Python com o uPyCraft

Uma vez que o MicroPython tenha sido instalado, é possível executar um script Python usando o uPyCraft.

Descarregar e instalar o uPyCraft

Em Ferramentas, seleccione a porta série correcta e o tipo de cartão “outro”

Pode então criar um script main.py

Exemplo de código

from machine import Pin
from time import sleep

led = Pin(25, Pin.OUT)
while True:
   print("LED ON")
   led.value(1)
   sleep(1)
   print("LED OFF")
   led.value(0)
   sleep(1)

Se a transferência funcionar, deve ser apresentada a seguinte mensagem

Ready to download this file,please wait!
..
download ok
exec(open('main.py').read(),globals())

Resultados

Quando o código tiver sido carregado, deverá ver o texto a deslocar-se na consola. Pode parar o código utilizando o botão “stop” ou Ctrl+c

Executar um script utilizando o Thonny

Outro IDE bem conhecido para o desenvolvimento em MicroPython é o Thonny.

Descarregar e instalar a versão mais recente do Thonny

Em “Run> Select interpreter”, seleccione “MicroPython (Raspberry Pi Pico)” e, em seguida, seleccione a porta série.

Pode então copiar o código anterior e executá-lo com o comando “Executar”

Deverá ver o LED a piscar e o texto a deslocar-se.

Há uma série de ferramentas disponíveis para desenvolver o teu código Micropython no Raspberry Pi Pico ou similar. Utilize a que melhor se adequar a si.

Fontes