Display terminal output with tqdm in QPlainTextEdit
Asked Answered
E

1

2

I'm trying to find a way of getting, along with other prints, the result/evolution of a progress bar in a pyqt application, for example in a QPlainTextEdit widget.

The problem I'm facing, is that progress bars can use some more advanced carriage return, or even more advanced cursor positionning that are mostly not supported by treams. I've tried io.StringIO, but the \r is kept literal.

import io
from tqdm import tqdm
s = io.StringIO()
for i in tqdm(range(3), file=s):    
    sleep(.1)

output:

s.getvalue()

Out[24]: '\n\r  0%|          | 0/3 [00:00<?, ?it/s]\x1b[A\n\r 33%|###3      | 1/3 [00:00<00:00,  9.99it/s]\x1b[A\n\r 67%|######6   | 2/3 [00:00<00:00,  9.98it/s]\x1b[A\n\r100%|##########| 3/3 [00:00<00:00,  9.98it/s]\x1b[A\n\x1b[A'

which translate into:

print(s.getvalue())
  0%|          | 0/3 [00:00<?, ?it/s]
 33%|###3      | 1/3 [00:00<00:00,  9.99it/s]
 67%|######6   | 2/3 [00:00<00:00,  9.98it/s]
100%|##########| 3/3 [00:00<00:00,  9.98it/s]

To be clear, in my output, I don't want one line per tqdm update, but just the current state, as it would be printed on the command line.

Any idea o how to do this ? Thanks!

Ewell answered 19/11, 2018 at 20:14 Comment(4)
what is text widget?Death
any widget that display multiline text, for example QPlainTextEditEwell
yes, of course, that was for the purpose of demonstration.Ewell
Thanks for helping ! I want basically the same thing as what I see in the standard output, but in a string, or anything I can display in a qt widget.Ewell
D
5

The idea is to remove the previous line if there is a new text added, but you must also remove \r and verify that it is not an empty text. Also, for an object to receive the text of tqdm, it must only have the write() method, so implement a custom QPlainTextEdit. Use QMetaObject::invokeMethod() to make it thread-safe

import time
import threading
from tqdm import tqdm
from PyQt5 import QtCore, QtGui, QtWidgets
import lorem

class LogTextEdit(QtWidgets.QPlainTextEdit):
    def write(self, message):
        if not hasattr(self, "flag"):
            self.flag = False
        message = message.replace('\r', '').rstrip()
        if message:
            method = "replace_last_line" if self.flag else "appendPlainText"
            QtCore.QMetaObject.invokeMethod(self,
                method,
                QtCore.Qt.QueuedConnection, 
                QtCore.Q_ARG(str, message))
            self.flag = True
        else:
            self.flag = False

    @QtCore.pyqtSlot(str)
    def replace_last_line(self, text):
        cursor = self.textCursor()
        cursor.movePosition(QtGui.QTextCursor.End)
        cursor.select(QtGui.QTextCursor.BlockUnderCursor)
        cursor.removeSelectedText()
        cursor.insertBlock()
        self.setTextCursor(cursor)
        self.insertPlainText(text)

def foo(w):
    for i in tqdm(range(100), file=w):
        time.sleep(0.1)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = LogTextEdit(readOnly=True)
    w.appendPlainText(lorem.paragraph())
    w.appendHtml("Welcome to Stack Overflow")
    w.show()
    threading.Thread(target=foo, args=(w,), daemon=True).start()
    sys.exit(app.exec_())
Death answered 19/11, 2018 at 23:37 Comment(1)
Thanks, I need a bit of time to test if it fits my need, but be sure I'll accept as soon as I got it workingEwell

© 2022 - 2024 — McMap. All rights reserved.