Walk the Filesystem by Creating a QThread Subclass

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
import os
import sys

from PySide6.QtCore import QThread, Slot, Signal, Qt
from PySide6.QtWidgets import (QApplication, QPushButton,
    QLabel, QWidget, QVBoxLayout)


# 1. Create a QThread subclass
#    and subclass its run() method.
#    Add signals as needed.

class WorkerThread(QThread):
    
    progress = Signal(str)
    
    def __init__(self, parent=None):
        super().__init__(parent)
        print('Init it', QThread.currentThread().objectName())
        
    def run(self):
        
        print('Running in: ',
            QThread.currentThread().objectName())
        print('event loop level: ',
            QThread.currentThread().loopLevel())

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


class Window(QWidget):
    
    def __init__(self):

        super().__init__()
        
        QThread.currentThread().setObjectName('Main thread')
        
        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):
        
        print('Main thread loop level', 
            QThread.currentThread().loopLevel())
        
        # 2. Create the WorkerThread object

        self.worker_thread = WorkerThread()
        self.worker_thread.setObjectName('Worker thread')

        # 3. Connect the signals with the slots

        self.worker_thread.progress.connect(self.on_progress)
        self.worker_thread.finished.connect(
            self.worker_thread.deleteLater)
        
        self.start_button.setDisabled(True)
        self.cancel_button.setEnabled(True)
        
        # 5. Start the worker thread
        
        self.worker_thread.start()
    
    # 4. Handle the signals
    
    @Slot()
    def on_cancel_button_clicked(self):
        
        self.start_button.setEnabled(True)
        self.cancel_button.setDisabled(True)
        
        if hasattr(self, 'worker_thread'):
            self.worker_thread.requestInterruption()
            self.worker_thread.wait()
        
    @Slot()
    def on_progress(self, msg):
        self.label.setText(msg)
        
    # Make sure the thread is destroyed
    # when the main window is closed.
    
    def closeEvent(self, event):        
        try:
            self.worker_thread.requestInterruption()
            self.worker_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())

Now let’s walk the filesystem using a QThread subclass:

  1. Create a QThread subclass named WorkerThread and override its run() method. In run() we first print the current thread object name to show that it indeed runs in the worker thread and after that we use os.walk to walk the filesystem recursively. For each filesystem object we emit the progress signal with the object’s name as the argument. Then, in the main window

  2. On the start button click create a WorkerThread object and set its object name to “Worker thread”,

  3. Connect the signals with the slots. Each time WorkerThread.progress is emitted we execute Window.on_progress) which sets the label’s text to the filesystem object name that progress emits as the argument. We also connect the WorkerThread.finished signal with WorkerThread.deleteLater() to dispose of the thread when it finishes.

  4. Handle the signals. When WorkerThread.progress is emitted Window.on_progress() updates the label text. When the cancel button is clicked we request the worker thread interruption using QThread.requestInterruption() and wait for it to finish.

  5. Start the worker thread each time the start button is clicked.

To make sure the thread is deleted when the user closes the main window we also override Qwidget.closeEvent(). Note that we didn’t need to call QThread.quit() but only QThread.wait() because the thread event loop is not running.