PyQt5:使用QThread弹出进度条

4
如何在弹出窗口中实现进度条,在所谓的Worker类(即时间/CPU消耗任务)中,通过QThread监视正在运行的函数的进度?
我已经查看了无数的示例和教程,但是进度条出现在弹出窗口中似乎让事情变得更加困难。我相信我想要的是一件相当简单的事情,但是我一直失败,并且我已经没有了主意。
我有一个我想要实现的示例,它基于this answer
import sys
import time
from PyQt5.QtCore import QThread, pyqtSignal, QObject, pyqtSlot
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout, QProgressBar, QVBoxLayout


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Widget")
        self.h_box = QHBoxLayout(self)
        self.main_window_button = QPushButton("Start")
        self.main_window_button.clicked.connect(PopUpProgressB)
        self.h_box.addWidget(self.main_window_button)
        self.setLayout(self.h_box)
        self.show()


class Worker(QObject):
    finished = pyqtSignal()
    intReady = pyqtSignal(int)

    @pyqtSlot()
    def proc_counter(self):  # A slot takes no params
        for i in range(1, 100):
            time.sleep(1)
            self.intReady.emit(i)

        self.finished.emit()


class PopUpProgressB(QWidget):

    def __init__(self):
        super().__init__()
        self.pbar = QProgressBar(self)
        self.pbar.setGeometry(30, 40, 500, 75)
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.pbar)
        self.setLayout(self.layout)
        self.setGeometry(300, 300, 550, 100)
        self.setWindowTitle('Progress Bar')
        self.show()

        self.obj = Worker()
        self.thread = QThread()
        self.obj.intReady.connect(self.on_count_changed)
        self.obj.moveToThread(self.thread)
        self.obj.finished.connect(self.thread.quit)
        self.thread.started.connect(self.obj.proc_counter)
        self.thread.start()

    def on_count_changed(self, value):
        self.pbar.setValue(value)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_window = MainWindow()
    sys.exit(app.exec_())

当我运行后者时(例如在PyCharm Community 2019.3中),程序会崩溃,我没有得到任何清晰的错误信息。
然而,当我进行调试时,它似乎是有效的,因为我能够看到我想要实现的内容: App working during debugging 我有一系列问题:
1. 为什么会崩溃? 2. 为什么在调试期间可以工作? 3. 我应该放弃,在应用程序的主窗口中实现进度条(固定)吗? 4. 我已经在过去实现了类似的东西,但没有使用线程:在工作函数的循环内部(即CPU消耗函数),我必须添加QApplication.processEvents(),以便在每次迭代时有效地更新进度条。这显然不是最佳方式。这仍然比我现在尝试实现的更好吗?

如果我忽略了一些显而易见的东西,或者这个问题已经在其他地方得到了解答(重复):我找不到这个问题的答案。非常感谢您提前的帮助。

2个回答

8

说明:

为了理解问题,您必须了解以下内容:

self.main_window_button.clicked.connect(PopUpProgressB)

等同于:

self.main_window_button.clicked.connect(foo)
# ...
def foo():
    PopUpProgressB()

观察到按下按钮时会创建一个名为PopUpProgressB的对象,它的生命周期就像执行“foo”函数一样短暂,所以弹出窗口会在很短的时间内显示和隐藏。

解决方案:

解决方法是将弹出窗口的作用域扩大到足够长,以便展示进度。应该将popup对象制作为类属性。

# ...
self.main_window_button = QPushButton("Start")
<b>self.popup = PopUpProgressB()
self.main_window_button.clicked.connect(self.popup.show)</b>
self.h_box.addWidget(self.main_window_button)
# ...

为了使其不显示,您必须删除 PopUpProgressB 的 show() 方法的调用:

class PopUpProgressB(QWidget):
    def __init__(self):
        super().__init__()
        # ...
        self.setWindowTitle('Progress Bar')
        <b># self.show() # <--- remove this line</b>
        self.obj = Worker()
        # ...

