简体   繁体   中英

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

I am trying to add some features to a GUI i created some time ago, in particular the function I need is a text widget where the terminal commands I send show their output. The redirector class looks like this at the moment:

class StdRed(object):
    def __init__(self, textwid):
        self.text_space = textwid

    def write(self, text):
        self.text_space.config(state=NORMAL)
        self.text_space.insert(END,text)
        self.text_space.see(END)
        self.text_space.update_idletasks()
        self.text_space.config(state=DISABLED)

    def flush(self):
        pass

and indeed it works. I replaced the os.system(...) command to open terminal commands with

a = subprocess.Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)

and I read stdout through: b = a.stdout.read() without a single problem (unfortunately i need that shell=True, otherwise some programs i need to call fail miserably). After that I tried to have a realtime output on the tkinter text widget, so I changed b -->

while True:
    b = a.stdout.readline().rstrip()
    if not b:
        break
    print b 

but it seems that the output appears only when the called process ends, ie a simple C software like

for(int i = 0; i<100000; i++){ cout << i << '\\n';}

will print very slowly (I remark slowly given that a simple "ls" command will be printed line by line very slowly too) all the numbers at the end of the for cycle. Other than that I noticed that the GUI is frozen while the programs called through subprocess are run. Any ideas on how to solve these problems?

EDIT :

I created a simple terminal which runs commands using the multiprocessing class and Popen:

from Tkinter import *
from multiprocessing import Process, Pipe, Queue
import sys
from subprocess import PIPE, Popen, STDOUT

root = Tk()
root.title("Test Terminal")
root.resizable(False, False)

class StdRed(object):
    def __init__(self, textwid):
        self.text_space = textwid

    def write(self, text):
        self.text_space.config(state=NORMAL)
        self.text_space.insert(END,text)
        self.text_space.see(END)
        self.text_space.update_idletasks()
        self.text_space.config(state=DISABLED)

    def flush(self):
        pass

terminal = Frame(root, bd=2, relief=GROOVE)
terminal.grid(row=0, sticky='NSEW')
TERM = Label(terminal, text='TERMINAL', font='Helvetica 16 bold')
TERM.grid(row=0, pady=10, sticky='NSEW')
termwid = Text(terminal, height=10)
termwid.grid(row=1, sticky='NSEW')   
termwid.configure(state=DISABLED, font="Helvetica 12")   
sys.stdout = StdRed(termwid) 
enter = StringVar()
enter.set("")
termen = Entry(terminal, textvariable=enter)
queue = Queue(maxsize=1)
a = None

def termexe(execute):
    a = Popen(execute, shell=True, stdout=PIPE, stderr=STDOUT) 
    while True:
        line = a.stdout.readline().rstrip()
        if not line:
            break
        else:
            queue.put(line)     
    queue.put('') 

def labterm(thi):
    if queue.empty():
        if thi != None:
            if thi.is_alive():
                root.after(0,lambda:labterm(thi))
            else:
                pass    
        else:
            pass                    
    else:
        q = queue.get()       
        print q
        root.after(0,lambda:labterm(thi))     


def comter(event=None, exe=None, seq=None):
    global enter   
    if seq == 1:
        if exe != None:     
            th = Process(target=termexe, args=(exe,))
            th.daemon = True
            th.start()
            labterm(th)
            th.join()
        else:
            pass
    else:            
        if exe != None:     
            th = Process(target=termexe, args=(exe,))
            th.daemon = True
            th.start()
            labterm(th)
        else:
            th = Process(target=termexe, args=(enter.get(),))
            th.daemon = True
            th.start()
            enter.set('')        
            labterm(th)

def resetterm():
    global termwid
    termwid.config(state=NORMAL)
    termwid.delete(1.0, END)
    termwid.config(state=DISABLED)    

termen.bind('<Return>', comter)
resterm = Button(terminal, text="Clear", command=resetterm)
terbut = Button(terminal, text="Command", command=comter)
termen.grid(row=2, sticky='NSEW')
terbut.grid(row=3, sticky='NSEW')
resterm.grid(row=4, sticky='NSEW')        

root.mainloop()

The problem is the acquisition is still not in real-time. Running from the entry in the software the program:

#include <iostream>
using namespace std;

int main()
{
    int i = 0;
    while(1)
    {
     cout << i << '\n';
     i++;
     int a = 0;
     while(a < 10E6)
     {
        a++;
     }
    }    
}

leads to nothing inside the text widget for a while and, after some time, the output appears suddenly. Any ideas on how to solve this problem?

The solution here is to use threading, otherwise the script wait till the job is done to make the GUI responsive again. With threading, your program will be running both the job and the GUI at the same time, an example of code:

import threading

def function():
    pass

t = threading.Thread(target=function)
t.daemon = True # close pipe if GUI process exits
t.start()

I used this std redirector:

class StdRedirector():
    """Class that redirects the stdout and stderr to the GUI console"""
    def __init__(self, text_widget):
        self.text_space = text_widget

    def write(self, string):
        """Updates the console widget with the stdout and stderr output"""
        self.text_space.config(state=NORMAL)
        self.text_space.insert("end", string)
        self.text_space.see("end")
        self.text_space.config(state=DISABLED)

I tried using threads as suggested by @Pau B (switched to multiprocessing in the end), and I solved indeed the problem of the stuck GUI. The issue now is that running the program

for(int i = 0; i<100000; i++){ cout << i << '\\n';}

doesn't return a realtime output, but it seems it is buffered and then appears after some time into the text widget. The code I'm using looks like this:

class StdRed(object):
    def __init__(self, textwid):
        self.text_space = textwid

    def write(self, text):
        self.text_space.config(state=NORMAL)
        self.text_space.insert(END,text)
        self.text_space.see(END)
        self.text_space.update_idletasks()
        self.text_space.config(state=DISABLED)

def termexe(execute):
        a = Popen(execute, shell=True, stdout=PIPE, stderr=STDOUT) 
        while True:
            line = a.stdout.readline().rstrip()
            if not line:
                break
            else:
                queue.put(line)    
        queue.put('') 

def labterm():
    global th
    if queue.empty():
        if th != None:
            if th.is_alive():
                root.after(0,labterm)
            else:
                pass    
        else:
            pass                    
    else:
        q = queue.get()        
        print q
        root.after(1,labterm)
def comter(event=None):
    global enter   
    global th
    if th != None:
        if not th.is_alive():
            th = Process(target=termexe, args=(enter.get(),))
            th.start()
        else:
            pass 
    else:    
        th = Process(target=termexe, args=(enter.get(),))
        th.start()      
    enter.set('')
    labterm()

where comter() is called by a button or binded to 'Return' inside a text entry.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM