Как мне поддерживать Resposive GUI, используя QThread с PyQGIS

11

Я разрабатывал некоторые инструменты пакетной обработки в виде плагинов Python для QGIS 1.8.

Я обнаружил, что когда мои инструменты работают, графический интерфейс перестает отвечать на запросы.

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

Я прочитал документы по берегам реки и изучил источник doGeometry.py (рабочая реализация из ftools ).

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

Общая структура - это запись в меню плагинов, которая запускает диалог с кнопками запуска и остановки. Кнопки управляют потоком, который считает до 100, посылая сигнал обратно в GUI для каждого номера. GUI получает каждый сигнал и отправляет строку, содержащую номер как журнала сообщений, так и заголовка окна.

Код этой реализации находится здесь:

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *

class ThreadTest:

    def __init__(self, iface):
        self.iface = iface

    def initGui(self):
        self.action = QAction( u"ThreadTest", self.iface.mainWindow())
        self.action.triggered.connect(self.run)
        self.iface.addPluginToMenu(u"&ThreadTest", self.action)

    def unload(self):
        self.iface.removePluginMenu(u"&ThreadTest",self.action)

    def run(self):
        BusyDialog(self.iface.mainWindow())

class BusyDialog(QDialog):
    def __init__(self, parent):
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setLayout(QVBoxLayout())
        self.startButton = QPushButton("Start", self)
        self.startButton.clicked.connect(self.startButtonHandler)
        self.layout().addWidget(self.startButton)
        self.stopButton=QPushButton("Stop", self)
        self.stopButton.clicked.connect(self.stopButtonHandler)
        self.layout().addWidget(self.stopButton)
        self.show()

    def startButtonHandler(self, toggle):
        self.workerThread = WorkerThread(self.parent)
        QObject.connect( self.workerThread, SIGNAL( "killThread(PyQt_PyObject)" ), \
                                                self.killThread )
        QObject.connect( self.workerThread, SIGNAL( "echoText(PyQt_PyObject)" ), \
                                                self.setText)
        self.workerThread.start(QThread.LowestPriority)
        QgsMessageLog.logMessage("end: startButtonHandler")

    def stopButtonHandler(self, toggle):
        self.killThread()

    def setText(self, text):
        QgsMessageLog.logMessage(str(text))
        self.setWindowTitle(text)

    def killThread(self):
        if self.workerThread.isRunning():
            self.workerThread.exit(0)


class WorkerThread(QThread):
    def __init__(self, parent):
        QThread.__init__(self,parent)

    def run(self):
        self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: starting work" )
        self.doLotsOfWork()
        self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: finshed work" )
        self.emit( SIGNAL( "killThread(PyQt_PyObject)"), "OK")

    def doLotsOfWork(self):
        count=0
        while count < 100:
            self.emit( SIGNAL( "echoText(PyQt_PyObject)" ), "Emit: " + str(count) )
            count += 1
#           if self.msleep(10):
#               return
#          QThread.yieldCurrentThread()

К сожалению, работать не так тихо, как я надеялся:

  • Заголовок окна обновляется «вживую» со счетчиком, но если я нажимаю на диалоговое окно, оно не отвечает.
  • Журнал сообщений неактивен до тех пор, пока не закончится счетчик, а затем представляет все сообщения одновременно. Эти сообщения помечаются меткой времени с помощью QgsMessageLog, и эти метки времени указывают, что они были получены «вживую» со счетчиком, т.е. они не помещаются в очередь ни рабочим потоком, ни диалогом.
  • Порядок сообщений в журнале (ниже следует отрывок) указывает, что startButtonHandler завершает выполнение до того, как рабочий поток начинает работу, т.е. поток ведет себя как поток.

    end: startButtonHandler
    Emit: starting work
    Emit: 0
    ...
    Emit: 99
    Emit: finshed work
    
  • Кажется, рабочий поток просто не делит ресурсы с потоком GUI. В конце вышеприведенного источника есть пара закомментированных строк, где я пытался вызвать msleep () и yieldCurrentThread (), но ни одна из них не помогла.

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

Келли Томас
источник
Это нормально, что на кнопку остановки нельзя нажать? Основная цель адаптивного графического интерфейса - отменить процесс, если он слишком длинный. Я пытаюсь изменить ваш скрипт, но не могу заставить кнопку работать должным образом. Как вы прерываете свою тему?
etrimaille

Ответы:

6

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

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

from PyQt4.QtCore import *
from PyQt4.QtGui import *

