【问题标题】:PyQt5 QProgressBar Does Not Appear when run in QThreadPyQt5 QProgressBar 在 QThread 中运行时不出现
【发布时间】:2021-10-27 22:14:40
【问题描述】:

这个问题已被删除,但我将代码更新为 MRE。我已经在我的终端上运行了它,它没有任何编译/运行时错误,但其行为如下所述。由于版主在我更正后没有回复我重新打开问题的原始请求,因此我删除了旧问题并将新问题放在这里。

我的信号会更新进度值,但进度条本身从未出现。我的代码有错误吗?

(要重新创建,请将下面列出的每个文件的代码放在下面显示的项目结构中。您只需要安装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 窗口在整个过程中只是这个:

如果我在View 中的__init__() 末尾添加self.progress_dialog.show()(为简洁起见)

# view.py

# Snip...

class View(QtWidgets.QMainWindow):

    def __init__( ... ):
        # Snip...
        self.progress_dialog.show()

然后添加一个进度条:

打开文件后,对话框的行为与预期一致:

【问题讨论】:

  • 为了以后的参考,尽量让你的代码更容易重现。目录结构不是问题的关键,您甚至可以对所有部分使用单个代码块,而不是要求人们创建四个单独的文件。请记住:人们应该专注于问题,不要因为重新创建问题而分心,如果需要太多操作来执行代码,许多用户(可能会回答你)实际上会感到沮丧,结果他们将完全忽略这个问题;提供一个易于复制的示例应该是您的责任。
  • 如果导入是问题(这不太可能,但仍然是一个正当的反对意见),尝试减少代码就会证明这一点。众所周知,创建 MRE 通常可以解决大约 50% 的问题。你是对的,你不必让任何人开心,也不应该在意。但你是在提出问题并寻找答案;确保它获得尽可能多的观众(通过增加答案的可能性)不仅符合您的利益,而且还通过提供他们的知识和经验。
  • 同意不同意
  • 好吧,当然,我们不必同意,这是一件好事。但是,您仍然有(有趣的)问题,除了您自己提供的答案之外,几乎没有有用的答案。这是一个公共社区,一旦我们同意加入它,我们也必须同意它的(有时是不言而喻的、微妙的甚至是有争议的)规则。我们可能不喜欢他们,这是我们个人和绝对正确的观点。但这是一个公共空间,而不是我们的后院:如果我们发布问题或答案并受到批评,同时知道 那 是一个极有可能的结果,那么大肆宣扬它对我们没有任何帮助方式。

标签: pyqt5 qthread qprogressbar


【解决方案1】:

在 Kiwi Pycon 2019 上进行了一次启发性的演讲,帮助我发现了问题:"Python, Threads & Qt: Boom!"

  1. 每个QObject 都归QThread 所有
  2. QObject 实例不得跨线程共享
  3. QWidget 对象(即您可以“看到”的任何东西)不可重入。因此,它们只能从主 UI 线程调用。

第 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)

因为它不会创建新的模型对象,而是重用旧对象。因此,不幸的是,必须将代码移到单独的工作对象中。

正确的代码是:

  1. 将on_start 和on_finish 从ProgressDialog 移动到View(我将它们重命名为show_progress_dialog 和hide_progress_dialog)
  2. 创建将open_file 逻辑放在单独的QObject worker 中
  3. 自行调用view.progress_dialog.show()(thread 可以在发出thread.finished 时调用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()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-06-20
    • 2017-09-24
    • 2018-07-20
    • 2018-12-11
    • 1970-01-01
    • 1970-01-01
    • 2021-12-03
    相关资源
    最近更新 更多