在 PyQt 的第二个线程中打开子对话框的正确方法是啥?
Posted
技术标签:
【中文标题】在 PyQt 的第二个线程中打开子对话框的正确方法是啥?【英文标题】:What is the proper way of opening a child dialog in a second thread in PyQt?在 PyQt 的第二个线程中打开子对话框的正确方法是什么? 【发布时间】:2020-02-25 10:43:17 【问题描述】:我有一个应用程序,我在第二个线程中运行一些进程,并且在某些时候,给定特定条件,另一个对话框窗口打开,它会暂停进程,直到您确认某些内容。这会导致以下错误消息:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x1f9c82383d0), parent's thread is QThread(0x1f9c7ade2a0), current thread is QThread(0x1f9c8358800)
有趣的是,如果您在进程运行时也将光标移到 MainWindow
上,并且在弹出新对话框之前,它还会多次生成此错误消息:
QBasicTimer::stop: Failed. Possibly trying to stop from a different thread
很奇怪。因为它只有在您将光标移到MainWindow
上时才会发生。
现在,在我的应用程序中,我实际上为使用PyQt5.uic.loadUi
弹出的新对话框加载了一个界面,这并没有造成任何问题。但是,当我为这篇文章创建示例时,又出现了另一个问题,因为我在初始化期间设置了新对话框的布局:
QObject::setParent: Cannot set parent, new parent is in a different thread
这会导致应用程序崩溃:
Process finished with exit code -1073741819 (0xC0000005)
对于我猜想的线程,我显然在这里做错了,但我不知道是什么。令我特别困惑的是,在初始化期间我无法设置新对话框的布局,而使用loadUi
完全没问题。这是我的示例代码:
import sys
import time
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import (
QDialog, QApplication, QPushButton, QGridLayout, QProgressBar, QLabel
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('pass variable')
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
def run(self):
result = None
i = 0
while i < 100 and self._isRunning:
if i == 50:
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('test')
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel('Result:')
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.run)
self.handler_thread.start()
def progress(self, val):
self.pbar.setValue(val)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f'Result: result')
self.pbar.setValue(0)
self.handler.stop()
self.handler.progress.disconnect(self.progress)
self.handler.finished.disconnect(self.finisher)
self.handler_thread.started.disconnect(self.handler.run)
self.handler_thread.terminate()
self.handler = None
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
编辑
忘了说我已经找到this post了,这可能和我的问题有关,不过,我不理解置顶答案中的解决方案的推理,更重要的是,我不说什么我相信是 C++。
【问题讨论】:
【参考方案1】:您不能从辅助线程创建或修改 GUI 元素,这由错误消息指示。
您必须重新设计 Handler 类,根据您的要求,您必须将 run 分成 2 个方法,第一个方法将产生高达 50% 的进度,GUI 将打开对话框,获取结果并启动第二个方法。
import sys
import time
import numpy as np
from functools import partial
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QTimer
from PyQt5.QtWidgets import (
QDialog,
QApplication,
QPushButton,
QGridLayout,
QProgressBar,
QLabel,
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("pass variable")
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
@pyqtSlot()
def task1(self):
i = 0
while i <= 50 and self._isRunning:
time.sleep(0.01)
i += 1
self.progress.emit(i)
@pyqtSlot(int)
def task2(self, result):
i = 50
while i < 100 and self._isRunning:
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("test")
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel("Result:")
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.task1)
self.handler_thread.start()
@pyqtSlot(int)
def progress(self, val):
self.pbar.setValue(val)
if val == 50:
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
wrapper = partial(self.handler.task2, result)
QTimer.singleShot(0, wrapper)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f"Result: result")
self.pbar.setValue(0)
self.handler.stop()
self.handler_thread.quit()
self.handler_thread.wait()
if __name__ == "__main__":
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
【讨论】:
感谢您的回答!这让我走上了我希望的正确道路。我想我让这个例子有点太简单了,因为我实际上不知道何时满足打开新对话框的条件(如果有的话),以及这种情况发生的频率。但是根据您的解决方案,我想出了自己的解决方案,尽管我不确定这有多好。如果您能在下面看看我的回答,那就太好了。 @mapf 1)我只根据他们提供的代码解决他们提出的问题,所以我不假设复杂性或修改。如果您提供了更好的 MRE,我会针对您的实际问题提出更合适的解决方案。 2)由于我的回答解决了您的问题,所以我希望您将其标记为正确,如果您不知道该怎么做,请查看tour 我明白,对不起。我不知道我的问题的简化对于解决方案有多重要。在包含太多或留下我们的相关位之间走是一条细线。你是否建议我用更复杂的 MRE 再次问同样的问题并关闭这个问题? @mapf 是的,使用有效的 MRE 提出一个新问题,并删除您的答案,因为它不正确。以上是关于在 PyQt 的第二个线程中打开子对话框的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章