python - ¿Cómo señalar las ranuras en una GUI de un proceso diferente?
pyqt multiprocessing (5)
Este es un ejemplo de aplicación Qt que muestra el envío de señales desde un proceso secundario a ranuras en el proceso madre. No estoy seguro de que este sea el enfoque correcto pero funciona.
Diferencio entre proceso como madre e hijo , porque la palabra padre ya se usa en el contexto Qt.
El proceso madre tiene dos hilos. El hilo principal del proceso madre envía los datos al proceso hijo a través de multiprocessing.Queue
. El proceso hijo envía los datos procesados y la firma de la señal que se enviará al segundo hilo del proceso madre mediante multiprocessing.Pipe
. El segundo hilo del proceso madre en realidad emite la señal.
Python 2.X, PyQt4:
from multiprocessing import Process, Queue, Pipe
from threading import Thread
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Emitter(QObject, Thread):
def __init__(self, transport, parent=None):
QObject.__init__(self,parent)
Thread.__init__(self)
self.transport = transport
def _emit(self, signature, args=None):
if args:
self.emit(SIGNAL(signature), args)
else:
self.emit(SIGNAL(signature))
def run(self):
while True:
try:
signature = self.transport.recv()
except EOFError:
break
else:
self._emit(*signature)
class Form(QDialog):
def __init__(self, queue, emitter, parent=None):
super(Form,self).__init__(parent)
self.data_to_child = queue
self.emitter = emitter
self.emitter.daemon = True
self.emitter.start()
self.browser = QTextBrowser()
self.lineedit = QLineEdit(''Type text and press <Enter>'')
self.lineedit.selectAll()
layout = QVBoxLayout()
layout.addWidget(self.browser)
layout.addWidget(self.lineedit)
self.setLayout(layout)
self.lineedit.setFocus()
self.setWindowTitle(''Upper'')
self.connect(self.lineedit,SIGNAL(''returnPressed()''),self.to_child)
self.connect(self.emitter,SIGNAL(''data(PyQt_PyObject)''), self.updateUI)
def to_child(self):
self.data_to_child.put(unicode(self.lineedit.text()))
self.lineedit.clear()
def updateUI(self, text):
text = text[0]
self.browser.append(text)
class ChildProc(Process):
def __init__(self, transport, queue, daemon=True):
Process.__init__(self)
self.daemon = daemon
self.transport = transport
self.data_from_mother = queue
def emit_to_mother(self, signature, args=None):
signature = (signature, )
if args:
signature += (args, )
self.transport.send(signature)
def run(self):
while True:
text = self.data_from_mother.get()
self.emit_to_mother(''data(PyQt_PyObject)'', (text.upper(),))
if __name__ == ''__main__'':
app = QApplication(sys.argv)
mother_pipe, child_pipe = Pipe()
queue = Queue()
emitter = Emitter(mother_pipe)
form = Form(queue, emitter)
ChildProc(child_pipe, queue).start()
form.show()
app.exec_()
Y como conveniencia también Python 3.X, PySide:
from multiprocessing import Process, Queue, Pipe
from threading import Thread
from PySide import QtGui, QtCore
class Emitter(QtCore.QObject, Thread):
def __init__(self, transport, parent=None):
QtCore.QObject.__init__(self, parent)
Thread.__init__(self)
self.transport = transport
def _emit(self, signature, args=None):
if args:
self.emit(QtCore.SIGNAL(signature), args)
else:
self.emit(QtCore.SIGNAL(signature))
def run(self):
while True:
try:
signature = self.transport.recv()
except EOFError:
break
else:
self._emit(*signature)
class Form(QtGui.QDialog):
def __init__(self, queue, emitter, parent=None):
super().__init__(parent)
self.data_to_child = queue
self.emitter = emitter
self.emitter.daemon = True
self.emitter.start()
self.browser = QtGui.QTextBrowser()
self.lineedit = QtGui.QLineEdit(''Type text and press <Enter>'')
self.lineedit.selectAll()
layout = QtGui.QVBoxLayout()
layout.addWidget(self.browser)
layout.addWidget(self.lineedit)
self.setLayout(layout)
self.lineedit.setFocus()
self.setWindowTitle(''Upper'')
self.lineedit.returnPressed.connect(self.to_child)
self.connect(self.emitter, QtCore.SIGNAL(''data(PyObject)''), self.updateUI)
def to_child(self):
self.data_to_child.put(self.lineedit.text())
self.lineedit.clear()
def updateUI(self, text):
self.browser.append(text[0])
class ChildProc(Process):
def __init__(self, transport, queue, daemon=True):
Process.__init__(self)
self.daemon = daemon
self.transport = transport
self.data_from_mother = queue
def emit_to_mother(self, signature, args=None):
signature = (signature, )
if args:
signature += (args, )
self.transport.send(signature)
def run(self):
while True:
text = self.data_from_mother.get()
self.emit_to_mother(''data(PyQt_PyObject)'', (text.upper(),))
if __name__ == ''__main__'':
app = QApplication(sys.argv)
mother_pipe, child_pipe = Pipe()
queue = Queue()
emitter = Emitter(mother_pipe)
form = Form(queue, emitter)
ChildProc(child_pipe, queue).start()
form.show()
app.exec_()
Contexto: en Python, un subproceso principal genera un segundo proceso (mediante el uso del módulo de multiprocesamiento) y luego inicia una GUI (mediante PyQt4). En este punto, el hilo principal se bloquea hasta que se cierra la GUI. El segundo proceso siempre se está procesando y, idealmente, debería emitir señales a una o varias ranuras en la GUI de forma asíncrona.
Pregunta: ¿Qué enfoque / herramientas están disponibles en Python y PyQt4 para lograr eso y cómo? Preferiblemente de una manera de interrupción suave en lugar de sondeo.
En términos abstractos, la solución que se me ocurre es una "herramienta / manejador" instanciada en el hilo principal que toma las ranuras disponibles de la instancia de la GUI y se conecta con las señales capturadas del segundo proceso, suponiendo que proporcione esta herramienta alguna información de lo que esperar o codificado duro. Esto podría ser instanciado a un tercer proceso / hilo.
Primero debe ver cómo funcionan las señales / ranuras en un solo proceso de Python:
Si solo hay un QThread en ejecución, solo llaman directamente a las ranuras.
Si la señal se emite en un subproceso diferente, tiene que encontrar el subproceso de destino de la señal y colocar un mensaje / publicar un evento en la cola de subprocesos de este subproceso. Este hilo luego, a su debido tiempo, procesará el mensaje / evento y llamará a la señal.
Por lo tanto, siempre hay algún tipo de sondeo interno y lo importante es que el sondeo no es bloqueante.
Los procesos creados por multiprocessing pueden comunicarse a través de Pipes, lo que le brinda dos connections para cada lado.
La función de poll
de la Connection
no es de bloqueo, por lo tanto, la QTimer
regularmente con un QTimer
y luego emitiría señales en consecuencia.
Otra solución podría ser tener un Thread
desde el módulo de subprocesamiento (o un QThread) específicamente esperando a get
nuevos mensajes de una Queue
con la función get
de la cola. Consulte la sección Tuberías y colas de multiprocessing para obtener más información.
Aquí hay un ejemplo de inicio de una GUI de Qt en otro Process
junto con un Thread
que escucha en una Connection
y, en un determinado mensaje, cierra la GUI que luego finaliza el proceso.
from multiprocessing import Process, Pipe
from threading import Thread
import time
from PySide import QtGui
class MyProcess(Process):
def __init__(self, child_conn):
super().__init__()
self.child_conn = child_conn
def run(self):
# start a qt application
app = QtGui.QApplication([])
window = QtGui.QWidget()
layout = QtGui.QVBoxLayout(window)
button = QtGui.QPushButton(''Test'')
button.clicked.connect(self.print_something)
layout.addWidget(button)
window.show()
# start thread which listens on the child_connection
t = Thread(target=self.listen, args = (app,))
t.start()
app.exec_() # this will block this process until somebody calls app.quit
def listen(self, app):
while True:
message = self.child_conn.recv()
if message == ''stop now'':
app.quit()
return
def print_something(self):
print("button pressed")
if __name__ == ''__main__'':
parent_conn, child_conn = Pipe()
s = MyProcess(child_conn)
s.start()
time.sleep(5)
parent_conn.send(''stop now'')
s.join()
Todo
Espero que esto no sea considerado como un necro-volcado, sin embargo, pensé que sería bueno actualizar la respuesta de Nizam agregando la actualización de su ejemplo a PyQt5, agregando algunos comentarios, eliminando algo de sintaxis de python2 y, sobre todo, usando el nuevo estilo de Señales disponibles en PyQt. Espero que alguien lo encuentre útil.
"""
Demo to show how to use PyQt5 and qt signals in combination with threads and
processes.
Description:
Text is entered in the main dialog, this is send over a queue to a process that
performs a "computation" (i.e. capitalization) on the data. Next the process sends
the data over a pipe to the Emitter which will emit a signal that will trigger
the UI to update.
Note:
At first glance it seems more logical to have the process emit the signal that
the UI can be updated. I tried this but ran into the error
"TypeError: can''t pickle ChildProc objects" which I am unable to fix.
"""
import sys
from multiprocessing import Process, Queue, Pipe
from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtWidgets import QApplication, QLineEdit, QTextBrowser, QVBoxLayout, QDialog
class Emitter(QThread):
""" Emitter waits for data from the capitalization process and emits a signal for the UI to update its text. """
ui_data_available = pyqtSignal(str) # Signal indicating new UI data is available.
def __init__(self, from_process: Pipe):
super().__init__()
self.data_from_process = from_process
def run(self):
while True:
try:
text = self.data_from_process.recv()
except EOFError:
break
else:
self.ui_data_available.emit(text.decode(''utf-8''))
class ChildProc(Process):
""" Process to capitalize a received string and return this over the pipe. """
def __init__(self, to_emitter: Pipe, from_mother: Queue, daemon=True):
super().__init__()
self.daemon = daemon
self.to_emitter = to_emitter
self.data_from_mother = from_mother
def run(self):
""" Wait for a ui_data_available on the queue and send a capitalized version of the received string to the pipe. """
while True:
text = self.data_from_mother.get()
self.to_emitter.send(text.upper())
class Form(QDialog):
def __init__(self, child_process_queue: Queue, emitter: Emitter):
super().__init__()
self.process_queue = child_process_queue
self.emitter = emitter
self.emitter.daemon = True
self.emitter.start()
# ------------------------------------------------------------------------------------------------------------
# Create the UI
# -------------------------------------------------------------------------------------------------------------
self.browser = QTextBrowser()
self.lineedit = QLineEdit(''Type text and press <Enter>'')
self.lineedit.selectAll()
layout = QVBoxLayout()
layout.addWidget(self.browser)
layout.addWidget(self.lineedit)
self.setLayout(layout)
self.lineedit.setFocus()
self.setWindowTitle(''Upper'')
# -------------------------------------------------------------------------------------------------------------
# Connect signals
# -------------------------------------------------------------------------------------------------------------
# When enter is pressed on the lineedit call self.to_child
self.lineedit.returnPressed.connect(self.to_child)
# When the emitter has data available for the UI call the updateUI function
self.emitter.ui_data_available.connect(self.updateUI)
def to_child(self):
""" Send the text of the lineedit to the process and clear the lineedit box. """
self.process_queue.put(self.lineedit.text().encode(''utf-8''))
self.lineedit.clear()
def updateUI(self, text):
""" Add text to the lineedit box. """
self.browser.append(text)
if __name__ == ''__main__'':
# Some setup for qt
app = QApplication(sys.argv)
# Create the communication lines.
mother_pipe, child_pipe = Pipe()
queue = Queue()
# Instantiate (i.e. create instances of) our classes.
emitter = Emitter(mother_pipe)
child_process = ChildProc(child_pipe, queue)
form = Form(queue, emitter)
# Start our process.
child_process.start()
# Show the qt GUI and wait for it to exit.
form.show()
app.exec_()
Tuve el mismo problema en C ++. Desde una aplicación de Q, yo engendro un objeto de servicio. El objeto crea el Gui Widget pero no es su padre (el padre es QApplication en ese momento). Para controlar el GuiWidget desde el widget de servicio, solo uso las señales y las ranuras como de costumbre y funciona como se esperaba. Nota: El hilo de GuiWidget y el del servicio son diferentes. El servicio es una subclase de QObject.
Si necesita un mecanismo de multiproceso de señal de proceso, intente utilizar Apache Thrift o un proceso de monitoreo de Qt que genere 2 objetos QProcess.
Un tema bastante interesante. Supongo que tener una señal que funciona entre hilos es una cosa muy útil. ¿Qué tal crear una señal personalizada basada en sockets? Todavía no he probado esto, pero esto es lo que obtuve con una investigación rápida:
class CrossThreadSignal(QObject):
signal = pyqtSignal(object)
def __init__(self, parent=None):
super(QObject, self).__init__(parent)
self.msgq = deque()
self.read_sck, self.write_sck = socket.socketpair()
self.notifier = QSocketNotifier(
self.read_sck.fileno(),
QtCore.QSocketNotifier.Read
)
self.notifier.activated.connect(self.recv)
def recv(self):
self.read_sck.recv(1)
self.signal.emit(self.msgq.popleft())
def input(self, message):
self.msgq.append(message)
self.write_sck.send(''s'')
Podría ponerte en el camino correcto.