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