PyQt: Conexión de una señal a una ranura para iniciar una operación en segundo plano
Tengo el siguiente código que realiza una operación en segundo plano ( scan_value
) mientras actualiza una barra de progreso en la interfaz de usuario ( progress
). scan_value
itera sobre algún valor en obj
, emitiendo una señal ( value_changed
) cada vez que se cambia el valor. Por razones que no son relevantes aquí, tengo que envolver esto en un objeto ( Scanner
) en otro hilo. Se llama al escáner cuando el botón a scan
está clicked
. Y aquí viene mi pregunta... el siguiente código funciona bien (es decir, la barra de progreso se actualiza a tiempo).
# I am copying only the relevant code here.
def update_progress_bar(new, old):
fraction = (new - start) / (stop - start)
progress.setValue(fraction * 100)
obj.value_changed.connect(update_progress_bar)
class Scanner(QObject):
def scan(self):
scan_value(start, stop, step)
progress.setValue(100)
thread = QThread()
scanner = Scanner()
scanner.moveToThread(thread)
thread.start()
scan.clicked.connect(scanner.scan)
Pero si cambio la última parte a esto:
thread = QThread()
scanner = Scanner()
scan.clicked.connect(scanner.scan) # This was at the end!
scanner.moveToThread(thread)
thread.start()
La barra de progreso se actualiza sólo al final (supongo que todo se ejecuta en el mismo hilo). ¿Debería ser irrelevante si conecto la señal a una ranura antes o después de mover el objeto que recibe el objeto al hilo?
No debería importar si la conexión se realiza antes o después de mover el objeto trabajador al otro hilo. Para citar los documentos Qt :
Qt::AutoConnection : si la señal se emite desde un hilo diferente al objeto receptor, la señal se pone en cola y se comporta como Qt::QueuedConnection . De lo contrario, la ranura se invoca directamente y se comporta como Qt::DirectConnection . El tipo de conexión se determina cuando se emite la señal . [énfasis añadido]
Entonces, siempre que el type
argumento de connect
esté establecido en QtCore.Qt.AutoConnection
(que es el valor predeterminado), Qt debería garantizar que las señales se emitan de la manera adecuada.
Es más probable que el problema con el código de ejemplo esté en la ranura que en la señal . El método de Python al que está conectada la señal probablemente deba marcarse como una ranura Qt, utilizando el decorador pyqtSlot :
from QtCore import pyqtSlot
class Scanner(QObject):
@pyqtSlot()
def scan(self):
scan_value(start, stop, step)
progress.setValue(100)
EDITAR :
Cabe aclarar que sólo en versiones bastante recientes de Qt se determina el tipo de conexión cuando se emite la señal. Este comportamiento se introdujo (junto con varios otros cambios en el soporte multiproceso de Qt) con la versión 4.4.
Además, podría valer la pena ampliar más la cuestión específica de PyQt. En PyQt, una señal se puede conectar a una ranura Qt, a otra señal o a cualquier Python invocable (incluidas lambda
las funciones). Para el último caso, se crea internamente un objeto proxy que envuelve el invocable de Python y proporciona la ranura requerida por el mecanismo de señal/ranura de Qt.
Es este objeto proxy el que es la causa del problema. Una vez creado el proxy, PyQt simplemente hará esto:
if (rx_qobj)
proxy->moveToThread(rx_qobj->thread());
lo cual está bien si la conexión se realiza después de que el objeto receptor (es decir rx_qobj
, ) se haya movido a su hilo; pero si se realiza antes , el proxy permanecerá en el hilo principal.
El uso del @pyqtSlot
decorador evita este problema por completo, porque crea una ranura Qt de forma más directa y no utiliza ningún objeto proxy.
Por último, también cabe señalar que este problema no afecta actualmente a PySide.