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

  1. Create the worker class. The method to be executed is called process() just as in the template script but now it uses the Python os.walk() to walk the file system. For each file system object walk() enumerates we emit a custom signal named progress that we added to Worker along with finished and error. If something requests the background thread interruption we return from the process() method which triggers the signal and slot chain to stop and delete both the worker object and the background thread. Note how in Worker.process() we access the current (ie background) thread using the static QThread.currentThread() method.

  2. In the main window class, create the thread object.

  3. Create a Worker object and move it to the created QThread using QObject.moveToThread()

  4. 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 both QThread.quit() and Worker.deleteLater() - QThread.finished triggers QThread.deleteLater().

  5. Finally start the background thread. All this happens in the Window.on_start_button_clicked() slot which means new QThread and Worker 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.