Wicaledon
Wicaledon

Reputation: 810

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

Upvotes: 0

Views: 289

Answers (2)

musicamante
musicamante

Reputation: 48374

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

Upvotes: 2

abhilb
abhilb

Reputation: 5757

Change the while loop in your abc function to

        while not os.path.exists('b.txt'):
            QtCore.QCoreApplication.processEvents()
            pass

Upvotes: 0

Related Questions