class ThreadManagerDialog(QDialog):
    def __init__( self, iface, title="Worker Thread"):
        QDialog.__init__( self, iface.mainWindow() )
        self.iface = iface
        self.setWindowTitle(title)
        self.setLayout(QVBoxLayout())
        self.primaryLabel = QLabel(self)
        self.layout().addWidget(self.primaryLabel)
        self.primaryBar = QProgressBar(self)
        self.layout().addWidget(self.primaryBar)
        self.secondaryLabel = QLabel(self)
        self.layout().addWidget(self.secondaryLabel)
        self.secondaryBar = QProgressBar(self)
        self.layout().addWidget(self.secondaryBar)
        self.closeButton = QPushButton("Close")
        self.closeButton.setEnabled(False)
        self.layout().addWidget(self.closeButton)
        self.closeButton.clicked.connect(self.reject)
    def run(self):
        self.runThread()
        self.exec_()
    def runThread( self):
        QObject.connect( self.workerThread, SIGNAL( "jobFinished( PyQt_PyObject )" ), self.jobFinishedFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryValue( PyQt_PyObject )" ), self.primaryValueFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryRange( PyQt_PyObject )" ), self.primaryRangeFromThread )
        QObject.connect( self.workerThread, SIGNAL( "primaryText( PyQt_PyObject )" ), self.primaryTextFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryValue( PyQt_PyObject )" ), self.secondaryValueFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryRange( PyQt_PyObject )" ), self.secondaryRangeFromThread )
        QObject.connect( self.workerThread, SIGNAL( "secondaryText( PyQt_PyObject )" ), self.secondaryTextFromThread )
        self.workerThread.start()
    def cancelThread( self ):
        self.workerThread.stop()
    def jobFinishedFromThread( self, success ):
        self.workerThread.stop()
        self.primaryBar.setValue(self.primaryBar.maximum())
        self.secondaryBar.setValue(self.secondaryBar.maximum())
        self.emit( SIGNAL( "jobFinished( PyQt_PyObject )" ), success )
        self.closeButton.setEnabled( True )
    def primaryValueFromThread( self, value ):
        self.primaryBar.setValue(value)
    def primaryRangeFromThread( self, range_vals ):
        self.primaryBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )
    def primaryTextFromThread( self, value ):
        self.primaryLabel.setText(value)
    def secondaryValueFromThread( self, value ):
        self.secondaryBar.setValue(value)
    def secondaryRangeFromThread( self, range_vals ):
        self.secondaryBar.setRange( range_vals[ 0 ], range_vals[ 1 ] )
    def secondaryTextFromThread( self, value ):
        self.secondaryLabel.setText(value)

class WorkerThread( QThread ):
    def __init__( self, parentThread):
        QThread.__init__( self, parentThread )
    def run( self ):
        self.running = True
        success = self.doWork()
        self.emit( SIGNAL( "jobFinished( PyQt_PyObject )" ), success )
    def stop( self ):
        self.running = False
        pass
    def doWork( self ):
        return True
    def cleanUp( self):
        pass

class CounterThread(WorkerThread):
    def __init__(self, parentThread):
        WorkerThread.__init__(self, parentThread)
    def doWork(self):
        target = 100000000
        stepP= target/100
        stepS=target/10000
        self.emit( SIGNAL( "primaryText( PyQt_PyObject )" ), "Primary" )
        self.emit( SIGNAL( "secondaryText( PyQt_PyObject )" ), "Secondary" )
        self.emit( SIGNAL( "primaryRange( PyQt_PyObject )" ), ( 0, 100 ) )
        self.emit( SIGNAL( "secondaryRange( PyQt_PyObject )" ), ( 0, 100 ) )
        count = 0
        while count < target:
            if count % stepP == 0:
                self.emit( SIGNAL( "primaryValue( PyQt_PyObject )" ), int(count / stepP) )
            if count % stepS == 0:  
                self.emit( SIGNAL( "secondaryValue( PyQt_PyObject )" ), count % stepP / stepS )
            if not self.running:
                return False
            count += 1
        return True

d = ThreadManagerDialog(qgis.utils.iface, "CounterThread Demo")
d.workerThread = CounterThread(qgis.utils.iface.mainWindow())
d.run()

Структура этого примера представляет собой класс ThreadManagerDialog, которому можно присвоить WorkerThread (или подкласс). Когда вызывается метод run диалога, он, в свою очередь, вызывает метод doWork на рабочем месте. В результате любой код в doWork будет выполняться в отдельном потоке, оставляя графический интерфейс свободным для ответа на ввод пользователя.

В этом примере экземпляр CounterThread назначается рабочим, и пара индикаторов будет занята в течение минуты или около того.

Примечание: это отформатировано так, что оно готово для вставки в консоль python. Последние три строки необходимо удалить перед сохранением в файл .py.

Келли Томас
источник
Это отличный пример подключи и играй! Мне любопытно, какая лучшая позиция в этом коде для реализации нашего собственного алгоритма работы. Должно ли такое быть включено в класс WorkerThread или, скорее, в класс CounterThread, def doWork? [Отвечает на вопрос о подключении этих индикаторов выполнения к введенному рабочему алгоритму (ам)]
Катальпа
Ага, CounterThreadэто просто голый пример детского класса WorkerThread. Если вы создаете свой собственный класс ребенка с более значимой реализацией , doWorkто вы должны быть хорошо.
Келли Томас,
Характеристики CounterThread применимы к моей цели (подробные уведомления пользователя о прогрессе) - но как это будет интегрировано с новой подпрограммой c.class 'doWork'? (также - мудрое размещение, «doWork» в правом
CounterThread
Реализация CounterThread выше a) инициализирует задание, b) инициализирует диалог, c) выполняет цикл ядра, d) возвращает true при успешном завершении. Любая задача, которая может быть реализована с помощью цикла, должна просто встать на место. Одно предупреждение, которое я предложу, состоит в том, что передача сигналов для связи с менеджером сопряжена с некоторыми накладными расходами, т. Е. При вызове при каждой итерации быстрого цикла это может вызвать большую задержку, чем фактическая работа.
Келли Томас
Спасибо за все советы. Может быть неприятно, чтобы это работало в моей ситуации. В настоящее время doWork вызывает сбой минидампа в qgis. В результате слишком большой нагрузки или моих (начинающих) навыков программирования?
Катальпа