我如何使用QThread和PyQGIS来维护可重复使用的GUI


11

我一直在开发一些批处理工具作为QGIS 1.8的python插件。

我发现在我的工具运行时,GUI变得无响应。

一般的看法是,应该在工作线程上完成工作,并将状态/完成信息作为信号传递回GUI。

我已经通过阅读河岸文档,并研究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(),但似乎都没有帮助。

有任何经验的人都能发现我的错误吗?我希望这是一个简单但基本的错误,一旦被识别,就可以轻松纠正。


无法单击停止按钮是正常现象吗?响应式GUI的主要目标是,如果过程太长,则取消该过程。我尝试修改您的脚本,但无法正常使用该按钮。您如何中止线程?
etrimaille 2014年

Answers:


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中的任何代码都将在单独的线程中运行,而GUI可以自由响应用户输入。

在此示例中,将CounterThread实例分配为工作程序,并且几个进度条将保持繁忙一分钟左右。

注意:此文件经过格式化,可以随时粘贴到python控制台中。在保存为.py文件之前,需要删除最后三行。


这是一个很好的即插即用示例!我很好奇此代码中实现我们自己的工作算法的最佳位置。是否需要将此类放置在WorkerThread类中,或更确切地说应将其放置在CounterThread,def doWork类中?[
出于

是的,CounterThread仅是的子类WorkerThread。如果您使用更有意义的实现创建自己的子类,doWork则应该没问题。
凯利·托马斯

CounterThread的特性适用于我的目标(向进度用户的详细通知)-但是如何将其与新的c.class'doWork'例程集成?(也是-明智的放置方式,在CounterThread中执行“ doWork”对吗?)
Katalpa 2014年

上面的CounterThread实现a)初始化作业,b)初始化对话框,c)执行核心循环,d)成功完成后返回true。可以通过循环执行的任何任务都应该放在适当的位置。我将提供的一个警告是,发出与管理器进行通信的信号会带来一些开销,即,如果每次快速循环迭代都调用它,可能会导致比实际作业更多的延迟。
凯利·托马斯

感谢所有的建议。在我的情况下,这样做可能会很麻烦。目前,doWork在qgis中导致小型转储崩溃。是因为负担太重,还是我的(新手)编程技巧?
Katalpa 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.