1

Below is my code for listing all the sub-directories of a directory. I'm using it to understand QThread and signal and slots in PySide. The problem is, when I'm not using Qtcore.QApplication.processEvents() in the scan() method of the Main class, the code does not work. Is the event-loop not already running?

import sys
import os
import time
from PySide import QtGui, QtCore

class Scanner(QtCore.QObject):
    folderFound = QtCore.Signal(str)
    done = QtCore.Signal()
    def __init__(self, path):
        super(Scanner, self).__init__()
        self.path = path

    @QtCore.Slot()
    def scan(self):
        for folder in os.listdir(self.path):
            time.sleep(1)
            self.folderFound.emit(folder)

class Main(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.resize(420,130)
        self.setWindowTitle('Threads')
        self.lyt = QtGui.QVBoxLayout()
        self.setLayout(self.lyt)

        self.topLyt = QtGui.QHBoxLayout()
        self.lyt.addLayout(self.topLyt)
        self.scanBtn = QtGui.QPushButton('Scan')
        self.scanBtn.clicked.connect(self.scan)
        self.clearBtn = QtGui.QPushButton('Clear')
        self.topLyt.addWidget(self.scanBtn)
        self.topLyt.addWidget(self.clearBtn)

        self.folders = list()

        self.show()

    def scan(self):
        self.th = QtCore.QThread()
        scanner = Scanner(r"D:\\")
        scanner.moveToThread(self.th)

        scanner.folderFound.connect(self.addFolder)
        scanner.done.connect(scanner.deleteLater)
        scanner.done.connect(self.quit)

        self.th.started.connect(scanner.scan)
        self.th.start()

        QtCore.QApplication.processEvents()

    @QtCore.Slot()
    def addFolder(self, folder):
        lbl = QtGui.QLabel(folder)
        self.folders.append(lbl)

        self.lyt.addWidget(lbl)

    @QtCore.Slot()
    def quit(self):
        self.th.quit()
        self.th.wait()

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    main = Main()    
    app.exec_()
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
Vicspidy
  • 31
  • 1
  • 8
  • What do you mean by "not working"? – 101 Feb 15 '18 at 07:35
  • It means when I click 'Scan' button, nothing happens, but when I use processEvents, It starts to add folder names in label which it is getting from the thread. – Vicspidy Feb 15 '18 at 07:38
  • http://qt-project.org/wiki/ThreadsEventsQObjects – Murphy Feb 15 '18 at 11:47
  • Possible duplicate of [Updating GUI elements in MultiThreaded PyQT](https://stackoverflow.com/questions/9957195/updating-gui-elements-in-multithreaded-pyqt) – Murphy Feb 15 '18 at 11:51

2 Answers2

2

It is a pure fluke that your example works at all.

When you attempt to call QtCore.QApplication.processEvents(), a NameError will be raised, because the QApplication class is actually in the QtGui module, not the QtCore module. However, raising the exception has the side-effect of preventing your scanner object from being garbage-collected, and so the thread appears to run normally.

The correct way to fix your code is to keep a reference to the scanner object, and get rid of the processEvents line, which is not needed:

def scan(self):
    self.th = QtCore.QThread()
    # keep a reference to the scanner
    self.scanner = Scanner(r"D:\\")
    self.scanner.moveToThread(self.th)

    self.scanner.folderFound.connect(self.addFolder)
    self.scanner.done.connect(self.scanner.deleteLater)
    self.scanner.done.connect(self.quit)

    self.th.started.connect(self.scanner.scan)
    self.th.start()

The rest of your code is okay, and the example will now work as expected.

ekhumoro
  • 115,249
  • 20
  • 229
  • 336
1

Event loop is not something that runs behind your back. You're always the one who has to run it. A thread cannot be doing two things at once: if your code is the locus of control, then obviously the event loop isn't! You need to return from your code to the event loop, and make sure that your code was called from the event loop. Any sort of a UI signal, networking event, or timeout is invoked from the event loop, so most likely your code already has the event loop on the call stack. To keep the loop spinning, you have to return to it, though.

Never use processEvents - instead, invert the control, so that the event loop calls into your code, and then you perform a chunk of work, and finally return back to the event loop.

The idiom for "keep my code working from event loop" is a zero-duration timer. The callable that performs the work is attached to the timeout signal.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313