PyQt4:如何暂停线程直到发出信号?
PyQt4: How to pause a Thread until a signal is emitted?
我有以下 pyqtmain.py:
#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
self.qt_app = QApplication(sys.argv)
QMainWindow.__init__(self, parent)
buttonWidget = QWidget()
rsltLabel = QLabel("Result:")
self.rsltFiled = QLineEdit()
self.buttonStart = QPushButton("Start")
verticalLayout = QVBoxLayout(buttonWidget)
verticalLayout.addWidget(rsltLabel)
verticalLayout.addWidget(self.rsltFiled)
verticalLayout.addWidget(self.buttonStart)
butDW = QDockWidget("Control", self)
butDW.setWidget(buttonWidget)
self.addDockWidget(Qt.LeftDockWidgetArea, butDW)
self.mthread = QThread() # New thread to run the Measurement Engine
self.worker = MeasurementEngine() # Measurement Engine Object
self.worker.moveToThread(self.mthread)
self.mthread.finished.connect(self.worker.deleteLater) # Cleanup after thread finished
self.worker.measure_msg.connect(self.showRslt)
self.buttonStart.clicked.connect(self.worker.run)
# Everything configured, start the worker thread.
self.mthread.start()
def run(self):
""" Show the window and start the event loop """
self.show()
self.qt_app.exec_() # Start event loop
@pyqtSlot(str)
def showRslt(self, mystr):
self.rsltFiled.setText(mystr)
def main():
win = MainWindow()
win.run()
if __name__ == '__main__':
main()
另一个执行实际测量的线程脚本:
from PyQt4.QtCore import *
import time
class MeasurementEngine(QObject):
measure_msg = pyqtSignal(str)
def __init__(self):
QObject.__init__(self) # Don't forget to call base class constructor
@pyqtSlot()
def run(self):
self.measure_msg.emit('phase1')
time.sleep(2) # here I would like to make it as an interrupt
self.measure_msg.emit('phase2')
这段代码现在所做的是,在按下开始按钮后,将执行线程中的函数运行。然而,实际上在函数运行中,有两个阶段的测量。现在我使用了时间延迟。
但实际上我想实现的是 'phase1' 测量完成后。会弹出一个消息框,同时跟帖paused/held。直到用户关闭消息框,线程函数才会恢复。
您不能在 QThread
中显示 QDialog
。所有与 GUI 相关的事情都必须在 GUI 线程(创建 QApplication
对象的线程)中完成。你可以做的是使用 2 QThread
:
- 第 1 步:执行 phase1。您可以将此
QThread
的 finished
信号连接到将显示弹出窗口的 QMainWindow
中的插槽(使用 QDialog.exec_()
,因此它将是模态的)。
- 第 2 次:执行 phase2。在上面显示的弹出窗口关闭后创建
QThread
。
您的线程可以向主线程发出信号 window 以显示对话框。
如果您不想在对话框打开时关闭线程,线程可以进入 while 循环等待。在 while 循环中,它可以不断检查主线程可以在对话结束后将其设置为 true 的变量。
这可能不是最干净的解决方案,但它应该有效。
为了澄清我的回答,我添加了一些伪代码。您需要关心的是如何共享 dialog_closed
变量。你可以例如使用线程的一个成员变量class.
Thread:
emit_signal
dialog_closed = False
while not dialog_closed:
pass
go_on_with_processing
MainThread:
def SignalRecieved():
open_dialog
dialog_closed = True
使用 QtCore
模块中的 QWaitCondition
。使用互斥锁,您可以将后台线程设置为 wait/sleep,直到前台线程将其唤醒。然后它将继续从那里开始工作。
#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
self.qt_app = QApplication(sys.argv)
QMainWindow.__init__(self, parent)
buttonWidget = QWidget()
rsltLabel = QLabel("Result:")
self.rsltFiled = QLineEdit()
self.buttonStart = QPushButton("Start")
verticalLayout = QVBoxLayout(buttonWidget)
verticalLayout.addWidget(rsltLabel)
verticalLayout.addWidget(self.rsltFiled)
verticalLayout.addWidget(self.buttonStart)
butDW = QDockWidget("Control", self)
butDW.setWidget(buttonWidget)
self.addDockWidget(Qt.LeftDockWidgetArea, butDW)
self.mutex = QMutex()
self.cond = QWaitCondition()
self.mthread = QThread() # New thread to run the Measurement Engine
self.worker = MeasurementEngine(self.mutex, self.cond) # Measurement Engine Object
self.worker.moveToThread(self.mthread)
self.mthread.finished.connect(self.worker.deleteLater) # Cleanup after thread finished
self.worker.measure_msg.connect(self.showRslt)
self.buttonStart.clicked.connect(self.worker.run)
# Everything configured, start the worker thread.
self.mthread.start()
def run(self):
""" Show the window and start the event loop """
self.show()
self.qt_app.exec_() # Start event loop
# since this is a slot, it will always get run in the event loop in the main thread
@pyqtSlot(str)
def showRslt(self, mystr):
self.rsltFiled.setText(mystr)
msgBox = QMessageBox(parent=self)
msgBox.setText("Close this dialog to continue to Phase 2.")
msgBox.exec_()
self.cond.wakeAll()
def main():
win = MainWindow()
win.run()
if __name__ == '__main__':
main()
并且:
from PyQt4.QtCore import *
import time
class MeasurementEngine(QObject):
measure_msg = pyqtSignal(str)
def __init__(self, mutex, cond):
QObject.__init__(self) # Don't forget to call base class constructor
self.mtx = mutex
self.cond = cond
@pyqtSlot()
def run(self):
# NOTE: do work for phase 1 here
self.measure_msg.emit('phase1')
self.mtx.lock()
try:
self.cond.wait(self.mtx)
# NOTE: do work for phase 2 here
self.measure_msg.emit('phase2')
finally:
self.mtx.unlock()
尽管如此,您的时机有点不对。您甚至在展示 window 之前就创建了应用程序并启动了线程。因此,消息框会在 弹出 之前弹出主 window 甚至弹出。要获得正确的事件顺序,您应该将线程作为 MainWindow 的 run
方法的一部分启动,after 你有已经使主要 window 可见。如果您希望等待条件与消息的设置分开,您可能需要一个单独的信号和插槽来处理它。
我最近不得不解决这个问题,做了一些研究并发现了 an elegant technique that seems to work reliably。我不需要那里详述的全部复杂性,所以这里是我所采取步骤的概要。
我的 GUI class 将两个信号定义为 class 属性。
oyn_sig = pyqtSignal(str) # Request for operator yes/no
ryn_sig = pyqtSignal(bool) # Response to yes/no request
在初始化 GUI 组件的方法中,此信号连接到 GUI 实例的信号处理程序。
self.oyn_sig.connect(self.operator_yes_no)
这是 GUI 的处理程序方法的代码:
@pyqtSlot(str)
def operator_yes_no(self, msg):
"Asks the user a `yes/no question on receipt of a signal then signal a bool answer.`"
answer = QMessageBox.question(None,
"Confirm Test Sucess",
msg,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
# Signal the caller that the result was received.
self.ryn_sig.emit(answer==QMessageBox.Yes)
像往常一样,GUI 在主线程中 运行,因此需要从在后台执行工作的线程发出信号。反过来,一旦收到操作员的响应,它就会向原始线程发出响应信号。
工作线程使用以下函数获取操作员响应。
def operator_yes_no(self, msg):
loop = LoopSpinner(self.gui, msg)
loop.exec_()
return loop.result
这将创建一个 LoopSpinner
对象并开始执行其事件循环,从而暂停当前线程的事件循环直到 "inner thread" 终止。大多数智能都隐藏在 LoopSpinner
class 中,可能应该更好地命名。这是它的定义。
class LoopSpinner(QEventLoop):
def __init__(self, gui, msg):
"Ask for an answer and communicate the result."
QEventLoop.__init__(self)
gui.ryn_sig.connect(self.get_answer)
gui.oyn_sig.emit(msg)
@pyqtSlot(bool)
def get_answer(self, result):
self.result = result
self.quit()
LoopSpinner 实例将响应信号连接到其 get_answer
方法并发出问题信号。收到信号后,答案将存储为属性值,然后循环退出。该循环仍由其调用者引用,调用者可以在实例被垃圾回收之前安全地访问结果属性。
我有以下 pyqtmain.py:
#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
self.qt_app = QApplication(sys.argv)
QMainWindow.__init__(self, parent)
buttonWidget = QWidget()
rsltLabel = QLabel("Result:")
self.rsltFiled = QLineEdit()
self.buttonStart = QPushButton("Start")
verticalLayout = QVBoxLayout(buttonWidget)
verticalLayout.addWidget(rsltLabel)
verticalLayout.addWidget(self.rsltFiled)
verticalLayout.addWidget(self.buttonStart)
butDW = QDockWidget("Control", self)
butDW.setWidget(buttonWidget)
self.addDockWidget(Qt.LeftDockWidgetArea, butDW)
self.mthread = QThread() # New thread to run the Measurement Engine
self.worker = MeasurementEngine() # Measurement Engine Object
self.worker.moveToThread(self.mthread)
self.mthread.finished.connect(self.worker.deleteLater) # Cleanup after thread finished
self.worker.measure_msg.connect(self.showRslt)
self.buttonStart.clicked.connect(self.worker.run)
# Everything configured, start the worker thread.
self.mthread.start()
def run(self):
""" Show the window and start the event loop """
self.show()
self.qt_app.exec_() # Start event loop
@pyqtSlot(str)
def showRslt(self, mystr):
self.rsltFiled.setText(mystr)
def main():
win = MainWindow()
win.run()
if __name__ == '__main__':
main()
另一个执行实际测量的线程脚本:
from PyQt4.QtCore import *
import time
class MeasurementEngine(QObject):
measure_msg = pyqtSignal(str)
def __init__(self):
QObject.__init__(self) # Don't forget to call base class constructor
@pyqtSlot()
def run(self):
self.measure_msg.emit('phase1')
time.sleep(2) # here I would like to make it as an interrupt
self.measure_msg.emit('phase2')
这段代码现在所做的是,在按下开始按钮后,将执行线程中的函数运行。然而,实际上在函数运行中,有两个阶段的测量。现在我使用了时间延迟。
但实际上我想实现的是 'phase1' 测量完成后。会弹出一个消息框,同时跟帖paused/held。直到用户关闭消息框,线程函数才会恢复。
您不能在 QThread
中显示 QDialog
。所有与 GUI 相关的事情都必须在 GUI 线程(创建 QApplication
对象的线程)中完成。你可以做的是使用 2 QThread
:
- 第 1 步:执行 phase1。您可以将此
QThread
的finished
信号连接到将显示弹出窗口的QMainWindow
中的插槽(使用QDialog.exec_()
,因此它将是模态的)。 - 第 2 次:执行 phase2。在上面显示的弹出窗口关闭后创建
QThread
。
您的线程可以向主线程发出信号 window 以显示对话框。 如果您不想在对话框打开时关闭线程,线程可以进入 while 循环等待。在 while 循环中,它可以不断检查主线程可以在对话结束后将其设置为 true 的变量。 这可能不是最干净的解决方案,但它应该有效。
为了澄清我的回答,我添加了一些伪代码。您需要关心的是如何共享 dialog_closed
变量。你可以例如使用线程的一个成员变量class.
Thread:
emit_signal
dialog_closed = False
while not dialog_closed:
pass
go_on_with_processing
MainThread:
def SignalRecieved():
open_dialog
dialog_closed = True
使用 QtCore
模块中的 QWaitCondition
。使用互斥锁,您可以将后台线程设置为 wait/sleep,直到前台线程将其唤醒。然后它将继续从那里开始工作。
#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
self.qt_app = QApplication(sys.argv)
QMainWindow.__init__(self, parent)
buttonWidget = QWidget()
rsltLabel = QLabel("Result:")
self.rsltFiled = QLineEdit()
self.buttonStart = QPushButton("Start")
verticalLayout = QVBoxLayout(buttonWidget)
verticalLayout.addWidget(rsltLabel)
verticalLayout.addWidget(self.rsltFiled)
verticalLayout.addWidget(self.buttonStart)
butDW = QDockWidget("Control", self)
butDW.setWidget(buttonWidget)
self.addDockWidget(Qt.LeftDockWidgetArea, butDW)
self.mutex = QMutex()
self.cond = QWaitCondition()
self.mthread = QThread() # New thread to run the Measurement Engine
self.worker = MeasurementEngine(self.mutex, self.cond) # Measurement Engine Object
self.worker.moveToThread(self.mthread)
self.mthread.finished.connect(self.worker.deleteLater) # Cleanup after thread finished
self.worker.measure_msg.connect(self.showRslt)
self.buttonStart.clicked.connect(self.worker.run)
# Everything configured, start the worker thread.
self.mthread.start()
def run(self):
""" Show the window and start the event loop """
self.show()
self.qt_app.exec_() # Start event loop
# since this is a slot, it will always get run in the event loop in the main thread
@pyqtSlot(str)
def showRslt(self, mystr):
self.rsltFiled.setText(mystr)
msgBox = QMessageBox(parent=self)
msgBox.setText("Close this dialog to continue to Phase 2.")
msgBox.exec_()
self.cond.wakeAll()
def main():
win = MainWindow()
win.run()
if __name__ == '__main__':
main()
并且:
from PyQt4.QtCore import *
import time
class MeasurementEngine(QObject):
measure_msg = pyqtSignal(str)
def __init__(self, mutex, cond):
QObject.__init__(self) # Don't forget to call base class constructor
self.mtx = mutex
self.cond = cond
@pyqtSlot()
def run(self):
# NOTE: do work for phase 1 here
self.measure_msg.emit('phase1')
self.mtx.lock()
try:
self.cond.wait(self.mtx)
# NOTE: do work for phase 2 here
self.measure_msg.emit('phase2')
finally:
self.mtx.unlock()
尽管如此,您的时机有点不对。您甚至在展示 window 之前就创建了应用程序并启动了线程。因此,消息框会在 弹出 之前弹出主 window 甚至弹出。要获得正确的事件顺序,您应该将线程作为 MainWindow 的 run
方法的一部分启动,after 你有已经使主要 window 可见。如果您希望等待条件与消息的设置分开,您可能需要一个单独的信号和插槽来处理它。
我最近不得不解决这个问题,做了一些研究并发现了 an elegant technique that seems to work reliably。我不需要那里详述的全部复杂性,所以这里是我所采取步骤的概要。
我的 GUI class 将两个信号定义为 class 属性。
oyn_sig = pyqtSignal(str) # Request for operator yes/no
ryn_sig = pyqtSignal(bool) # Response to yes/no request
在初始化 GUI 组件的方法中,此信号连接到 GUI 实例的信号处理程序。
self.oyn_sig.connect(self.operator_yes_no)
这是 GUI 的处理程序方法的代码:
@pyqtSlot(str)
def operator_yes_no(self, msg):
"Asks the user a `yes/no question on receipt of a signal then signal a bool answer.`"
answer = QMessageBox.question(None,
"Confirm Test Sucess",
msg,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
# Signal the caller that the result was received.
self.ryn_sig.emit(answer==QMessageBox.Yes)
像往常一样,GUI 在主线程中 运行,因此需要从在后台执行工作的线程发出信号。反过来,一旦收到操作员的响应,它就会向原始线程发出响应信号。
工作线程使用以下函数获取操作员响应。
def operator_yes_no(self, msg):
loop = LoopSpinner(self.gui, msg)
loop.exec_()
return loop.result
这将创建一个 LoopSpinner
对象并开始执行其事件循环,从而暂停当前线程的事件循环直到 "inner thread" 终止。大多数智能都隐藏在 LoopSpinner
class 中,可能应该更好地命名。这是它的定义。
class LoopSpinner(QEventLoop):
def __init__(self, gui, msg):
"Ask for an answer and communicate the result."
QEventLoop.__init__(self)
gui.ryn_sig.connect(self.get_answer)
gui.oyn_sig.emit(msg)
@pyqtSlot(bool)
def get_answer(self, result):
self.result = result
self.quit()
LoopSpinner 实例将响应信号连接到其 get_answer
方法并发出问题信号。收到信号后,答案将存储为属性值,然后循环退出。该循环仍由其调用者引用,调用者可以在实例被垃圾回收之前安全地访问结果属性。