Search code examples
pythonpython-3.xmultithreadingpyqt5qthread

Prevent "GUI Freezing" While Checking a File Exists


I'm trying to build a file listener app in pyqt5. My code works as I want, but I want to improve it.

There is a simple button Listen. When I click it, it opens a notepad and it starts to always listen until a a.txt file exists. After exist, a new button Start exists, old button is removed.

My problem is; my GUI is freezing while it is listening the a.txt file even I use threading. Am I using wrong? Can you fix my code?

My main code;

from PyQt5 import QtCore, QtWidgets
import sys
import os

class ListenWindow(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(ListenWindow, self).__init__(parent)
        self.setWindowTitle("Listen")

        self.button_listen = QtWidgets.QPushButton('Listen', self)
        font1 = self.button_listen.font()
        font1.setPointSize(10)
        self.button_listen.setFont(font1)
        self.button_listen.setFixedSize(200, 50)
        self.button_listen.clicked.connect(self.startToListen)

        self.v_box1 = QtWidgets.QVBoxLayout(self)
        self.v_box1.addWidget(self.button_listen)

        self.h_box1 = QtWidgets.QHBoxLayout(self)
        self.v_box1.addLayout(self.h_box1)

    def abc(self):
        while not os.path.exists('C:/Users/Wicaledon/PycharmProjects/myproject/a.txt'):
            pass

        if os.path.isfile('C:/Users/Wicaledon/PycharmProjects/myproject/a.txt'):
            self.button_start = QtWidgets.QPushButton('Start', self)
            font2 = self.button_start.font()
            font2.setPointSize(10)
            self.button_start.setFont(font2)
            self.button_start.setFixedSize(200, 50)
            self.h_box1.addWidget(self.button_start, 0, QtCore.Qt.AlignCenter)
        else:
            raise ValueError("%s isn't a file!" % 'C:/Users/Wicaledon/PycharmProjects/myproject/a.txt')
        self.v_box1.removeWidget(self.button_listen)

    def startToListen(self):
        def thread_function(my_text):
            import subprocess
            import os
            FNULL = open(os.devnull, 'w')
            args = my_text
            subprocess.call(args, stdout=FNULL, stderr=FNULL, shell=True)
            # os.system(my_text)
            return
        import threading
        my_text = "notepad"
        x = threading.Thread(target=thread_function,args=(my_text,))
        x.start()
        y = threading.Thread(target=ListenWindow.abc(self))
        y.start()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = ListenWindow()
    window.setWindowTitle('Login')
    window.show()
    sys.exit(app.exec_())

My code for creating a.txt;

f=open("a.txt", "w+")
f.write("delete_me")
f.close()

Solution

  • It's not a good thing to use that simple while approach to check if a file exists, because it consumes a lot of CPU resources unnecessarily.
    Also, using any kind of loop (while/for or recursive function calls) is a bad idea when dealing with an UI: as you've already seen, it blocks the interface; while the solution proposed by abhilb might seem to work, it just makes the UI responsive, but will keep the CPU spiking anyway, even after the program is closed and the file has not been created.

    PyQt already has a file listener, QFileSystemWatcher, and external systems should avoided with Qt that functionality has already provided by it, this counts not only for the file watcher, but also for threading.

    Another important aspect to remember is that Qt has its own event loop, and it's not a good idea to use python's threads to interact with it (as a matter of fact, every UI interaction has to be done in the main thread, even when using Qt's threads).

    If you really want to do a basic file listener (eg, for study purposes), you should at least add a waiter within the cycle, and that cycle has to be in another thread, otherwise the GUI will be blocked anyway.

    The following is an implementation completely based on Qt, based on your code.

    import sys
    from PyQt5 import QtCore, QtWidgets
    
    fileToWatch = 'C:/Users/Wicaledon/PycharmProjects/myproject/a.txt'
    editorProgram = 'notepad'
    
    class ListenWindow(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(ListenWindow, self).__init__(parent)
            self.setWindowTitle("Listen")
    
            self.button_listen = QtWidgets.QPushButton('Listen', self)
            font1 = self.button_listen.font()
            font1.setPointSize(10)
            self.button_listen.setFont(font1)
            self.button_listen.setFixedSize(200, 50)
            self.button_listen.clicked.connect(self.startToListen)
    
            self.v_box1 = QtWidgets.QVBoxLayout(self)
            self.v_box1.addWidget(self.button_listen)
    
            # no parent with an already existing layout should be set for a new
            # layout; in this case, the current widget already has the v_box1 layout
            # set, therefore the new h_box1 layout should have no argument
            self.h_box1 = QtWidgets.QHBoxLayout()
            self.v_box1.addLayout(self.h_box1)
    
            self.listener = QtCore.QFileSystemWatcher(self)
            self.listener.directoryChanged.connect(self.checkFile)
    
        def startToListen(self):
            fileInfo = QtCore.QFileInfo(fileToWatch)
            if fileInfo.exists():
                self.createStart()
                return
            elif fileInfo.absolutePath() not in self.listener.directories():
                self.listener.addPath(fileInfo.absolutePath())
    
            # create an empty file so that there's no error when trying to open
            # it in the editor
            emptyFile = QtCore.QFile(fileToWatch)
            emptyFile.open(emptyFile.WriteOnly)
            emptyFile.close()
    
            process = QtCore.QProcess(self)
            process.start(editorProgram, [fileToWatch])
    
            # optional: disable the interface until the program has quit
            self.setEnabled(False)
            process.finished.connect(lambda: self.setEnabled(True))
    
        def checkFile(self, path):
            fileInfo = QtCore.QFileInfo(fileToWatch)
            if fileInfo.exists():
                if self.h_box1:
                    # the layout already contains the start button, ignore
                    return
                else:
                    self.createStart()
            else:
                # file has been [re]moved/renamed, maybe do something here...
                pass
    
        def createStart(self):
            self.button_start = QtWidgets.QPushButton('Start', self)
            font2 = self.button_start.font()
            font2.setPointSize(10)
            self.button_start.setFont(font2)
            self.button_start.setFixedSize(200, 50)
            self.h_box1.addWidget(self.button_start, 0, QtCore.Qt.AlignCenter)
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)
        window = ListenWindow()
        window.setWindowTitle('Login')
        window.show()
        sys.exit(app.exec_())