Rozwijałem niektóre narzędzia przetwarzania wsadowego jako wtyczki Pythona dla QGIS 1.8.
Przekonałem się, że podczas działania moich narzędzi GUI przestaje odpowiadać.
Ogólna mądrość jest taka, że praca powinna być wykonywana w wątku roboczym, a informacje o statusie / zakończeniu przekazywane z powrotem do GUI jako sygnały.
Przeczytałem dokumentację dotyczącą brzegów rzek i przestudiowałem źródło doGeometry.py (działającej implementacji z narzędzi ftools ).
Korzystając z tych źródeł, próbowałem zbudować prostą implementację w celu zbadania tej funkcjonalności przed wprowadzeniem zmian w ustalonej bazie kodu.
Ogólna struktura to pozycja w menu wtyczek, która wyświetla okno dialogowe z przyciskami start i stop. Przyciski sterują wątkiem, który liczy się do 100, wysyłając sygnał z powrotem do GUI dla każdej liczby. GUI odbiera każdy sygnał i wysyła ciąg zawierający numer zarówno dziennika komunikatów, jak i tytułu okna.
Kod tej implementacji znajduje się tutaj:
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()
Niestety nie jest tak cicho, jak miałem nadzieję:
- Tytuł okna aktualizuje się „na żywo” za pomocą licznika, ale jeśli kliknę okno dialogowe, nie odpowiada.
- Dziennik komunikatów jest nieaktywny, dopóki licznik się nie skończy, a następnie wyświetla wszystkie komunikaty jednocześnie. Te wiadomości są oznaczone znacznikiem czasu przez QgsMessageLog i te znaczniki czasu wskazują, że zostały odebrane „na żywo” z licznikiem, tj. Nie są w kolejce ani przez wątek roboczy, ani przez okno dialogowe.
Kolejność komunikatów w dzienniku (ćwiczenie poniżej) wskazuje, że startButtonHandler kończy wykonywanie przed uruchomieniem wątku roboczego, tzn. Wątek zachowuje się jak wątek.
end: startButtonHandler Emit: starting work Emit: 0 ... Emit: 99 Emit: finshed work
Wygląda na to, że wątek roboczy po prostu nie udostępnia żadnych zasobów wątkowi GUI. Na końcu powyższego źródła znajduje się kilka skomentowanych wierszy, w których próbowałem wywołać msleep () i fedCurrentThread (), ale żadne z nich nie pomogło.
Czy ktoś z jakimś doświadczeniem może wykryć mój błąd? Mam nadzieję, że jest to prosty, ale fundamentalny błąd, który można łatwo naprawić po zidentyfikowaniu.
źródło
Odpowiedzi:
Ponownie przyjrzałem się temu problemowi. Zacząłem od zera i odniosłem sukces, a potem wróciłem do kodu powyżej i nadal nie mogę go naprawić.
Aby zapewnić praktyczny przykład dla każdego, kto bada ten temat, podam tutaj kod funkcjonalny:
Struktura tego przykładu jest klasą ThreadManagerDialog, której można przypisać WorkerThread (lub podklasę). Po wywołaniu metody uruchamiania okna dialogowego wywoła ona metodę doWork na pracowniku. W rezultacie dowolny kod w doWork będzie działał w osobnym wątku, pozostawiając GUI swobodny w odpowiedzi na dane wejściowe użytkownika.
W tym przykładzie wystąpienie CounterThread jest przypisane jako proces roboczy, a kilka pasków postępu będzie przez chwilę zajęte.
Uwaga: jest sformatowany w taki sposób, że jest gotowy do wklejenia w konsoli Pythona. Ostatnie trzy wiersze będą musiały zostać usunięte przed zapisaniem do pliku .py.
źródło
CounterThread
to tylko przykładowa klasa dla dzieciWorkerThread
. Jeśli utworzysz własną klasę podrzędną z bardziej znaczącą implementacją,doWork
wszystko powinno być w porządku.