PyQt5: pop-up progressbar using QThread

穿精又带淫゛_ 提交于 2020-08-20 09:00:28

问题


How can I implement a progress bar in a pop-up window that monitors the progress of a running function from a so-called Worker class (i.e. time/CPU-consuming task) by means of a QThread?

I have checked countless examples and tutorials, but the fact that the progressbar shows up in a pop-up window seems to make everything harder. I believe what I want is a rather simple thing but I keep failing at it and I ran out of ideas.

I have an example of what I am trying to achieve, which is based on 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_())

When I run the latter (e.g. in PyCharm Community 2019.3), the program crashes and I don't get any clear error message.

When I debug it though, it looks like it works, as I am able to see what I intended to achieve:

I have a series of questions:

  1. Why does it crash?
  2. Why does it work during debugging?
  3. Sould I just give up and implement the progress bar (anchored) in the main window of the app?
  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?

I beg your pardon if there is something obvious that I am missing, or if this has been already be answered somewehere (duplicate): I am unable to find the answer to this problem. Thank you very much in advance.


回答1:


Explanation:

To understand the problem you must know that the following:

self.main_window_button.clicked.connect(PopUpProgressB)

Equivalent to:

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

Where it is observed that when pressing the button a PopUpProgressB object is created that does not have a life cycle just like the execution of the "foo" function that is practically instantaneous so the popup will be shown and hidden in a very short time.

Solution:

The idea is that the popup has a scope that allows it to have a life cycle large enough to show the progress for it should be made to the class attribute popup object.

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

And so that it doesn't show you must remove the call to the show() method of PopUpProgressB:

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

Since I already explained the failure of your problem I will answer your questions:

  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.


Finally, many of the errors that beginners report in Qt refer to the scope of the variables so I recommend you analyze how much it should be for each variable, for example if you want an object to live the same as the class then make that variable is an attribute of the class, if instead you only use it in a method then it is only a local variable, etc.




回答2:


Based on eyllanesc's answer, this is how a working code could look:

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_())


来源:https://stackoverflow.com/questions/59959562/pyqt5-pop-up-progressbar-using-qthread

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!