Как мне поддерживать 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. В результате слишком большой нагрузки или моих (начинающих) навыков программирования?
— Катальпа
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.