Search code examples
pythontimesleeppause

Pausing Python Code When Console Output Has Been Re-Routed to GUI


I borrowed a design that I found on stackoverflow to redirect console output to a PyQt5 GUI textEdit widget. This works fine, but the text is not displayed in "real-time". It seems to output the text to the GUI once a process has completed. This has not been a problem until I tried to use time.sleep(secs) to print something, pause, then print something else. What ends up happening is that the program pauses for secs, then it prints all of the statements at once.

This class is in the mainWindow file for the GUI:

class EmittingStream(QtCore.QObject):

    textWritten = QtCore.pyqtSignal(str)

    def write(self, text):
        self.textWritten.emit(str(text))

This is in the __init__ method of my event handling file:

sys.stdout = EmittingStream(textWritten=self.normalOutputWritten)
self.case_setup_console.setReadOnly(True)
self.main_console.setReadOnly(True)

This function is in the main class of event handling file (outside __init__):

    def normalOutputWritten(self, text):
        """Append text to the QTextEdit."""
        # Maybe QTextEdit.append() works as well, but this is how I do it:
        cursor = self.case_setup_console.textCursor()
        cursor.movePosition(QtGui.QTextCursor.End)
        cursor.insertText(text)
        self.case_setup_console.setTextCursor(cursor)
        self.case_setup_console.ensureCursorVisible()

This works as intended to re-route the output to the text edit widget self.case_setup_console. But, when I try to run a code such as:

print('This is the first message')
time.sleep(5)
print('This should print 5 seconds later')

What happens is that the program waits 5 seconds, then it prints both statements together.


Solution

  • When programing for GUI code, there is a fundamental shift in how the program is designed. To make it short: after building and initialisation, the program is all the time running in an "event loop" provided by the GUI framework, and your code is only called when specific events take place.

    That is in contrast with a terminal application where your code is running all the time, and you tell when to do "print"s, "input"s and pauses with "time.sleep".

    The GUI code is responsible for taking notes of events (keyboard, UI, network, etc...), redrawing window contents and calling your code in response to events, or just when it is time to redraw a content that is defined in your code (like updating a background image).

    So, it can only render the text that is supposed to show up in a window, with your redirected "print", when the control is passed back to its event loop. When you do time.sleep you pause the return - no code in the event loop is run, and it can't, of course, do any screen drawing.

    What is needed is that you write your pauses in the program in a way that during the pause, the GUI event loop is running - not "time.sleep", that just suspends your whole thread.

    In Qt the way to do that is create a QTimer object to call the code you want to use to print text at a particular moment, and then just surrender the execution to the the QtMainloop by returning from your function.

    Thanks to Python's support for nested functions, that can be done in painless ways, even using lambda functions when setting the timer itself.

    ...
    print('This is the first message')
    timer = QtCore.QTimer
    
    timer.singleShot(5000, lambda *_: print('This should print 5 seconds later'))
    

    Should work for the given example. (The call, as usual for UIs, takes the pause time in miliseconds rather than in seconds).

    If you will need to schedule more text to be printed after each phrase is output, you will need to call the scheduling inside the callback itself, and will need a little more sophistication, but it still could be as simple as:

    phrases = iter(("hello", "world", "I", "am", "talking", "slowly!"))
    timer = QtCore.QTimer()
    
    def talker(*_):
        phrase = next(phrases, None)
        if not phrase: 
             return
        print(phrase)
        timer.singleShot(1000, talker)
    
    timer.singleShot(1000, talker)
    

    (Note that there is nothing special about the *_ parameter name either: I am just indicating that there might be any number of positional arguments for the callback - (The "*" part, that is Python syntax) - and that I won't care about then (I call the argument sequence as "_" to indicate I don't care how this is called, as it won't be used anyway - that is a coding convention) )

    The iter and next calls are more "Python dialect" than one might be used, but one could just use a list and a counter up to the list length all the same.