Как предотвратить обнаружение Qgis как «не отвечающего» при запуске тяжелого плагина?

10

Я использую следующую строку, чтобы сообщить пользователю о статусе:

iface.mainWindow().statusBar().showMessage("Status:" + str(i))

Плагину требуется около 2 минут для запуска моего набора данных, но Windows обнаруживает его как «не отвечающий» и перестает показывать обновления статуса. Для нового пользователя это не так хорошо, так как похоже, что программа потерпела крах.

Есть ли обходной путь, чтобы пользователь не оставался в неведении относительно статуса плагина?

Йохан Холтби
источник

Ответы:

13

Как отмечает Натан У , способ сделать это - использовать многопоточность, но использование подклассов QThread не является лучшей практикой. Смотрите здесь: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Ниже приведен пример того, как создать, а QObjectзатем переместить его в QThread(т. Е. «Правильный» способ сделать это). В этом примере вычисляется общая площадь всех объектов векторного слоя (с использованием нового API QGIS 2.0!).

Сначала мы создаем «рабочий» объект, который сделает за нас тяжелую работу:

class Worker(QtCore.QObject):
    def __init__(self, layer, *args, **kwargs):
        QtCore.QObject.__init__(self, *args, **kwargs)
        self.layer = layer
        self.total_area = 0.0
        self.processed = 0
        self.percentage = 0
        self.abort = False

    def run(self):
        try:
            self.status.emit('Task started!')
            self.feature_count = self.layer.featureCount()
            features = self.layer.getFeatures()
            for feature in features:
                if self.abort is True:
                    self.killed.emit()
                    break
                geom = feature.geometry()
                self.total_area += geom.area()
                self.calculate_progress()
            self.status.emit('Task finished!')
        except:
            import traceback
            self.error.emit(traceback.format_exc())
            self.finished.emit(False, self.total_area)
        else:
            self.finished.emit(True, self.total_area)

    def calculate_progress(self):
        self.processed = self.processed + 1
        percentage_new = (self.processed * 100) / self.feature_count
        if percentage_new > self.percentage:
            self.percentage = percentage_new
            self.progress.emit(self.percentage)

    def kill(self):
        self.abort = True

    progress = QtCore.pyqtSignal(int)
    status = QtCore.pyqtSignal(str)
    error = QtCore.pyqtSignal(str)
    killed = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal(bool, float)

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

thread = QtCore.QThread()
worker = Worker(layer)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.progress.connect(self.ui.progressBar)
worker.status.connect(iface.mainWindow().statusBar().showMessage)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
worker.finished.connect(thread.quit)
thread.start()

Этот пример иллюстрирует несколько ключевых моментов:

  • Все в run()методе работника находится внутри оператора try-кроме. Трудно восстановить, когда ваш код падает в потоке. Он генерирует трассировку через сигнал ошибки, который я обычно подключаю к QgsMessageLog.
  • Готовый сигнал сообщает подключенному методу, если процесс завершен успешно, а также результат.
  • Сигнал прогресса вызывается только при изменении процента выполнения, а не один раз для каждой функции. Это предотвращает слишком большое количество вызовов для обновления индикатора выполнения, замедляя рабочий процесс, что лишает смысла запускать рабочий в другом потоке: отделять вычисления от пользовательского интерфейса.
  • Работник реализует kill()метод, который позволяет функции завершить изящно. Не пытайтесь использовать terminate()метод в QThread- плохие вещи могут случиться!

Обязательно отслеживайте ваши объекты threadи workerобъекты где-то в структуре вашего плагина. Qt рассердится, если вы этого не сделаете. Самый простой способ сделать это - сохранить их в диалоговом окне при их создании, например:

thread = self.thread = QtCore.QThread()
worker = self.worker = Worker(layer)

Или вы можете позволить Qt стать владельцем QThread:

thread = QtCore.QThread(self)

Мне потребовалось много времени, чтобы выкопать все учебники, чтобы собрать этот шаблон вместе, но с тех пор я использовал его повсеместно.

Snorfalorpagus
источник
Спасибо, это было именно то, что я искал, и это было очень полезно! Я привык к потокам я C #, но не думал об этом в Python.
Йохан Холтби
Да, это правильный путь.
Натан W
1
Должно ли быть «я». перед слоем в "features = layer.getFeatures ()"? -> "features = self.layer.getFeatures ()"
Håvard Tveite
@ HåvardTveite Вы правы. Я исправил код в ответе.
Снорфалорпаг
Я пытаюсь следовать этому шаблону для сценария обработки, который я пишу, и у меня возникают проблемы с его работой. Я попытался скопировать этот пример в файл сценария, добавил необходимые операторы импорта и изменил worker.progress.connect(self.ui.progressBar)на что-то другое, но каждый раз, когда я запускаю его, qgis-bin падает. У меня нет опыта отладки кода на Python или qgis. Все, что я получаю, Access violation reading location 0x0000000000000008так это, кажется, что-то ноль. Есть ли какой-то код настройки, который отсутствует, чтобы можно было использовать это в сценарии обработки?
TJ Рокфеллер
4

Ваш единственный верный способ сделать это - многопоточность.

class MyLongRunningStuff(QThread):
    progressReport = pyqtSignal(str)
    def __init__(self):
       QThread.__init__(self)

    def run(self):
       # do your long runnning thing
       self.progressReport.emit("I just did X")

 thread = MyLongRunningStuff()
 thread.progressReport.connect(self.updatetheuimethod)
 thread.start()

Некоторое дополнительное чтение http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Примечание. Некоторые люди не любят наследовать от QThread, и, видимо, это не «правильный» способ сделать это, но он работает так…

Натан У
источник
:) Похоже, хороший грязный способ сделать это. Иногда стиль не нужен. На этот раз (первый в pyqt) я думаю, что пойду правильным путем, так как я привык к этому в C #.
Йохан Холтби
2
Это не грязный способ, это был старый способ сделать это.
Натан W