Bagaimana cara mempertahankan GUI resposif menggunakan QThread dengan PyQGIS

11

Saya telah mengembangkan beberapa alat pemrosesan batch sebagai plugin python untuk QGIS 1.8.

Saya menemukan bahwa ketika alat saya menjalankan GUI menjadi tidak responsif.

Kebijaksanaan umum adalah bahwa pekerjaan harus dilakukan pada utas pekerja, dengan informasi status / penyelesaian dikembalikan ke GUI sebagai sinyal.

Saya telah membaca dokumen tepi sungai , dan mempelajari sumber doGeometry.py (implementasi kerja dari ftools ).

Dengan menggunakan sumber-sumber ini saya telah mencoba membangun implementasi sederhana untuk menjelajahi fungsionalitas ini sebelum membuat perubahan pada basis kode yang sudah ada.

Struktur keseluruhan adalah entri dalam menu plugins, yang menghubungkan dialog dengan tombol start dan stop. Tombol-tombol mengontrol utas yang diperhitungkan hingga 100, mengirimkan sinyal kembali ke GUI untuk setiap nomor. GUI menerima setiap sinyal dan mengirimkan string yang berisi nomor log pesan, dan judul jendela.

Kode implementasi ini ada di sini:

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

Sayangnya itu tidak tenang bekerja seperti yang saya harapkan:

  • Judul jendela memperbarui "langsung" dengan penghitung tetapi jika saya mengklik pada dialog, itu tidak responsif.
  • Log pesan tidak aktif hingga penghitung berakhir, kemudian menyajikan semua pesan sekaligus. Pesan-pesan ini ditandai dengan cap waktu oleh QgsMessageLog dan perangko waktu ini menunjukkan bahwa mereka diterima "langsung" dengan penghitung yaitu mereka tidak sedang antri oleh thread pekerja, atau dialog.
  • Urutan pesan dalam log (kutipan berikut) menunjukkan bahwa startButtonHandler menyelesaikan eksekusi sebelum utas pekerja mulai berfungsi yaitu utas berperilaku sebagai utas.

    end: startButtonHandler
    Emit: starting work
    Emit: 0
    ...
    Emit: 99
    Emit: finshed work
    
  • Tampaknya utas pekerja tidak membagikan sumber daya apa pun dengan utas GUI. Ada beberapa baris komentar di akhir sumber di atas di mana saya mencoba memanggil msleep () dan yieldCurrentThread (), tetapi sepertinya tidak ada yang membantu.

Adakah yang punya pengalaman dengan ini dapat menemukan kesalahan saya? Saya berharap ini adalah kesalahan sederhana namun mendasar yang mudah untuk diperbaiki setelah diidentifikasi.

Kelly Thomas
sumber
Apakah normal bahwa tombol berhenti tidak dapat diklik? Tujuan utama GUI responsif adalah untuk membatalkan proses jika terlalu lama. Saya mencoba untuk memodifikasi skrip Anda tetapi saya tidak dapat mengaktifkan tombol dengan benar. Bagaimana Anda membatalkan utas Anda?
etrimaille

Jawaban:

6

Jadi saya melihat masalah ini lagi. Saya mulai dari awal dan sukses, kemudian kembali untuk melihat kode di atas dan masih tidak dapat memperbaikinya.

Demi memberikan contoh kerja bagi siapa pun yang meneliti subjek ini, saya akan memberikan kode fungsional di sini:

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

Struktur sampel ini adalah kelas ThreadManagerDialog yang dapat ditetapkan sebagai WorkerThread (atau subclass). Ketika metode jalankan dialog dipanggil, pada gilirannya akan memanggil metode doWork pada pekerja. Hasilnya adalah kode apa pun di doWork akan berjalan di utas terpisah, sehingga GUI bebas merespons input pengguna.

Dalam contoh ini contoh CounterThread ditugaskan sebagai pekerja dan beberapa progress bar akan tetap sibuk selama satu menit atau lebih.

Catatan: ini diformat sehingga siap untuk ditempelkan ke konsol python. Tiga baris terakhir harus dihapus sebelum menyimpan ke file .py.

Kelly Thomas
sumber
Ini adalah contoh plug and play yang bagus! Saya ingin tahu tentang posisi terbaik dalam kode ini untuk menerapkan algorythmn kerja kita sendiri. Apakah perlu ditempatkan di kelas WorkerThread, atau lebih tepatnya di kelas CounterThread, def doWork? [Diminta untuk menghubungkan bilah kemajuan ini dengan algoritme pekerja yang disisipkan]
Katalpa
Ya, CounterThreadhanyalah contoh sederhana dari kelas anak WorkerThread. Jika Anda membuat kelas anak Anda sendiri dengan implementasi yang lebih bermakna doWorkmaka Anda harus baik-baik saja.
Kelly Thomas
Karakteristik CounterThread berlaku untuk tujuan saya (pemberitahuan terperinci untuk pengguna kemajuan) - tetapi bagaimana hal itu akan diintegrasikan dengan rutin baru c.class 'doWork'? (Juga - penempatan bijaksana, 'doWork' di CounterThread kan?)
Katalpa
Implementasi CounterThread di atas a) menginisialisasi pekerjaan, b) menginisialisasi dialog, c) melakukan loop inti, d) mengembalikan true pada penyelesaian yang berhasil. Tugas apa pun yang dapat diimplementasikan dengan sebuah loop harus segera diberlakukan. Satu peringatan yang akan saya tawarkan adalah bahwa memancarkan sinyal untuk berkomunikasi dengan manajer datang dengan beberapa overhead yaitu jika dipanggil dengan setiap iterasi dari loop cepat, hal itu dapat menyebabkan latensi lebih dari pekerjaan yang sebenarnya.
Kelly Thomas
Terima kasih untuk semua sarannya. Mungkin merepotkan untuk bekerja dalam situasi saya ini. Saat ini, doWork menyebabkan crash minidump di qgis. Akibat beban yang terlalu berat, atau keterampilan pemrograman (pemula) saya?
Katalpa