ฉันจะรักษา GUI ที่ตอบสนองได้อย่างไรโดยใช้ QThread กับ PyQGIS


11

ฉันได้พัฒนาเครื่องมือการประมวลผลแบบแบตช์เป็นปลั๊กอินหลามสำหรับ QGIS 1.8

ฉันพบว่าในขณะที่เครื่องมือของฉันกำลังเรียกใช้ 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 และการประทับเวลาเหล่านี้บ่งชี้ว่าพวกเขาได้รับ "สด" ด้วยตัวนับนั่นคือพวกเขาไม่ได้ถูกจัดคิวโดยเธรดผู้ปฏิบัติงานหรือไดอะล็อก
  • ลำดับของข้อความในบันทึก (exerpt follow) บ่งชี้ว่า 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()

โครงสร้างของตัวอย่างนี้เป็นคลาส ThreadManagerDialog ที่เกินกว่าที่สามารถกำหนด WorkerThread (หรือคลาสย่อย) เมื่อเรียกใช้วิธีการโต้ตอบของมันจะเรียกวิธีการทำงานของพนักงาน ผลลัพธ์ก็คือโค้ดใด ๆ ใน doWork จะทำงานในเธรดแยกออกจากกันทำให้ GUI สามารถตอบสนองต่อการป้อนข้อมูลของผู้ใช้ได้

ในตัวอย่างนี้ตัวอย่างของ CounterThread ถูกกำหนดให้เป็นผู้ปฏิบัติงานและแถบความคืบหน้าสองสามอันจะไม่ว่างเป็นเวลาหนึ่งนาที

หมายเหตุ: สิ่งนี้ถูกจัดรูปแบบเพื่อให้พร้อมที่จะวางลงในคอนโซลหลาม สามบรรทัดสุดท้ายจะต้องถูกลบออกก่อนบันทึกเป็นไฟล์. py


นี่เป็นตัวอย่างแบบปลั๊กแอนด์เพลย์ที่ยอดเยี่ยม! ฉันอยากรู้ว่าตำแหน่งที่ดีที่สุดในรหัสนี้สำหรับการใช้งาน algorythmn ของเราเอง จะต้องมีการวาง emplaced ในคลาส WorkerThread หรือมากกว่าใน CounterThread ของคลาส def defWork [ถามเพื่อประโยชน์ในการเชื่อมต่อแถบความคืบหน้าเหล่านี้กับอัลกอริธึมผู้ปฏิบัติงานที่แทรกไว้]
Katalpa

ใช่เป็นเพียงกระดูกเปลือยตัวอย่างเช่นระดับของเด็กCounterThread WorkerThreadหากคุณสร้างชั้นเรียนสำหรับลูกของคุณเองด้วยการใช้งานที่มีความหมายมากกว่านี้doWorkคุณก็ควรจะปรับ
Kelly Thomas

ลักษณะของ CounterThread สามารถใช้ได้กับเป้าหมายของฉัน (การแจ้งเตือนโดยละเอียดถึงผู้ใช้ของความคืบหน้า) - แต่จะรวมเข้ากับรูทีน c.class 'doWork' ใหม่ได้อย่างไร (เช่น - การจัดวางอย่างชาญฉลาด 'doWork' ภายใน CounterThread ใช่มั้ย)
Katalpa

การติดตั้ง CounterThread ด้านบนก) เริ่มต้นงาน b) เริ่มต้นการโต้ตอบ c) ทำการวนรอบแกน d) ส่งคืนค่าจริงเมื่อสำเร็จการสำเร็จ งานใด ๆ ที่สามารถนำไปใช้กับการวนซ้ำได้ หนึ่งคำเตือนที่ฉันจะเสนอคือการส่งสัญญาณเพื่อสื่อสารกับผู้จัดการนั้นมาพร้อมกับค่าใช้จ่ายเช่นถ้าเรียกด้วยการวนซ้ำอย่างรวดเร็วของลูปทุกครั้งมันอาจทำให้เกิดความหน่วงแฝงมากกว่างานจริง
Kelly Thomas

ขอบคุณสำหรับคำแนะนำทั้งหมด อาจมีปัญหาสำหรับการทำงานในสถานการณ์ของฉัน ในปัจจุบัน doWork ทำให้เกิด minidump crash ใน qgis เป็นผลมาจากการโหลดที่มากเกินไปหรือทักษะการเขียนโปรแกรม (สามเณร) ของฉันหรือไม่
Katalpa
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.