PyQGIS와 함께 QThread를 사용하여 반응 형 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

답변:


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()

이 샘플의 구조는 WorkerThread (또는 서브 클래스)에 지정할 수있는 ThreadManagerDialog 클래스입니다. 대화 상자의 run 메소드가 호출되면 작업자의 doWork 메소드가 호출됩니다. 결과적으로 doWork의 모든 코드는 별도의 스레드에서 실행되므로 GUI는 사용자 입력에 자유롭게 응답 할 수 있습니다.

이 샘플에서는 CounterThread 인스턴스가 작업자로 지정되며 몇 개의 진행률 표시 줄이 1 분 정도 바쁘게 유지됩니다.

참고 : 이것은 파이썬 콘솔에 붙여 넣을 수 있도록 형식화되어 있습니다. .py 파일로 저장하기 전에 마지막 세 줄을 제거해야합니다.


이것은 훌륭한 플러그 앤 플레이 예제입니다! 우리 자신의 작업 algorythmn을 구현하기 위해이 코드에서 가장 좋은 위치에 대해 궁금합니다. WorkerThread 클래스에 배치해야합니까, 오히려 CounterThread 클래스에 def doWork를 배치해야합니까? [이 진행률 표시 줄을 삽입 된 작업자 알고리즘에 연결하기 위해 관심을
보임

예,의 CounterThread하위 클래스 예제입니다 WorkerThread. 보다 의미있는 구현으로 자신의 자식 클래스를 만들면 doWork괜찮을 것입니다.
Kelly Thomas

CounterThread의 특성은 내 목표 (진행 사용자에게 자세한 알림)에 적용 할 수 있지만 새로운 c.class 'doWork'루틴과 어떻게 통합됩니까? (또한-카운터
쓰레드

위의 CounterThread 구현은 a) 작업을 초기화합니다. b) 대화 상자를 초기화합니다. c) 코어 루프를 수행합니다. d) 성공적으로 완료되면 true를 반환합니다. 루프로 구현할 수있는 모든 작업은 제자리에 있어야합니다. 내가 제공 할 한 가지 경고는 관리자와 통신하기 위해 신호를 방출하면 약간의 오버 헤드가 발생한다는 것입니다. 즉, 빠른 루프가 반복 될 때마다 호출되면 실제 작업보다 대기 시간이 길어질 수 있습니다.
Kelly Thomas

모든 조언에 감사드립니다. 내 상황 에서이 작업이 번거로울 수 있습니다. 현재 doWork는 qgis에서 미니 덤프 충돌을 일으 킵니다. 너무 많은 부하 또는 나의 (초보자) 프로그래밍 기술의 결과?
카탈 파
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.