Como evito que o Qgis seja detectado como "não respondendo" ao executar um plugin pesado?

10

Eu uso a linha a seguir para informar o usuário sobre o status:

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

O plug-in leva cerca de 2 minutos para ser executado no meu conjunto de dados, mas o Windows o detecta como "não está respondendo" e para de mostrar as atualizações de status. Para um novo usuário, isso não é tão bom, pois parece que o programa travou.

Existe alguma solução alternativa para que o usuário não fique no escuro com relação ao status do plug-in?

Johan Holtby
fonte

Respostas:

13

Como Nathan W aponta, a maneira de fazer isso é com multithreading, mas a subclasse de QThread não é a melhor prática. Veja aqui: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Veja abaixo um exemplo de como criar um QObjecte mova-o para um QThread(ou seja, a maneira "correta" de fazê-lo). Este exemplo calcula a área total de todos os recursos em uma camada vetorial (usando a nova API do QGIS 2.0!).

Primeiro, criamos o objeto "worker" que fará o trabalho pesado para nós:

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)

Para usar o trabalhador, precisamos inicializá-lo com uma camada vetorial, mova-o para o encadeamento, conecte alguns sinais e inicie-o. Provavelmente, é melhor consultar o blog vinculado acima para entender o que está acontecendo aqui.

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

Este exemplo ilustra alguns pontos principais:

  • Tudo dentro do run()método do trabalhador está dentro de uma instrução try-except. É difícil recuperar quando seu código falha dentro de um thread. Emite o rastreio através do sinal de erro, ao qual costumo conectar QgsMessageLog.
  • O sinal finalizado informa ao método conectado se o processo foi concluído com êxito, bem como o resultado.
  • O sinal de progresso é chamado apenas quando a porcentagem concluída é alterada, em vez de uma vez para cada recurso. Isso evita muitas chamadas para atualizar a barra de progresso, retardando o processo do trabalhador, o que anularia todo o ponto de execução do trabalhador em outro encadeamento: para separar o cálculo da interface do usuário.
  • O trabalhador implementa um kill()método, que permite que a função termine normalmente. Não tente usar o terminate()método QThread- coisas ruins podem acontecer!

Certifique-se de acompanhar seus objetos threade workerem algum lugar da estrutura de plugins. Qt fica bravo se você não. A maneira mais fácil de fazer isso é armazená-los na sua caixa de diálogo quando você os cria, por exemplo:

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

Ou você pode deixar o Qt se apropriar do QThread:

thread = QtCore.QThread(self)

Levei muito tempo para desenterrar todos os tutoriais para montar este modelo, mas desde então eu tenho reutilizado tudo isso em todo o lugar.

Snorfalorpagus
fonte
Obrigado, isto era exatamente o que eu estava procurando e foi muito útil! Estou acostumado a threads em c #, mas não pensei nisso em python.
precisa
Sim, esta é a maneira correta.
Nathan W
1
Deveria haver um "eu". na frente da camada em "features = layer.getFeatures ()"? -> "features = self.layer.getFeatures ()"
Håvard Tveite
@ HåvardTveite Você está correto. Corrigi o código na resposta.
Snorfalorpagus
Estou tentando seguir esse padrão para um script de processamento que estou escrevendo e estou tendo problemas para fazê-lo funcionar. Tentei copiar este exemplo em um arquivo de script, adicionei as instruções de importação necessárias e mudei worker.progress.connect(self.ui.progressBar)para outra coisa, mas sempre que o executo, o qgis-bin está travando. Não tenho experiência em depurar código python ou qgis. Tudo o que estou recebendo é Access violation reading location 0x0000000000000008que parece que algo é nulo. Existe algum código de configuração ausente para poder usá-lo em um script de processamento?
TJ Rockefeller
4

Sua única maneira verdadeira de fazer isso é por multithreading.

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

Alguma leitura extra http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Nota Algumas pessoas não gostam de herdar o QThread e, aparentemente, essa não é a maneira "correta" de fazê-lo, mas funciona assim ....

Nathan W
fonte
:) Parece uma boa maneira suja de fazê-lo. Às vezes, o estilo não é necessário. Por esse tempo (o primeiro no pyqt), acho que seguirei da maneira correta, pois estou acostumado a isso em c #.
precisa
2
Não é uma maneira suja, era a maneira antiga de fazê-lo.
Nathan W
2

Como essa pergunta é relativamente antiga, merece uma atualização. Com o QGIS 3, há uma abordagem com QgsTask.fromFunction (), QgsProcessingAlgRunnerTask () e QgsApplication.taskManager (). AddTask ().

Mais sobre isso, por exemplo, em Usando Threads no PyQGIS3 POR MARCO BERNASOCCHI

Miro
fonte