2

I am trying to get my stdout displayed on a QTextEdit made via Qt Designer (PyQt5). Actually I made it work yet it doesn't show the info at the same time it was made. Instead it waits for the process to completely end and only then it shows all the information at once. I understand that this should be solved via threading. Also since the QTextEdit (itself) is a GUI element i need a different approach. I found the answer I was looking for here:

This question is referenced to: Redirecting stdout and stderr to a PyQt4 QTextEdit from a secondary thread

@three_pineapples provided the answer.

My question is pretty much exactly the same, thus the answer is also correct. But my scenario is a little different and I'm having trouble making it work.

In all the threading answers I only see them using Classes. But the thing is in my main class I have a function that does all the stuff that would be printed on the QTextEdit. Sometimes it takes minutes to complete. I am looking for a way for the example code below to work using the answer provided by @three_pineapples.

Here is the example code:

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QTextCursor

from ui_form import Ui_Form


class EmittingStream(QObject):  # test
    textWritten = pyqtSignal(str)

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


class Form(QMainWindow):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        # Install the custom output stream
        sys.stdout = EmittingStream(textWritten=self.normalOutputWritten)  # test

        self.ui = Ui_Form()
        self.ui.setupUi(self)

        self.ui.pushButton_text.clicked.connect(self.test_write)

    def __del__(self):  # test
        # Restore sys.stdout
        sys.stdout = sys.__stdout__

    def normalOutputWritten(self, text):  # test
        """Append text to the QTextEdit."""
        cursor = self.ui.textEdit.textCursor()
        cursor.movePosition(QTextCursor.End)
        cursor.insertText(text)
        self.ui.textEdit.setTextCursor(cursor)
        self.ui.textEdit.ensureCursorVisible()

    def test_write(self):  # this is a long, complicated function. its nested in this class. I don't have a way to get it out as a different class.
        print("something written")


def main():
    app = QApplication(sys.argv)
    form = Form()
    form.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

Is there a way to get the provided solution to work -directly- on the (test_write) function in my main class? How would I implement it?

To make it more clear, from the reference link "LongRunningThing" class is not available for me. The function that needs to run on a separate thread is within the main class (named Form() in the example code). Perhaps a nested class could be used that encapsulates the test_write function inside my main class? Is that even possible?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
shafuq
  • 465
  • 6
  • 20
  • Are you sure this isn't about stream buffering? I'd call `flush` on `sys.stdout` after the print call, or invoke the entire process via `python -u` to test that. – Pedro Silva Apr 28 '18 at 15:11
  • its about threading. As a new programmer I've never used threading before but read a lot and understand the concept. Also since i want the thread to update a gui element -QTextEdit- (which has to be on the main thread) the link provided seems to be exactly what I need. I just don't know how to implement threading to a function within my (Main) Form class. The function uses lots of ui element references from the Main class. This is what prevents me from trying to take it out of it. – shafuq Apr 28 '18 at 15:17
  • @Mr.Robot Why do you want to use threading ?, in your case it is not necessary. It seems that you are a beginner and want to abuse threading. – eyllanesc Apr 28 '18 at 15:55
  • 1
    @eyllanesc thanks for reading my post. You were the one helping me on my last question. At the moment, when I click on the RUN button on my program, it could take a while to compute. Sometimes this takes minutes. During this process I want the stdout to show itself on the QTextEdit. That way it would give me live information from the print statements I've put within my running function(s). I've got it to work to show the stdout.But since the GUI freezes once RUN is pressed, I do not get the print outputs till the very end. Also since my program hangs I cant press anything else. Hence Threading – shafuq Apr 28 '18 at 16:01
  • @Mr.Robot okay, I understand, in that function does not change anything in the GUI? – eyllanesc Apr 28 '18 at 16:03
  • The function does computing. Only print statements to show on the QTextEdit is changed with the mentioned function. Also the RUN button setEnabled is set to False when first pressed. When it's done that button is set back to True so it could be pressed again. – shafuq Apr 28 '18 at 16:07
  • @Mr.Robot Try with my answer – eyllanesc Apr 28 '18 at 16:19

1 Answers1

3

For this case you can use the native threading of python:

import sys

import threading
import time

from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QTextCursor

from ui_form import Ui_Form


class EmittingStream(QObject):  # test
    textWritten = pyqtSignal(str)

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


class Form(QMainWindow):
    finished = pyqtSignal()
    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        # Install the custom output stream
        sys.stdout = EmittingStream(textWritten=self.normalOutputWritten)  # test

        self.ui = Ui_Form()
        self.ui.setupUi(self)

        self.ui.pushButton_text.clicked.connect(self.start_task)
        self.finished.connect(lambda: self.ui.pushButton_text.setEnabled(True))

    def start_task(self):
        var = self.ui.lineEdit.text()
        self.thread = threading.Thread(target=self.test_write, args=(args, ))
        self.thread.start()
        self.ui.pushButton_text.setEnabled(False)

    def __del__(self):  # test
        # Restore sys.stdout
        sys.stdout = sys.__stdout__

    def normalOutputWritten(self, text):  # test
        """Append text to the QTextEdit."""
        cursor = self.ui.textEdit.textCursor()
        cursor.movePosition(QTextCursor.End)
        cursor.insertText(text)
        self.ui.textEdit.setTextCursor(cursor)
        self.ui.textEdit.ensureCursorVisible()

    def test_write(self, *args):
        var1 = args[0]
        print("something written")
        time.sleep(5) # simulate expensive task
        print("something written ----")
        self.finished.emit()


def main():
    app = QApplication(sys.argv)
    form = Form()
    form.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you. I implemented your solution. Its working but with these errors/warnings: QObject::setParent: Cannot set parent, new parent is in a different thread and QBasicTimer::start: QBasicTimer can only be used with threads started with QThread. Also i didn't mention it but the running function works with loops (hundreds, thousands sometimes), so the time i set was 1. Is that the shortest possible? I understand that it delays the program for the mentioned seconds. – shafuq Apr 28 '18 at 17:13
  • @Mr.Robot I think your code is modifying objects that you have not mentioned. What variables are you modifying ?, in my example I do not modify variables created in the GUI thread, use QThread in this case it would have the same effect but you would have to write more code, please provide a code closer to the one you have. – eyllanesc Apr 28 '18 at 17:18
  • I assign two variables within the function from the GUI. var1 = int(self.ui.lineEdit.text()). Both are the same way. Other than that there's nothing else in the function that has the self.ui.xxx format. Let me take a deeper look and get back to you. – shafuq Apr 28 '18 at 17:26
  • @Mr.Robot Do not access the GUI from another thread, in your case I think you should get the data just before starting the thread, and pass it through args, see my updated answer. – eyllanesc Apr 28 '18 at 17:31
  • It's gonna take some time for me to analyze this thoroughly. I'll keep you posted. Thanks again! – shafuq Apr 28 '18 at 17:55
  • Your answer helped me a lot. Thank you @eyllanesc. The problem wasn't the variables, I hard coded them and it didn't change anything. But you were right, I had two functions (within the threading one) that were modifying some ui.labels on the main form. I changed your lambda func to a regular one (named end_task) and put those ui lines there. It all worked! Just a small question if you have the time, I have an exit button with a print("xx") in it, but that doesnt show on the screen. It only has this in it:sys.exit(). my print is before it, tried putting time(5) there but didnt work. Any ideas? – shafuq Apr 29 '18 at 14:43