Làm cách nào để duy trì GUI phản hồi bằng QThread với PyQGIS


11

Tôi đã phát triển một số công cụ xử lý hàng loạt như các plugin python cho QGIS 1.8.

Tôi đã thấy rằng trong khi các công cụ của tôi đang chạy GUI trở nên không phản hồi.

Sự khôn ngoan chung là công việc nên được thực hiện trên một luồng công nhân, với thông tin trạng thái / hoàn thành được chuyển trở lại GUI dưới dạng tín hiệu.

Tôi đã đọc qua các tài liệu ở bờ sông và nghiên cứu nguồn doGeometry.py (một triển khai làm việc từ ftools ).

Sử dụng các nguồn này, tôi đã cố gắng xây dựng một triển khai đơn giản để khám phá chức năng này trước khi thực hiện các thay đổi đối với cơ sở mã đã thiết lập.

Cấu trúc tổng thể là một mục trong menu bổ trợ, bao gồm một hộp thoại với các nút bắt đầu và dừng. Các nút điều khiển một luồng đếm đến 100, gửi tín hiệu trở lại GUI cho mỗi số. GUI nhận từng tín hiệu và gửi một chuỗi chứa số cả nhật ký tin nhắn và tiêu đề cửa sổ.

Mã của việc thực hiện này là ở đây:

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

Thật không may, nó không hoạt động yên tĩnh như tôi hy vọng:

  • Tiêu đề cửa sổ đang cập nhật "trực tiếp" với bộ đếm nhưng nếu tôi nhấp vào hộp thoại thì nó không phản hồi.
  • Nhật ký tin nhắn không hoạt động cho đến khi bộ đếm kết thúc, sau đó trình bày tất cả các tin nhắn cùng một lúc. Các thông báo này được gắn thẻ có dấu thời gian bởi QssMessageLog và những dấu thời gian này cho biết chúng đã được nhận "trực tiếp" với bộ đếm, tức là chúng không được xếp hàng bởi luồng công nhân hoặc hộp thoại.
  • Thứ tự của các thông báo trong nhật ký (trích dẫn sau) chỉ ra rằng startButtonHandler hoàn thành việc thực thi trước khi các luồng xử lý của công nhân hoạt động tức là luồng xử lý như một luồng.

    end: startButtonHandler
    Emit: starting work
    Emit: 0
    ...
    Emit: 99
    Emit: finshed work
    
  • Có vẻ như luồng công nhân không chia sẻ bất kỳ tài nguyên nào với luồng GUI. Có một vài dòng nhận xét ở cuối nguồn trên, nơi tôi đã thử gọi ms ngủ () và ieldCảnThread (), nhưng dường như không có ích.

Có ai có kinh nghiệm với điều này có thể phát hiện ra lỗi của tôi không? Tôi hy vọng đó là một lỗi đơn giản nhưng cơ bản, dễ sửa chữa một khi nó được xác định.


Có phải bình thường là nút dừng không thể được bấm? Mục tiêu chính của GUI đáp ứng là hủy quá trình nếu quá dài. Tôi cố gắng sửa đổi tập lệnh của bạn nhưng tôi không thể làm cho nút hoạt động bình thường. Làm thế nào để bạn hủy bỏ chủ đề của bạn?
etrimaille

Câu trả lời:


6

Vì vậy, tôi đã có một cái nhìn khác về vấn đề này. Tôi đã bắt đầu từ đầu và đã thành công, sau đó quay lại xem mã ở trên và vẫn không thể sửa nó.

Vì lợi ích của việc cung cấp một ví dụ hoạt động cho bất kỳ ai nghiên cứu về chủ đề này, tôi sẽ cung cấp mã chức năng ở đây:

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

Cấu trúc của mẫu này là một lớp ThreadManagerDialog có thể được gán một WorkerThread (hoặc lớp con). Khi phương thức chạy của hộp thoại được gọi, nó sẽ lần lượt gọi phương thức doWork trên worker. Kết quả là bất kỳ mã nào trong doWork sẽ chạy trong một luồng riêng biệt, để GUI tự do trả lời đầu vào của người dùng.

Trong mẫu này, một phiên bản của CounterThread được chỉ định là công nhân và một vài thanh tiến trình sẽ được giữ bận rộn trong một phút hoặc lâu hơn.

Lưu ý: đây là định dạng để nó sẵn sàng dán vào bảng điều khiển python. Ba dòng cuối cùng sẽ cần phải được xóa trước khi lưu vào tệp .py.


Đây là một ví dụ cắm và chơi tuyệt vời! Tôi tò mò về vị trí tốt nhất trong mã này để thực hiện thuật toán làm việc của chính chúng ta. Sẽ cần phải được đặt trong lớp WorkerThread, hay đúng hơn là trong lớp CounterThread, def doWork? [Được hỏi về lợi ích của việc kết nối các thanh tiến trình này với (các) thuật toán worker worker được chèn]
Katalpa

Vâng, CounterThreadchỉ là một ví dụ lớp xương trần của trẻ em WorkerThread. Nếu bạn tạo lớp con của riêng bạn với việc thực hiện có ý nghĩa hơn doWorkthì bạn sẽ ổn thôi.
Kelly Thomas

Các đặc điểm của CounterThread có thể áp dụng cho mục tiêu của tôi (thông báo chi tiết cho người dùng về tiến trình) - nhưng làm thế nào để được tích hợp với thói quen c. Class 'doWork' mới? (cũng - vị trí khôn ngoan, 'doWork' trong CounterThread phải không?)
Katalpa

Việc triển khai CounterThread ở trên a) khởi tạo công việc, b) khởi tạo hộp thoại, c) thực hiện một vòng lặp cốt lõi, d) trả về true khi hoàn thành thành công. Bất kỳ nhiệm vụ có thể được thực hiện với một vòng lặp chỉ nên thả tại chỗ. Một cảnh báo tôi sẽ đưa ra là việc phát ra các tín hiệu để liên lạc với người quản lý đi kèm với một số chi phí tức là nếu được gọi với mỗi lần lặp của vòng lặp nhanh, nó có thể gây ra độ trễ nhiều hơn so với công việc thực tế.
Kelly Thomas

Cảm ơn vì tất cả những lời khuyên. Có thể gây rắc rối cho điều này để làm việc trong tình huống của tôi. Hiện tại, doWork gây ra sự cố nhỏ trong qgis. Một kết quả của tải quá nặng, hoặc kỹ năng lập trình (người mới) của tôi?
Katalpa
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.