Display Realtime Output of a Subprocess in a Tkinter Widget

Display realtime output of a subprocess in a tkinter widget

Finally I found the solution.
After the window construction, you must add :

frame.pack()
# force drawing of the window
win.update_idletasks()

And then after every line insertion in the widget, you must also force a refresh with the same method only on the widget.

# insert the line in the Text widget
t.insert(tk.END, out)
# force widget to display the end of the text (follow the input)
t.see(tk.END)
# force refresh of the widget to be sure that thing are displayed
t.update_idletasks()

How can I write output from a Subprocess to a text widget in tkinter in realtime?

welcome to stack overflow. I modified your code a bit (you had a , after defining start_button and didnt import sys, also i put your code below ##### Window Setting #### into a boilerplate-code).

Your main issue was, that you do not make your Text widget available in your run and furthermore in your test function (executed as a thread). So i handed over your widget as an argument to both functions (probably not the most pythonic way, however). For executing a command bound to a button i used from functools import partial and binded the command including an argument via command=partial(run, textbox). Then i simply handed over the argument in run to the thread with args=[textbox] in the line where you create & start the thread. Finally, i updated the textbox with textbox.insert(tk.END, msg + "\n") in your test function while removing the print(). The insert appends any text at the end to the textbox, the "\n" starts a new line.

Here is the (slightly restructured) complete code (app.py):

import tkinter as tk
import subprocess
import threading
import sys
from functools import partial

# ### classes ####

class Redirect:

def __init__(self, widget, autoscroll=True):
self.widget = widget
self.autoscroll = autoscroll

def write(self, textbox):
self.widget.insert('end', textbox)
if self.autoscroll:
self.widget.see('end') # autoscroll

def flush(self):
pass

def run(textbox=None):
threading.Thread(target=test, args=[textbox]).start()

def test(textbox=None):

p = subprocess.Popen("python myprogram.py".split(), stdout=subprocess.PIPE, bufsize=1, text=True)
while p.poll() is None:
msg = p.stdout.readline().strip() # read a line from the process output
if msg:
textbox.insert(tk.END, msg + "\n")

if __name__ == "__main__":
fenster = tk.Tk()
fenster.title("My Program")
textbox = tk.Text(fenster)
textbox.grid()
scrollbar = tk.Scrollbar(fenster, orient=tk.VERTICAL)
scrollbar.grid()

textbox.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=textbox.yview)

start_button = tk.Button(fenster, text="Start", command=partial(run, textbox))
start_button.grid()

old_stdout = sys.stdout
sys.stdout = Redirect(textbox)

fenster.mainloop()
sys.stdout = old_stdout

And here is the code of the test-file myprogram.py i created:

import time

for i in range(10):
print(f"TEST{i}")
time.sleep(1)

Widget to Display subprocess stdout?

You may want to use the Tkinter.Text widget, described here.

The description is quite long, but what you need to understand is mainly that this widget can be used as a buffer, as text should be inserted inside.

So, for each new line in the subprocess output, you will have to insert text where you want it. Example:

t = Tkinter.Text(root)
while some_condition:
s = p.readline() # get subprocess output
t.insert(END, s)

EDIT Have a look here for getting subprocess output line by line.

You may also want to have a look to Tkinter.Scrollbar and Text.see() to tune the display.

Python 3 - Getting realtime output of cmd in Tkinter

You can solve this in two ways, first better for YT is to use pafy library, base of pafy is ytdl so it can be done since i had made that but it was slow, and i got another idea that i'am developing now, and with little modification you can connect pafys output with tkinter wodget.Second solution is

def sudo(self, cmnd, terminal, top):   # 1

sudo_password = 'your sudo code' + '\n'
sudos = ['sudo', '-S']

terminal.delete('1.0', END)

for item in eval(cmnd):
cmd = sudos + item.split()

p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, universal_newlines=True)
p.stdin.write(sudo_password)
p.poll()

while True:
line = p.stdout.readline()
terminal.insert(END, line)
terminal.see(END)
top.updates()
if not line and p.poll is not None: break

while True:
err = p.stderr.readline()
terminal.insert(END, err)
terminal.see(END)
top.updates()
if not err and p.poll is not None: break
terminal.insert(END, '\n * END OF PROCESS *')

cmnd - list of commands you want to execute, ['youtube-dl some link'], with even one command it should be LIST

terminal - thats Text widget in my app, but you can use any wiget as well, only you would have to change all lines terminal.insert(END, 'some text') to terminal.insert(0, 'some text') - END to 0

top is scrollbar container for my app which you can remove if you don't need it

of course you have to provide root=Tk(), parents and other containers for the terminal widget .

TKinter GUI freezes until subprocess ends and realtime output to text Widget

Maybe this could help others, I solved the issue replacing '\n' with endl. It seems cout in the while loop is buffered and the stdout flush is called only after a while, while with endl the function is called after every cycle

Issues intercepting subprocess output in real time

The problem here is that process.stdout.readline() will block until a full line is available. This means the condition line == '' will never be met until the process exits. You have two options around this.

First you can set stdout to non-blocking and manage a buffer yourself. It would look something like this. EDIT: As Terry Jan Reedy pointed out this is a Unix only solution. The second alternative should be preferred.

import fcntl
...

def startProcess(self):
self.process = subprocess.Popen(['./subtest.sh'],
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=0) # prevent any unnecessary buffering

# set stdout to non-blocking
fd = self.process.stdout.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)

# schedule updatelines
self.after(100, self.updateLines)

def updateLines(self):
# read stdout as much as we can
line = ''
while True:
buff = self.process.stdout.read(1024)
if buff:
buff += line.decode()
else:
break

self.console.config(state=tkinter.NORMAL)
self.console.insert(tkinter.END, line)
self.console.config(state=tkinter.DISABLED)

# schedule callback
if self.process.poll() is None:
self.after(100, self.updateLines)

The second alternative is to have a separate thread read the lines into a queue. Then have updatelines pop from the queue. It would look something like this

from threading import Thread
from queue import Queue, Empty

def readlines(process, queue):
while process.poll() is None:
queue.put(process.stdout.readline())
...

def startProcess(self):
self.process = subprocess.Popen(['./subtest.sh'],
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE)

self.queue = Queue()
self.thread = Thread(target=readlines, args=(self.process, self.queue))
self.thread.start()

self.after(100, self.updateLines)

def updateLines(self):
try:
line = self.queue.get(False) # False for non-blocking, raises Empty if empty
self.console.config(state=tkinter.NORMAL)
self.console.insert(tkinter.END, line)
self.console.config(state=tkinter.DISABLED)
except Empty:
pass

if self.process.poll() is None:
self.after(100, self.updateLines)

The threading route is probably safer. I'm not positive that setting stdout to non-blocking will work on all platforms.



Related Topics



Leave a reply



Submit