PyQt5: всплывающая панель прогресса с использованием QThread - PullRequest
1 голос
/ 29 января 2020

Как я могу реализовать индикатор выполнения во всплывающем окне , которое отслеживает ход выполнения функции из так называемого класса Worker (т. Е. Требует много времени / ресурсов процессора) задача) с помощью 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), программа вылетает, и я не получаю четкого сообщения об ошибке.

При отладке хотя, похоже, это работает, так как я вижу, чего я хотел достичь: App working during debugging

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

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

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

Ответы [ 2 ]

3 голосов
/ 29 января 2020

Объяснение:

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

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")
<b>self.popup = PopUpProgressB()
self.main_window_button.clicked.connect(self.popup.show)</b>
self.h_box.addWidget(self.main_window_button)
# ...

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

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

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

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

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

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

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


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

0 голосов
/ 30 января 2020

Исходя из ответа eyllanes c , рабочий код может выглядеть так:

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