Як я підтримую репозитивний графічний інтерфейс, використовуючи 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 () та returnCurrentThread (), але ні один з них не допоміг.

Хто-небудь, хто має досвід цього, може помітити мою помилку? Я сподіваюся, що це проста, але принципова помилка, яку легко виправити, як тільки вона виявиться.


Чи нормально, що не можна натиснути кнопку зупинки? Основна мета чуйного графічного інтерфейсу - скасувати процес, якщо він занадто довгий. Я намагаюся змінити ваш сценарій, але не можу заставити кнопку працювати належним чином. Як ви перериваєте свою нитку?
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 (або підклас). Коли буде викликаний метод запуску діалогового вікна, він, у свою чергу, викликає метод doWork на робочому. Результат полягає в тому, що будь-який код у doWork працюватиме в окремій потоці, залишаючи GUI вільним реагувати на введення користувача.

У цьому зразку екземпляр CounterThread присвоюється робочим, і пара смужок прогресу буде зайнята протягом хвилини або близько того.

Примітка: це відформатовано так, щоб воно було готове вставити в консоль пітона. Останні три рядки потрібно буде видалити перед збереженням у файл .py.


Це чудовий приклад підключення та гри! Мені цікаво найкраще положення в цьому коді для реалізації нашої власної робочої алгоритми. Чи потрібно таке розміщувати в класі WorkerThread, а точніше в класі CounterThread, def doWork? [Запитано в інтересах підключити ці смуги прогресу до вставленого алгоритму робітника]
Katalpa

Так, CounterThreadце просто голі кістки, наприклад, дитячий клас WorkerThread. Якщо ви створюєте власний дочірній клас із більш змістовною реалізацією, doWorkтоді вам слід добре.
Келлі Томас

Характеристики CounterThread застосовні до моєї цілі (детальне сповіщення користувача про прогрес) - але як би таке було інтегровано з новим порядком c.class 'doWork'? (також - розміщення мудре, 'doWork' в CounterThread right?)
Katalpa

Реалізація CounterThread вище a) ініціалізує завдання, б) ініціалізує діалогове вікно, в) виконує цикл ядра, г) повертає істину після успішного завершення. Будь-яке завдання, яке можна реалізувати за допомогою циклу, повинно просто впасти на місце. Одне попередження, яке я запропоную, - це те, що випромінювання сигналів для спілкування з менеджером відбувається з деякими накладними витратами, тобто, якщо викликається з кожною ітерацією швидкого циклу, це може спричинити більше затримок, ніж фактична робота.
Келлі Томас

Дякую за всі поради. Це може бути клопітно для роботи в моїй ситуації. Наразі doWork спричиняє крах мінідуму в qgis. Результат занадто великого навантаження чи мої навички (початківця) програмування?
Катальпа
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.