Bagaimana cara mencegah Qgis terdeteksi sebagai "tidak merespons" saat menjalankan plugin yang berat?

10

Saya menggunakan baris berikut untuk memberi tahu pengguna tentang status:

iface.mainWindow().statusBar().showMessage("Status:" + str(i))

Plugin ini membutuhkan waktu sekitar 2 menit untuk berjalan pada set data saya tetapi windows mendeteksinya sebagai "tidak merespons" dan berhenti menampilkan pembaruan status. Untuk pengguna baru, ini tidak begitu baik karena sepertinya programnya macet.

Apakah ada pekerjaan di sekitar sehingga pengguna tidak dibiarkan dalam kegelapan mengenai status plugin?

Johan Holtby
sumber

Jawaban:

13

Seperti yang ditunjukkan oleh Nathan W , cara untuk melakukan ini adalah dengan multithreading, tetapi mensubklasifikasikan QThread bukan praktik terbaik. Lihat di sini: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

Lihat di bawah ini contoh cara membuat QObject, lalu pindahkan ke QThread(yaitu cara "benar" untuk melakukannya). Contoh ini menghitung luas total semua fitur dalam lapisan vektor (menggunakan QGIS 2.0 API baru!).

Pertama, kita membuat objek "pekerja" yang akan melakukan tugas berat untuk kita:

class Worker(QtCore.QObject):
    def __init__(self, layer, *args, **kwargs):
        QtCore.QObject.__init__(self, *args, **kwargs)
        self.layer = layer
        self.total_area = 0.0
        self.processed = 0
        self.percentage = 0
        self.abort = False

    def run(self):
        try:
            self.status.emit('Task started!')
            self.feature_count = self.layer.featureCount()
            features = self.layer.getFeatures()
            for feature in features:
                if self.abort is True:
                    self.killed.emit()
                    break
                geom = feature.geometry()
                self.total_area += geom.area()
                self.calculate_progress()
            self.status.emit('Task finished!')
        except:
            import traceback
            self.error.emit(traceback.format_exc())
            self.finished.emit(False, self.total_area)
        else:
            self.finished.emit(True, self.total_area)

    def calculate_progress(self):
        self.processed = self.processed + 1
        percentage_new = (self.processed * 100) / self.feature_count
        if percentage_new > self.percentage:
            self.percentage = percentage_new
            self.progress.emit(self.percentage)

    def kill(self):
        self.abort = True

    progress = QtCore.pyqtSignal(int)
    status = QtCore.pyqtSignal(str)
    error = QtCore.pyqtSignal(str)
    killed = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal(bool, float)

Untuk menggunakan pekerja kita perlu menginisialisasi dengan lapisan vektor, pindahkan ke utas, hubungkan beberapa sinyal, lalu mulai itu. Mungkin yang terbaik untuk melihat blog yang ditautkan di atas untuk memahami apa yang terjadi di sini.

thread = QtCore.QThread()
worker = Worker(layer)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.progress.connect(self.ui.progressBar)
worker.status.connect(iface.mainWindow().statusBar().showMessage)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
worker.finished.connect(thread.quit)
thread.start()

Contoh ini menggambarkan beberapa poin utama:

  • Segala sesuatu di dalam run()metode pekerja ada di dalam pernyataan coba-kecuali. Sulit untuk dipulihkan ketika kode Anda macet di dalam utas. Ini memancarkan traceback melalui sinyal kesalahan, yang biasanya saya sambungkan ke QgsMessageLog.
  • Sinyal yang sudah selesai memberitahu metode yang terhubung jika proses selesai dengan sukses, dan juga hasilnya.
  • Sinyal progres hanya dipanggil ketika persentase selesai berubah, bukan satu kali untuk setiap fitur. Ini mencegah terlalu banyak panggilan untuk memperbarui bilah kemajuan memperlambat proses pekerja, yang akan mengalahkan seluruh titik menjalankan pekerja di utas lain: untuk memisahkan perhitungan dari antarmuka pengguna.
  • Pekerja menerapkan kill()metode, yang memungkinkan fungsi untuk mengakhiri dengan anggun. Jangan mencoba dan menggunakan terminate()metode ini QThread- hal-hal buruk bisa terjadi!

Pastikan untuk melacak objek Anda threaddan workerdi suatu tempat di struktur plugin Anda. Qt marah kalau kamu tidak. Cara termudah untuk melakukan ini adalah menyimpannya di dialog saat Anda membuatnya, misalnya:

thread = self.thread = QtCore.QThread()
worker = self.worker = Worker(layer)

Atau Anda dapat membiarkan Qt mengambil kepemilikan atas QThread:

thread = QtCore.QThread(self)

Butuh waktu lama untuk menggali semua tutorial untuk menyatukan templat ini, tetapi sejak itu saya telah menggunakannya kembali di semua tempat.

Snorfalorpagus
sumber
Terima kasih ini persis apa yang saya cari dan itu sangat membantu! Saya sudah terbiasa dengan utas i C # tetapi tidak memikirkannya dengan python.
Johan Holtby
Ya ini cara yang benar.
Nathan W
1
Haruskah ada "diri". di depan layer dalam "features = layer.getFeatures ()"? -> "features = self.layer.getFeatures ()"
Håvard Tveite
@ HåvardTveite Anda benar. Saya telah memperbaiki kode dalam jawabannya.
Snorfalorpagus
Saya mencoba mengikuti pola ini untuk skrip pemrosesan yang saya tulis, dan saya kesulitan membuatnya berfungsi. Saya mencoba menyalin contoh ini ke dalam file skrip, menambahkan pernyataan impor yang diperlukan, dan mengubah worker.progress.connect(self.ui.progressBar)ke yang lain, tetapi setiap kali saya menjalankannya qgis-bin mogok. Saya tidak punya pengalaman men-debug kode python atau qgis. Semua yang saya dapatkan adalah Access violation reading location 0x0000000000000008jadi sepertinya ada sesuatu yang nol. Apakah ada beberapa kode penyiapan yang tidak dapat digunakan dalam skrip pemrosesan?
TJ Rockefeller
4

Satu-satunya cara sejati Anda untuk melakukan ini adalah dengan multithreading.

class MyLongRunningStuff(QThread):
    progressReport = pyqtSignal(str)
    def __init__(self):
       QThread.__init__(self)

    def run(self):
       # do your long runnning thing
       self.progressReport.emit("I just did X")

 thread = MyLongRunningStuff()
 thread.progressReport.connect(self.updatetheuimethod)
 thread.start()

Beberapa bacaan tambahan http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/

Catatan Beberapa orang tidak suka mewarisi dari QThread, dan tampaknya ini bukan cara yang "benar" untuk melakukannya, tetapi itu berhasil ....

Nathan W
sumber
:) Sepertinya cara kotor yang bagus untuk melakukannya. Beberapa kali gaya tidak perlu. Untuk kali ini (yang pertama di pyqt) saya pikir saya akan pergi dengan cara yang benar karena saya sudah terbiasa dengan itu di C #.
Johan Holtby
2
Itu bukan cara yang kotor, itu cara lama melakukannya.
Nathan W
2

Karena pertanyaan ini relatif lama, ia layak diperbarui. Dengan QGIS 3 ada pendekatan dengan QgsTask.fromFunction (), QgsProcessingAlgRunnerTask () dan QgsApplication.taskManager (). AddTask ().

Lebih lanjut tentang hal itu misalnya di Menggunakan Thread di PyQGIS3 OLEH MARCO BERNASOCCHI

Miro
sumber