QThread中运行时PyQt5 QProgressBar不出现
PyQt5 QProgressBar Does Not Appear when run in QThread
这个问题被删除了,但是我把代码更新成了一个MRE。我的终端上有 运行 它,它没有任何 compilation/runtime 错误,但其行为如下所述。由于版主没有回应我最初提出的在我更正问题后重新打开问题的请求,所以我删除了旧问题并将新问题放在这里。
我的信号更新了进度值,但进度条本身从未出现。我的代码有错误吗?
(要重新创建,请将下面列出的每个文件的代码放在如下所示的项目结构中。您只需要安装 PyQt5
。我在 Windows 10 上并使用 Python 3.8 带诗的虚拟环境。虚拟环境和诗是可选的)
主要
# main.py
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from app.controller.controller import Controller
from app.model.model import Model
from app.view.view import View
class MainApp:
def __init__(self) -> None:
self.controller = Controller()
self.model: Model = self.controller.model
self.view: View = self.controller.view
def show(self) -> None:
self.view.showMaximized()
if __name__ == "__main__":
app: QApplication = QApplication([])
app.setStyle("fusion")
app.setAttribute(Qt.AA_DontShowIconsInMenus, True)
root: MainApp = MainApp()
root.show()
app.exec_()
查看
# view.py
from typing import Any, Optional
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, pyqtSignal
class ProgressDialog(QtWidgets.QDialog):
def __init__(
self,
parent_: Optional[QtWidgets.QWidget] = None,
title: Optional[str] = None,
):
super().__init__(parent_)
self._title = title
self.pbar = QtWidgets.QProgressBar(self)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.pbar)
self.setLayout(layout)
self.resize(500, 50)
def on_start(self):
self.setModal(True)
self.show()
def on_finish(self):
self.hide()
self.setModal(False)
self.pbar.reset()
self.title = None
def on_update(self, value: int):
self.pbar.setValue(value)
print(self.pbar.value()) # For debugging...
@property
def title(self):
return self._title
@title.setter
def title(self, title_):
self._title = title_
self.setWindowTitle(title_)
class View(QtWidgets.QMainWindow):
def __init__(
self, controller, parent_: QtWidgets.QWidget = None, *args: Any, **kwargs: Any
) -> None:
super().__init__(parent_, *args, **kwargs)
self.controller: Controller = controller
self.setWindowTitle("App")
self.container = QtWidgets.QFrame()
self.container_layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.container_layout)
self.setCentralWidget(self.container)
# Create and position widgets
self.open_icon = self.style().standardIcon(QtWidgets.QStyle.SP_DirOpenIcon)
self.open_action = QtWidgets.QAction(self.open_icon, "&Open file...", self)
self.open_action.triggered.connect(self.controller.on_press_open_button)
self.toolbar = QtWidgets.QToolBar("Main ToolBar")
self.toolbar.setIconSize(QtCore.QSize(16, 16))
self.addToolBar(self.toolbar)
self.toolbar.addAction(self.open_action)
self.file_dialog = self._create_open_file_dialog()
self.progress_dialog = ProgressDialog(self)
def _create_open_file_dialog(self) -> QtWidgets.QFileDialog:
file_dialog = QtWidgets.QFileDialog(self)
filters = [
"Excel Documents (*.xlsx)",
]
file_dialog.setWindowTitle("Open File...")
file_dialog.setNameFilters(filters)
file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
return file_dialog
型号
# model.py
import time
from typing import Any
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QObject, pyqtSignal
class Model(QObject):
start_task: pyqtSignal = pyqtSignal()
finish_task: pyqtSignal = pyqtSignal()
update_task: pyqtSignal = pyqtSignal(int)
def __init__(
self,
controller,
*args: Any,
**kwargs: Any,
) -> None:
super().__init__()
self.controller = controller
def open_file(self, files: str) -> None:
self.start_task.emit()
for ndx, file_ in enumerate(files):
print(file_) # In truth, here, I'm actually performing processing
time.sleep(1) # Only here for simulating a long-running task
self.update_task.emit(int((ndx + 1) / len(files) * 100))
self.finish_task.emit()
控制器
# controller.py
from typing import Any
from app.model.model import Model
from app.view.view import View
from PyQt5 import QtCore, QtGui, QtWidgets
class Controller:
def __init__(
self,
*args: Any,
**kwargs: Any,
) -> None:
self.model = Model(controller=self, *args, **kwargs)
self.view = View(controller=self, *args, **kwargs)
def on_press_open_button(self) -> None:
if self.view.file_dialog.exec_() == QtWidgets.QDialog.Accepted:
file_names = self.view.file_dialog.selectedFiles()
self.view.progress_dialog.title = "Opening files..."
self.thread = QtCore.QThread()
self.model.moveToThread(self.thread)
self.thread.started.connect(lambda: self.model.open_file(file_names))
self.thread.finished.connect(self.thread.deleteLater)
self.model.start_task.connect(self.view.progress_dialog.on_start)
self.model.update_task.connect(
lambda value: self.view.progress_dialog.on_update(value)
)
self.model.finish_task.connect(self.view.progress_dialog.on_finish)
self.model.finish_task.connect(self.thread.quit)
self.model.finish_task.connect(self.model.deleteLater)
self.model.finish_task.connect(self.thread.deleteLater)
self.thread.start()
当我 运行 在一个包含 6 个文件的文件夹中执行上述操作时,它并没有 运行 处理得太快(我实际上正在执行总共需要大约 5 秒的处理)。它成功完成并且我的终端输出:
16
33
50
66
83
100
但是我的ProgressDialog
window整个过程就是这样:
如果我在 View
中的 __init__()
末尾添加 self.progress_dialog.show()
(为简洁起见被删减)
# view.py
# Snip...
class View(QtWidgets.QMainWindow):
def __init__( ... ):
# Snip...
self.progress_dialog.show()
然后添加进度条:
打开文件后,对话框按预期运行:
在 Kiwi Pycon 2019 上进行了一次启发性的演讲,帮助我确定了问题所在:"Python, Threads & Qt: Boom!"
- Every
QObject
is owned by a QThread
- A
QObject
instance must not be shared across threads
QWidget
objects (i.e. anything you can "see") are not re-entrant. Thus, they can only be called from the main UI thread.
第 3 点是我的问题。 Qt
不会阻止从主线程外部调用 QWidget
对象,但它不起作用。即使将我的 ProgressDialog
移动到创建的 QThread
也无济于事。因此,显示和隐藏 ProgressDialog
必须由主线程处理。
此外,一旦 QObject
被移动到一个单独的线程,重新运行代码将给出错误:
QObject::moveToThread: Current thread (0xoldbeef) is not the object's thread (0x0).
Cannot move to target thread (0xnewbeef)
因为它没有创建新的模型对象,而是复用了旧对象。因此,不幸的是,代码必须移动到一个单独的工作对象中。
正确的代码是:
- 将
on_start
和on_finish
从ProgressDialog
移动到View
(我将它们重命名为show_progress_dialog
和hide_progress_dialog
)
- 创建将
open_file
逻辑放在单独的 QObject
worker 中
- 自己调用
view.progress_dialog.show()
(尽管发出 thread.finished
时 thread
可以调用 hide
或 open
;我猜这是因为特殊逻辑当线程结束时在 Qt 中实现)
查看
from typing import Any, Optional
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, pyqtSignal
class ProgressDialog(QtWidgets.QDialog):
def __init__(
self,
parent_: Optional[QtWidgets.QWidget] = None,
title: Optional[str] = None,
):
super().__init__(parent_)
self._title = title
self.pbar = QtWidgets.QProgressBar(self)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.pbar)
self.setLayout(layout)
self.resize(500, 50)
def on_update(self, value: int):
self.pbar.setValue(value)
@property
def title(self):
return self._title
@title.setter
def title(self, title_):
self._title = title_
self.setWindowTitle(title_)
class View(QtWidgets.QMainWindow):
def __init__(
self, controller, parent_: QtWidgets.QWidget = None, *args: Any, **kwargs: Any
) -> None:
super().__init__(parent_, *args, **kwargs)
self.controller: Controller = controller
self.setWindowTitle("App")
self.container = QtWidgets.QFrame()
self.container_layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.container_layout)
self.setCentralWidget(self.container)
# Create and position widgets
self.open_icon = self.style().standardIcon(QtWidgets.QStyle.SP_DirOpenIcon)
self.open_action = QtWidgets.QAction(self.open_icon, "&Open file...", self)
self.open_action.triggered.connect(self.controller.on_press_open_button)
self.toolbar = QtWidgets.QToolBar("Main ToolBar")
self.toolbar.setIconSize(QtCore.QSize(16, 16))
self.addToolBar(self.toolbar)
self.toolbar.addAction(self.open_action)
self.file_dialog = self._create_open_file_dialog()
self.progress_dialog = ProgressDialog(self)
def _create_open_file_dialog(self) -> QtWidgets.QFileDialog:
file_dialog = QtWidgets.QFileDialog(self)
filters = [
"Excel Documents (*.xlsx)",
]
file_dialog.setWindowTitle("Open File...")
file_dialog.setNameFilters(filters)
file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
return file_dialog
def show_progress_dialog(self):
self.progress_dialog.setModal(True)
self.progress_dialog.show()
def hide_progress_dialog(self):
self.progress_dialog.hide()
self.progress_dialog.setModal(False)
self.progress_dialog.pbar.reset()
self.progress_dialog.title = None
型号
# model.py
import time
from typing import Any, Optional
from PyQt5.QtCore import QObject, pyqtSignal
class Model:
def __init__(
self,
controller,
*args: Any,
**kwargs: Any,
) -> None:
super().__init__()
self.controller = controller
class OpenFileWorker(QObject):
update: pyqtSignal = pyqtSignal(int)
finished: pyqtSignal = pyqtSignal()
def __init__(self) -> None:
super().__init__()
def open_file(self, files: str) -> None:
for ndx, file_ in enumerate(files):
print(file_) # In truth, here, I'm actually performing processing
time.sleep(1) # Only here for simulating a long-running task
self.update.emit(int((ndx + 1) / len(files) * 100))
self.finished.emit()
控制器
# controller.py
from typing import Any
from app.model.model import Model, OpenFileWorker
from app.view.view import View
from PyQt5 import QtCore, QtGui, QtWidgets
class Controller:
def __init__(
self,
*args: Any,
**kwargs: Any,
) -> None:
self.model = Model(controller=self, *args, **kwargs)
self.view = View(controller=self, *args, **kwargs)
def on_press_open_button(self) -> None:
if self.view.file_dialog.exec_() == QtWidgets.QDialog.Accepted:
file_names = self.view.file_dialog.selectedFiles()
self.view.progress_dialog.title = "Opening files..."
self.thread = QtCore.QThread()
self.open_worker = OpenFileWorker()
self.open_worker.moveToThread(self.thread)
self.view.show_progress_dialog()
self.thread.started.connect(lambda: self.open_worker.open_file(file_names))
self.open_worker.update.connect(
lambda value: self.view.progress_dialog.on_update(value)
)
self.open_worker.finished.connect(self.view.hide_progress_dialog)
self.open_worker.finished.connect(self.thread.quit)
self.thread.finished.connect(self.open_worker.deleteLater)
self.thread.start()
这个问题被删除了,但是我把代码更新成了一个MRE。我的终端上有 运行 它,它没有任何 compilation/runtime 错误,但其行为如下所述。由于版主没有回应我最初提出的在我更正问题后重新打开问题的请求,所以我删除了旧问题并将新问题放在这里。
我的信号更新了进度值,但进度条本身从未出现。我的代码有错误吗?
(要重新创建,请将下面列出的每个文件的代码放在如下所示的项目结构中。您只需要安装 PyQt5
。我在 Windows 10 上并使用 Python 3.8 带诗的虚拟环境。虚拟环境和诗是可选的)
主要
# main.py
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from app.controller.controller import Controller
from app.model.model import Model
from app.view.view import View
class MainApp:
def __init__(self) -> None:
self.controller = Controller()
self.model: Model = self.controller.model
self.view: View = self.controller.view
def show(self) -> None:
self.view.showMaximized()
if __name__ == "__main__":
app: QApplication = QApplication([])
app.setStyle("fusion")
app.setAttribute(Qt.AA_DontShowIconsInMenus, True)
root: MainApp = MainApp()
root.show()
app.exec_()
查看
# view.py
from typing import Any, Optional
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, pyqtSignal
class ProgressDialog(QtWidgets.QDialog):
def __init__(
self,
parent_: Optional[QtWidgets.QWidget] = None,
title: Optional[str] = None,
):
super().__init__(parent_)
self._title = title
self.pbar = QtWidgets.QProgressBar(self)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.pbar)
self.setLayout(layout)
self.resize(500, 50)
def on_start(self):
self.setModal(True)
self.show()
def on_finish(self):
self.hide()
self.setModal(False)
self.pbar.reset()
self.title = None
def on_update(self, value: int):
self.pbar.setValue(value)
print(self.pbar.value()) # For debugging...
@property
def title(self):
return self._title
@title.setter
def title(self, title_):
self._title = title_
self.setWindowTitle(title_)
class View(QtWidgets.QMainWindow):
def __init__(
self, controller, parent_: QtWidgets.QWidget = None, *args: Any, **kwargs: Any
) -> None:
super().__init__(parent_, *args, **kwargs)
self.controller: Controller = controller
self.setWindowTitle("App")
self.container = QtWidgets.QFrame()
self.container_layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.container_layout)
self.setCentralWidget(self.container)
# Create and position widgets
self.open_icon = self.style().standardIcon(QtWidgets.QStyle.SP_DirOpenIcon)
self.open_action = QtWidgets.QAction(self.open_icon, "&Open file...", self)
self.open_action.triggered.connect(self.controller.on_press_open_button)
self.toolbar = QtWidgets.QToolBar("Main ToolBar")
self.toolbar.setIconSize(QtCore.QSize(16, 16))
self.addToolBar(self.toolbar)
self.toolbar.addAction(self.open_action)
self.file_dialog = self._create_open_file_dialog()
self.progress_dialog = ProgressDialog(self)
def _create_open_file_dialog(self) -> QtWidgets.QFileDialog:
file_dialog = QtWidgets.QFileDialog(self)
filters = [
"Excel Documents (*.xlsx)",
]
file_dialog.setWindowTitle("Open File...")
file_dialog.setNameFilters(filters)
file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
return file_dialog
型号
# model.py
import time
from typing import Any
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QObject, pyqtSignal
class Model(QObject):
start_task: pyqtSignal = pyqtSignal()
finish_task: pyqtSignal = pyqtSignal()
update_task: pyqtSignal = pyqtSignal(int)
def __init__(
self,
controller,
*args: Any,
**kwargs: Any,
) -> None:
super().__init__()
self.controller = controller
def open_file(self, files: str) -> None:
self.start_task.emit()
for ndx, file_ in enumerate(files):
print(file_) # In truth, here, I'm actually performing processing
time.sleep(1) # Only here for simulating a long-running task
self.update_task.emit(int((ndx + 1) / len(files) * 100))
self.finish_task.emit()
控制器
# controller.py
from typing import Any
from app.model.model import Model
from app.view.view import View
from PyQt5 import QtCore, QtGui, QtWidgets
class Controller:
def __init__(
self,
*args: Any,
**kwargs: Any,
) -> None:
self.model = Model(controller=self, *args, **kwargs)
self.view = View(controller=self, *args, **kwargs)
def on_press_open_button(self) -> None:
if self.view.file_dialog.exec_() == QtWidgets.QDialog.Accepted:
file_names = self.view.file_dialog.selectedFiles()
self.view.progress_dialog.title = "Opening files..."
self.thread = QtCore.QThread()
self.model.moveToThread(self.thread)
self.thread.started.connect(lambda: self.model.open_file(file_names))
self.thread.finished.connect(self.thread.deleteLater)
self.model.start_task.connect(self.view.progress_dialog.on_start)
self.model.update_task.connect(
lambda value: self.view.progress_dialog.on_update(value)
)
self.model.finish_task.connect(self.view.progress_dialog.on_finish)
self.model.finish_task.connect(self.thread.quit)
self.model.finish_task.connect(self.model.deleteLater)
self.model.finish_task.connect(self.thread.deleteLater)
self.thread.start()
当我 运行 在一个包含 6 个文件的文件夹中执行上述操作时,它并没有 运行 处理得太快(我实际上正在执行总共需要大约 5 秒的处理)。它成功完成并且我的终端输出:
16
33
50
66
83
100
但是我的ProgressDialog
window整个过程就是这样:
如果我在 View
中的 __init__()
末尾添加 self.progress_dialog.show()
(为简洁起见被删减)
# view.py
# Snip...
class View(QtWidgets.QMainWindow):
def __init__( ... ):
# Snip...
self.progress_dialog.show()
然后添加进度条:
打开文件后,对话框按预期运行:
在 Kiwi Pycon 2019 上进行了一次启发性的演讲,帮助我确定了问题所在:"Python, Threads & Qt: Boom!"
- Every
QObject
is owned by aQThread
- A
QObject
instance must not be shared across threadsQWidget
objects (i.e. anything you can "see") are not re-entrant. Thus, they can only be called from the main UI thread.
第 3 点是我的问题。 Qt
不会阻止从主线程外部调用 QWidget
对象,但它不起作用。即使将我的 ProgressDialog
移动到创建的 QThread
也无济于事。因此,显示和隐藏 ProgressDialog
必须由主线程处理。
此外,一旦 QObject
被移动到一个单独的线程,重新运行代码将给出错误:
QObject::moveToThread: Current thread (0xoldbeef) is not the object's thread (0x0).
Cannot move to target thread (0xnewbeef)
因为它没有创建新的模型对象,而是复用了旧对象。因此,不幸的是,代码必须移动到一个单独的工作对象中。
正确的代码是:
- 将
on_start
和on_finish
从ProgressDialog
移动到View
(我将它们重命名为show_progress_dialog
和hide_progress_dialog
) - 创建将
open_file
逻辑放在单独的QObject
worker 中
- 自己调用
view.progress_dialog.show()
(尽管发出thread.finished
时thread
可以调用hide
或open
;我猜这是因为特殊逻辑当线程结束时在 Qt 中实现)
查看
from typing import Any, Optional
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, pyqtSignal
class ProgressDialog(QtWidgets.QDialog):
def __init__(
self,
parent_: Optional[QtWidgets.QWidget] = None,
title: Optional[str] = None,
):
super().__init__(parent_)
self._title = title
self.pbar = QtWidgets.QProgressBar(self)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.pbar)
self.setLayout(layout)
self.resize(500, 50)
def on_update(self, value: int):
self.pbar.setValue(value)
@property
def title(self):
return self._title
@title.setter
def title(self, title_):
self._title = title_
self.setWindowTitle(title_)
class View(QtWidgets.QMainWindow):
def __init__(
self, controller, parent_: QtWidgets.QWidget = None, *args: Any, **kwargs: Any
) -> None:
super().__init__(parent_, *args, **kwargs)
self.controller: Controller = controller
self.setWindowTitle("App")
self.container = QtWidgets.QFrame()
self.container_layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.container_layout)
self.setCentralWidget(self.container)
# Create and position widgets
self.open_icon = self.style().standardIcon(QtWidgets.QStyle.SP_DirOpenIcon)
self.open_action = QtWidgets.QAction(self.open_icon, "&Open file...", self)
self.open_action.triggered.connect(self.controller.on_press_open_button)
self.toolbar = QtWidgets.QToolBar("Main ToolBar")
self.toolbar.setIconSize(QtCore.QSize(16, 16))
self.addToolBar(self.toolbar)
self.toolbar.addAction(self.open_action)
self.file_dialog = self._create_open_file_dialog()
self.progress_dialog = ProgressDialog(self)
def _create_open_file_dialog(self) -> QtWidgets.QFileDialog:
file_dialog = QtWidgets.QFileDialog(self)
filters = [
"Excel Documents (*.xlsx)",
]
file_dialog.setWindowTitle("Open File...")
file_dialog.setNameFilters(filters)
file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
return file_dialog
def show_progress_dialog(self):
self.progress_dialog.setModal(True)
self.progress_dialog.show()
def hide_progress_dialog(self):
self.progress_dialog.hide()
self.progress_dialog.setModal(False)
self.progress_dialog.pbar.reset()
self.progress_dialog.title = None
型号
# model.py
import time
from typing import Any, Optional
from PyQt5.QtCore import QObject, pyqtSignal
class Model:
def __init__(
self,
controller,
*args: Any,
**kwargs: Any,
) -> None:
super().__init__()
self.controller = controller
class OpenFileWorker(QObject):
update: pyqtSignal = pyqtSignal(int)
finished: pyqtSignal = pyqtSignal()
def __init__(self) -> None:
super().__init__()
def open_file(self, files: str) -> None:
for ndx, file_ in enumerate(files):
print(file_) # In truth, here, I'm actually performing processing
time.sleep(1) # Only here for simulating a long-running task
self.update.emit(int((ndx + 1) / len(files) * 100))
self.finished.emit()
控制器
# controller.py
from typing import Any
from app.model.model import Model, OpenFileWorker
from app.view.view import View
from PyQt5 import QtCore, QtGui, QtWidgets
class Controller:
def __init__(
self,
*args: Any,
**kwargs: Any,
) -> None:
self.model = Model(controller=self, *args, **kwargs)
self.view = View(controller=self, *args, **kwargs)
def on_press_open_button(self) -> None:
if self.view.file_dialog.exec_() == QtWidgets.QDialog.Accepted:
file_names = self.view.file_dialog.selectedFiles()
self.view.progress_dialog.title = "Opening files..."
self.thread = QtCore.QThread()
self.open_worker = OpenFileWorker()
self.open_worker.moveToThread(self.thread)
self.view.show_progress_dialog()
self.thread.started.connect(lambda: self.open_worker.open_file(file_names))
self.open_worker.update.connect(
lambda value: self.view.progress_dialog.on_update(value)
)
self.open_worker.finished.connect(self.view.hide_progress_dialog)
self.open_worker.finished.connect(self.thread.quit)
self.thread.finished.connect(self.open_worker.deleteLater)
self.thread.start()