PyQGIS ile QThread kullanarak resposive GUI'yi nasıl koruyabilirim?


11

QGIS 1.8 için bazı toplu işlem araçlarını python eklentileri olarak geliştiriyorum.

Araçlarım çalışırken GUI yanıt vermiyor.

Genel bilgelik, işin bir iş parçacığında yapılması gerektiğidir, durum / tamamlanma bilgileri GUI'ye sinyal olarak geri gönderilir.

İçinden okudum nehir docs ve doGeometry.py kaynağını (bir çalışma uygulaması çalışılan ftools ).

Bu kaynakları kullanarak, yerleşik bir kod tabanında değişiklik yapmadan önce bu işlevselliği keşfetmek için basit bir uygulama oluşturmaya çalıştım.

Genel yapı, eklentiler menüsündeki başlat ve durdur düğmelerini içeren bir iletişim kutusu içeren bir giriştir. Düğmeler, her sayı için GUI'ye bir sinyal göndererek 100'e kadar olan bir iş parçacığını kontrol eder. GUI her sinyali alır ve hem mesaj günlüğünü hem de pencere başlığını içeren bir dize gönderir.

Bu uygulamanın kodu burada:

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

Ne yazık ki umduğum gibi sessiz çalışma:

  • Pencere başlığı sayaçla "canlı" olarak güncelleniyor, ancak iletişim kutusunu tıklarsam yanıt vermez.
  • Mesaj günlüğü sayaç sona erene kadar devre dışı kalır, ardından tüm mesajları bir kerede sunar. Bu mesajlar QgsMessageLog tarafından bir zaman damgası ile etiketlenir ve bu zaman damgaları sayaç ile "canlı" olarak alındıklarını, yani ya iş parçacığı ya da iletişim kutusu tarafından sıraya alınmadıklarını gösterir.
  • Günlükteki iletilerin sırası (exerpt izler), işçi iş parçacığı gelmeden önce startButtonHandler'ın yürütmeyi tamamladığını, yani iş parçacığının bir iş parçacığı gibi davrandığını gösterir.

    end: startButtonHandler
    Emit: starting work
    Emit: 0
    ...
    Emit: 99
    Emit: finshed work
    
  • İşçi iş parçacığı sadece GUI iş parçacığı ile herhangi bir kaynak paylaşmıyor gibi görünüyor. Yukarıdaki kaynağın sonunda msleep () ve giveCurrentThread () çağırmayı denedim, ancak ikisi de yardımcı görünmüyordu.

Bu konuda herhangi bir deneyimi olan herhangi biri benim hata tespit edebilirsiniz? Bir kez tanımlandıktan sonra düzeltilmesi kolay olan basit ama temel bir hata olduğunu umuyorum.


Durdur düğmesine tıklanmaması normal mi? Duyarlı GUI'nin ana hedefi, çok uzunsa işlemi iptal etmektir. Komut dosyanızı değiştirmeye çalışıyorum ama düğmenin düzgün çalışmasını sağlayamıyorum. İpliğinizi nasıl iptal edersiniz?
etrimaille

Yanıtlar:


6

Bu soruna bir kez daha baktım. Sıfırdan başladım ve başarılı oldum, sonra yukarıdaki koda bakmak için geri döndüm ve hala düzeltemiyorum.

Bu konuyu araştıran herkes için çalışan bir örnek sunmak adına burada fonksiyonel kod sağlayacağım:

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

Bu örneğin yapısı bir WorkerThread (veya alt sınıf) atanabilen bir ThreadManagerDialog sınıfıdır. İletişim kutusunun çalıştırma yöntemi çağrıldığında, çalışandaki doWork yöntemini çağırır. Sonuç olarak, doWork'teki herhangi bir kod ayrı bir iş parçacığında çalışacak ve GUI'yi kullanıcı girdilerine yanıt vermede serbest bırakacaktır.

Bu örnekte, işçi olarak bir CounterThread örneği atanır ve birkaç ilerleme çubuğu bir dakika kadar meşgul kalır.

Not: Bu, python konsoluna yapıştırmaya hazır olacak şekilde biçimlendirilir. Bir .py dosyasına kaydetmeden önce son üç satırın kaldırılması gerekir.


Bu harika bir tak ve çalıştır örneği! Kendi çalışma algoritmamızı uygulamak için bu koddaki en iyi pozisyonu merak ediyorum. Böyle bir işçi WorkerThread sınıfına, daha doğrusu CounterThread sınıfına yerleştirilmelidir mi def doWork? [Bu ilerleme çubuklarını yerleştirilen çalışan algoritmalarına bağlamakla ilgilendi]
Katalpa

Evet, CounterThreadsadece çocuk sınıfının çıplak kemikleri WorkerThread. Daha anlamlı bir uygulama ile kendi çocuk sınıfınızı yaratırsanız, doWorko zaman iyi olmalısınız.
Kelly Thomas

CounterThread'in özellikleri amacım için geçerlidir (ilerleyen kullanıcıya ayrıntılı bildirimler) - ancak bu yeni bir sınıf 'doWork' rutini ile nasıl entegre edilebilir? (ayrıca - yerleştirme akıllıca,
CounterThread

Yukarıdaki CounterThread uygulaması a) işi başlatır, b) iletişim kutusunu başlatır, c) bir çekirdek döngü gerçekleştirir, d) başarılı bir şekilde tamamlandığında true değerini döndürür. Bir döngü ile uygulanabilecek herhangi bir görev yerine bırakılmalıdır. Sunacağım bir uyarı, yöneticiyle iletişim kurmak için sinyalleri yaymanın bazı ek yüklerle gelmesidir, yani her hızlı döngü yinelemesi ile çağrılırsa, gerçek işten daha fazla gecikmeye neden olabilir.
Kelly Thomas

Tüm tavsiyeler için teşekkürler. Benim durumumda bunun çalışması zahmetli olabilir. Şu anda, doWork qgis'te bir mini döküm çökmesine neden oluyor. Çok ağır bir yükün mü yoksa (acemi) programlama becerilerimin bir sonucu mu?
Katalpa
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.