PyQt5: всплывающая полоса прогресса с использованием QThread

Как я могу реализовать индикатор выполнения во всплывающем окне, которое отслеживает прогресс выполняемой функции из так называемого рабочего класса (т.е. задачи, потребляющей время / процессор) с помощью QThread?

Я проверил бесчисленное количество примеров и руководств, но тот факт, что индикатор выполнения отображается во всплывающем окне, кажется, все усложняет. Я считаю, что то, что я хочу, - это довольно простая вещь, но у меня все не получается, и у меня заканчиваются идеи.

У меня есть пример того, чего я пытаюсь достичь, основанный на этом ответе:

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), программа дает сбой, и я не получаю четкого сообщения об ошибке.

Однако, когда я отлаживаю его, похоже, что он работает, так как я могу видеть, чего я намеревался достичь:

У меня ряд вопросов:

  1. Почему вылетает?
  2. Почему работает при отладке?
  3. Могу ли я просто сдаться и реализовать индикатор выполнения (закрепленный) в главном окне приложения?
  4. Я уже реализовал подобную вещь в прошлом, но без потоковой передачи: в цикле рабочей функции (т.е. функции, потребляющей процессор) мне пришлось добавить QApplication.processEvents()так что на каждой итерации индикатор выполнения эффективно обновлялся. По-видимому, поступать таким образом неоптимально. Это все еще лучшая альтернатива тому, чего я пытаюсь достичь сейчас?

Прошу прощения, если есть что-то очевидное, чего я упускаю, или если на этот вопрос уже был дан ответ где-то (дубликат): Я не могу найти ответ на эту проблему. Заранее большое спасибо.

2 ответа

Решение

Пояснение:

Чтобы разобраться в проблеме, вы должны знать, что следующее:

self.main_window_button.clicked.connect(PopUpProgressB)

Эквивалентен:

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

При этом замечено, что при нажатии кнопки создается объект PopUpProgressB, у которого нет жизненного цикла, точно так же, как выполнение функции "foo", которое практически мгновенно, поэтому всплывающее окно будет отображаться и скрываться за очень короткое время.

Решение:

Идея состоит в том, что у всплывающего окна есть область действия, которая позволяет ему иметь достаточно большой жизненный цикл, чтобы показать прогресс, поскольку он должен быть выполнен для всплывающего объекта атрибута класса.

# ...
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)
# ...

И чтобы он не отображался, вы должны удалить вызов метода show() PopUpProgressB:

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

Поскольку я уже объяснил, в чем проблема вашей проблемы, отвечу на ваши вопросы:

  1. Почему вылетает? Когда всплывающий объект удаляется, созданный QThread также удаляется, но Qt обращается к уже не выделенной памяти (дамп памяти), в результате чего приложение закрывается без каких-либо исключений.

  2. Почему работает при отладке? Многие IDE, такие как PyCharm, не обрабатывают ошибки Qt, поэтому IMHO рекомендует, чтобы при возникновении таких ошибок они выполняли свой код в терминале /CMD, например, когда я выполняю ваш код, который я получил:

    QThread: Destroyed while thread is still running
    Aborted (core dumped)
    
  3. Должен ли я просто сдаться и реализовать индикатор выполнения (закрепленный) в главном окне приложения? Нет.

  4. Я уже реализовал аналогичную вещь в прошлом, но без потоковой передачи: в цикле рабочей функции (т.е. функции, потребляющей процессор) мне пришлось добавить QApplication.processEvents(), чтобы на каждой итерации индикатор выполнения эффективно обновлялся. По-видимому, поступать таким образом неоптимально. Это все еще лучшая альтернатива тому, чего я пытаюсь достичь сейчас? Не используйте QApplication::processEvents(), если есть лучшие альтернативы, в этом случае потоки являются лучшими, поскольку они делают основной поток менее загруженным.


Наконец, многие ошибки, о которых новички сообщают в Qt, относятся к области действия переменных, поэтому я рекомендую вам проанализировать, сколько это должно быть для каждой переменной, например, если вы хотите, чтобы объект жил так же, как класс, тогда сделайте эту переменную является атрибутом класса, если вместо этого вы используете его только в методе, тогда это только локальная переменная и т. д.

Основываясь на ответе Эйлланеска, вот как может выглядеть рабочий код:

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_())
Другие вопросы по тегам