自从我已经解释了问题的失败,我将回答你的问题:
  1. Why does it crash? When the popup object is deleted, the created QThread is also deleted but Qt accesses no longer allocated memory (core dumped) causing the application to close without throwing any exceptions.

  2. Why does it work during debugging? Many IDEs like PyCharm do not handle Qt errors, so IMHO recommends that when they have such errors they execute their code in the terminal/CMD, for example when I execute your code I obtained:

    QThread: Destroyed while thread is still running
    Aborted (core dumped)
    
  3. Should I just give up and implement the progress bar (anchored) in the main window of the app? No.

  4. I already implemented a similar thing in the past but without threading: within the loop of the worker function (i.e. CPU-consuming function) I had to add QApplication.processEvents() so that at each iteration the progressbar was effectively updated. It is apparently suboptimal to do things this way. Is it still a better alternative to what I am trying to achieve now? Do not use QApplication::processEvents() if there are better alternatives, in this case the threads is the best since it makes the main thread less busy.


最后,初学者在使用Qt时报告的许多错误都涉及变量的作用域,因此我建议您分析每个变量应该具有的作用域,例如,如果您希望一个对象与类一样持续存在,那么将该变量设置为类的属性;如果您只在方法中使用它,则它只是一个局部变量等等。


你的解决方案非常完美!非常感谢您的优秀和详细的答案!我只是觉得可惜,每次点击self.main_window_button时,我不能只创建class PopUpProgressB的一个实例,因为在class MainWindow中已经有了一个实例,这意味着在进度完成后我必须手动处理它,以便self.popup被隐藏并且进度被重置,通过在class PopUpProgressB中的一个函数中运行self.show()self.thread.start()来实现,该函数在每次按下按钮时都会调用。 - rubebop
1
@rubebop 是的,你可以这样做。在你的问题中没有明确指出这个要求,所以我没有把重点放在上面。等我有时间了,我会补充那部分内容。 - eyllanesc
是的,你说得对,我没有指定它。我用基于你的解决方案适合我的代码回答了我的问题。不要犹豫,让我知道你的想法:也许有更好或替代的方法来做到这一点。 - rubebop
只有当你有时间的时候,我会非常有兴趣看到我提到的内容的一个可工作实现:“每次单击self.main_window_button时创建一个PopUpProgressB类的实例”。非常感谢您的帮助。 - rubebop

5

根据eyllanesc的答案,以下是一个可行的代码示例:

import sys
import time
from PyQt5.QtCore import QThread, pyqtSignal, QObject, pyqtSlot
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QHBoxLayout, QProgressBar, QVBoxLayout


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Widget")
        self.h_box = QHBoxLayout(self)
        self.main_window_button = QPushButton("Start")
        self.popup = PopUpProgressB()  # Creating an instance instead as an attribute instead of creating one 
        # everytime the button is pressed 
        self.main_window_button.clicked.connect(self.popup.start_progress)  # To (re)start the progress
        self.h_box.addWidget(self.main_window_button)
        self.setLayout(self.h_box)
        self.show()


class Worker(QObject):
    finished = pyqtSignal()
    intReady = pyqtSignal(int)

    @pyqtSlot()
    def proc_counter(self):  # A slot takes no params
        for i in range(1, 100):
            time.sleep(0.1)
            self.intReady.emit(i)

        self.finished.emit()


class PopUpProgressB(QWidget):

    def __init__(self):
        super().__init__()
        self.pbar = QProgressBar(self)
        self.pbar.setGeometry(30, 40, 500, 75)
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.pbar)
        self.setLayout(self.layout)
        self.setGeometry(300, 300, 550, 100)
        self.setWindowTitle('Progress Bar')
        # self.show()

        self.obj = Worker()
        self.thread = QThread()
        self.obj.intReady.connect(self.on_count_changed)
        self.obj.moveToThread(self.thread)
        self.obj.finished.connect(self.thread.quit)
        self.obj.finished.connect(self.hide)  # To hide the progress bar after the progress is completed
        self.thread.started.connect(self.obj.proc_counter)
        # self.thread.start()  # This was moved to start_progress

    def start_progress(self):  # To restart the progress every time
        self.show()
        self.thread.start()

    def on_count_changed(self, value):
        self.pbar.setValue(value)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_window = MainWindow()
    sys.exit(app.exec_())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接