Walk the Filesystem Using QObject.moveToThread()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import os
import sys
from PySide6.QtCore import QObject, QThread, Slot, Signal, Qt
from PySide6.QtWidgets import (QApplication, QPushButton,
QLabel, QWidget, QVBoxLayout)
# 1. Create the worker class
class Worker(QObject):
finished = Signal()
progress = Signal(str)
error = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
# This is the method we want to execute.
# We are in a tight loop.
@Slot()
def process(self):
path = os.path.abspath('.').split(os.path.sep)[0] + os.path.sep
for root, _, _ in os.walk(path):
if QThread.currentThread().isInterruptionRequested():
return
self.progress.emit(os.path.basename(root))
self.finished.emit()
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.start_button = QPushButton('Start background thread')
self.start_button.clicked.connect(self.on_start_button_clicked)
self.cancel_button = QPushButton('Cancel')
self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
self.cancel_button.setDisabled(True)
self.label = QLabel()
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.start_button)
layout.addWidget(self.cancel_button)
layout.addWidget(self.label)
@Slot()
def on_start_button_clicked(self):
self.start_button.setDisabled(True)
self.cancel_button.setEnabled(True)
# 2. Create the thread
self.background_thread = QThread()
# 3. Create the worker and move it to the thread
self.worker = Worker()
self.worker.moveToThread(self.background_thread)
self.worker.finished.connect(self.on_finished)
# 4. Connect the appropriate signals to ensure
# both the worker and the thread are destroyed.
self.worker.error.connect(self.on_error)
self.background_thread.started.connect(self.worker.process)
self.worker.finished.connect(self.background_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.background_thread.finished.connect(self.background_thread.deleteLater)
self.worker.progress.connect(self.label.setText)
# 5. Start the thread.
self.background_thread.start()
# Stop the work by requesting interruption
@Slot()
def on_cancel_button_clicked(self):
self.start_button.setEnabled(True)
self.cancel_button.setDisabled(True)
if hasattr(self, 'background_thread'):
self.background_thread.requestInterruption()
self.background_thread.quit()
self.background_thread.wait()
@Slot()
def on_finished(self):
self.label.setText('Worker finished')
@Slot()
def on_error(self, message):
print(message)
# Make sure the thread is destroyed
# when the main window is closed.
def closeEvent(self, event):
try:
self.background_thread.requestInterruption()
self.background_thread.quit()
self.background_thread.wait()
except Exception as e:
print(e)
if __name__ == '__main__':
app = QApplication(sys.argv)
main_window = Window()
main_window.show()
sys.exit(app.exec())
In the moveToThread()
template we saw a template for moving a QObject
to a QThread
but the worker object object did nothing but print a message in the terminal. Now let’s see a more realistic example - the Worker.process()
method walks the file system using os.walk()
starting from the file system root. This can take some time and would actually block the Gui if done from the main thread. To walk the file system from the background thread using QObject.moveToThread()
-
Create the worker class. The method to be executed is called
process()
just as in the template script but now it uses the Pythonos.walk()
to walk the file system. For each file system objectwalk()
enumerates we emit a custom signal namedprogress
that we added toWorker
along withfinished
anderror
. If something requests the background thread interruption we return from theprocess()
method which triggers the signal and slot chain to stop and delete both the worker object and the background thread. Note how inWorker.process()
we access the current (ie background) thread using the staticQThread.currentThread()
method. -
In the main window class, create the thread object.
-
Create a
Worker
object and move it to the createdQThread
usingQObject.moveToThread()
-
Use signals and slots to make sure that both the worker and the background thread are created and deleted properly: - starting the background thread triggers the
Worker.process()
execution -QObject.finished
triggers bothQThread.quit()
andWorker.deleteLater()
-QThread.finished
triggersQThread.deleteLater()
. -
Finally start the background thread. All this happens in the
Window.on_start_button_clicked()
slot which means newQThread
andWorker
objects are created each time the start button is clicked.
One thing to note is that we override QWidget.closeEvent()
to interrupt the background thread which makes sure the thread and the worker are deleted if the user closes the main window while the thread is still running. The same sequence, QThread.requestInterruption()
+ QThread.quit()
+ QThread.wait()
, is used in Window.on_cancel_button_clicked()
to make sure that the background thread exits cleanly when interrupted.