¿Cómo señalar las ranuras en una GUI de un proceso diferente?

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.

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

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 multiprocesamiento pueden comunicarse a través de Pipes, lo que le brinda dos conexiones para cada lado.

La función de poll de la Connection no es de locking, por lo tanto, la QTimer regularmente con un QTimer y luego emití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 multiprocesamiento 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() 

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.

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.

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 syntax 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" (ie 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 ') 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 (ie 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_()