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 14

回答:


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のインスタンスがワーカーとして割り当てられ、2、3のプログレスバーが1分間ほどビジーになります。

注:これは、Pythonコンソールに貼り付けられるようにフォーマットされています。.pyファイルに保存する前に、最後の3行を削除する必要があります。


これは素晴らしいプラグアンドプレイの例です!独自の作業アルゴリズムを実装するためのこのコードの最適な位置について興味があります。そのようなクラスはWorkerThreadクラスに配置する必要がありますか、それともCounterThreadクラス、def doWorkに配置する必要がありますか?[挿入ワーカーアルゴリズム(複数可)に、これらのプログレスバーを接続するの関心に尋ね]
Katalpa

ええ、CounterThread単なる子クラスの子クラスですWorkerThread。より意味のある実装を使用して独自の子クラスを作成するdoWork場合は、問題ありません。
ケリートーマス14

CounterThreadの特性は私の目標(進行状況のユーザーへの詳細な通知)に適用できますが、新しいc.class 'doWork'ルーチンとどのように統合されますか?(また、配置の賢明さ、CounterThread内での 'doWork'の権利?)
Katalpa 14

上記のCounterThread実装は、a)ジョブを初期化、b)ダイアログを初期化、c)コアループを実行、d)正常終了時にtrueを返します。ループで実装できるタスクは、適切な場所にドロップするだけです。私が提供する警告の1つは、マネージャーと通信するためのシグナルを送信するとオーバーヘッドが発生することです。つまり、高速ループの各反復で呼び出されると、実際のジョブよりも多くのレイテンシーが発生する可能性があります。
ケリートーマス14

すべてのアドバイスをありがとう。これが私の状況で機能するのは面倒かもしれません。現在、doWorkはqgisでミニダンプクラッシュを引き起こします。負荷が大きすぎる結果、または私の(初心者)プログラミングスキルですか?
カタパル14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.