My question is almost the same as this one:
Widget to Display subprocess stdout?
but a step further.
I have the following code (python2.7):
def btnGoClick(p1):
params = w.line.get()
if len(params) == 0:
return
# create child window
win = tk.Toplevel()
win.title('Bash')
win.resizable(0, 0)
# create a frame to be able to attach the scrollbar
frame = ttk.Frame(win)
# the Text widget - the size of the widget define the size of the window
t = tk.Text(frame, width=80, bg="black", fg="green")
t.pack(side="left", fill="both")
s = ttk.Scrollbar(frame)
s.pack(side="right", fill="y")
# link the text and scrollbar widgets
s.config(command=t.yview)
t.config(yscrollcommand=s.set)
frame.pack()
process = subprocess.Popen(["<bashscript>", params], shell=False,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
out = process.stdout.readline()
if out == '' and process.poll() is not None:
break
print out
t.insert(tk.END, out)
The output from the "longrunning" bash script is captured in realtime (appear in the console) but the Tkinter window appear only after the end of the subprocess !!
How can I have the window appearing before the subprocess start and update its content in realtime ?
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()
This is an interesting solution. Would that be possible to have the whole working code?
I am asking because I was wonder how the while True does not block the usability of the whole GUI... does it not?
As suspected, I tried and this example is not really work. If you use something like "ls -Rf /" as command, you will see that the while loop will make the txt output flowing pretty well. However both windows (main and secondary) will block big time.
I suspect you need to send the print txt part in a separated thread or process. Or, if you use pygtk you can use stuff like
gobject.io_add_watch(self.ep1.stdout, # file descriptor
gobject.IO_IN, # condition
self.write_to_buffer ) # callback
which was actually invented for this.
Just in case someone else is looking for it...
log_box_1 = tk.Text(root, borderwidth=3, relief="sunken")
with subprocess.Popen("ls -la", shell=True, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) as p:
for line in p.stdout:
log_box_1.insert(tk.END, line)
From here
Related
How can I change it to non-modal ?
If button is clicked then cursor doesn't change. If I change it to non-modal then it should be ok.
def start_new_proc():
# text.config(cursor="clock")
root.config(cursor="clock")
text.config(state="normal")
command = "ping 8.8.8.8 -c 1"
proc = Popen(command, shell=True, stdout=PIPE)
for line in iter(proc.stdout.readline, ''):
line = line.decode('utf-8')
if line == '':
break
text.insert("end", line)
proc.wait()
# text.config(cursor="")
root.config(cursor="")
text.config(state="disable")
root = tk.Tk()
text = tk.Text(root, state="disabled")
text.pack()
button = tk.Button(root, text="Run", command=start_new_proc)
button.pack()
root.mainloop()
You can achieve this using threading.
import threading
def ping_func(output):
command = "ping 8.8.8.8 -c 1"
proc = Popen(command, shell=True, stdout=PIPE)
for line in iter(proc.stdout.readline, ''):
line = line.decode('utf-8')
if line == '':
break
output.append(line)
def check_complete():
if not ping_thread.is_alive():
root.config(cursor = "")
button.config(state = "normal")
else:
root.after(100, check_complete)
def check_output():
if output != []:
text.config(state = "normal")
text.insert("end", output.pop(0))
text.config(state = "disabled")
root.after(100, check_output)
def start_new_proc():
#text.config(cursor="clock")
root.config(cursor="clock")
button.config(state = "disabled") #Disable button until process completed
global output, ping_thread
output = []
ping_thread = threading.Thread(target = ping_func, args = (output,))
ping_thread.start()
check_output()
check_complete()
root = tk.Tk()
text = tk.Text(root, state="disabled")
text.pack()
button = tk.Button(root, text="Run", command=start_new_proc)
button.pack()
root.mainloop()
By putting the command execution in a thread, it doesn't hold up the tkinter thread. The function ping_func is what the thread runs. It does what the program did before, but the output is different. Because tkinter isn't thread safe, you can't use tkinter objects in other threads. Therefore we need to pass a variable instead which can then be polled by the tkinter thread. This is what the output list is for. The output from ping_func is appended to output. To check the contents of output and insert it into the Text widget, we need a function check_output. This calls itself every 100ms to check if the contents of output have changed. If they have it inserts the new line into the Text widget. There is also check_complete which checks every 100ms if the thread has ended and changes the cursor and enables the button if it has.
Here is one approach, it uses threading and queue to not block the .mainloop() and to not call tkinter methods from another thread (which may potentially break it):
import tkinter as tk
from subprocess import Popen, PIPE
from threading import Thread
from queue import Queue
def start_new_proc():
# text.config(cursor="clock")
root.config(cursor="clock")
text.config(state="normal")
queue = Queue()
Thread(target=lambda: run_proc(queue)).start()
update_text(queue)
def after_proc():
# text.config(cursor="")
root.config(cursor="")
text.config(state="disable")
def update_text(queue):
data = queue.get()
if data == 'break':
after_proc()
return
text.insert('end', data)
root.after(100, update_text, queue)
def run_proc(queue):
command = "ping 8.8.8.8"
proc = Popen(command, shell=True, stdout=PIPE)
for line in iter(proc.stdout.readline, ''):
line = line.decode('utf-8')
if line == '':
queue.put('break')
break
queue.put(line)
# proc.wait()
root = tk.Tk()
text = tk.Text(root, state="disabled")
text.pack()
button = tk.Button(root, text="Run", command=start_new_proc)
button.pack()
root.mainloop()
Quick explanation:
When you click the button, it calls start_new_proc():
Now first all the configurations are run so now the cursor and text are configured (note that the clock cursor is visible only when the cursor is not on Text widget, since the config for text widget is commented out)
Then a queue object is created, this will allow to safely communicate between threads (and it will get garbage collected once the function "stops")
Then a thread is started where the cmd command is run (oh and I changed it a bit (removed "-c 1") because I couldn't test otherwise). So every iteration the data is put into the queue for the main thread to safely receive it, other stuff is the same, also notice that there is '"break"' put into queue once the loop has to stop, so that the other end knows to stop too (probably better to place some object since "break" can be placed there by line variable)
Then the update_text() function is called which receives data from the queue and then inserts it to the text widget (there is also the "break" check) and then the .after() method for making the function loop without blocking the .mainloop() (and also when "break" apppears then configuration function gets called to config the state and cursor)
everyone! I've run into a problem. Let's say I have a script that is launched using subprocess module from tkinter giu like this (params includes the name of the script):
p = Popen(['python.exe'] + params)
During being executed, my script has some messages that I want to be printed in my gui. While I was using console, I just did it with print function like this:
print(f'Connected to {ppm_file}')
What I now need to do, is to make this message be printed in a text widget in tkinter gui.
I suppose it implys using stdout, but I'm a newbie to this concept and find it a little bit hard to understand.
Thanks for your help!
You can capture the console output of the external script and insert the captured output into a Text widget.
Below is an example:
import tkinter as tk
import subprocess
import threading
root = tk.Tk()
logbox = tk.Text(root, width=80, height=20)
logbox.pack()
def capture_output():
# "python -u" make stdout stream to be unbuffered
p = subprocess.Popen(["python", "-u", "other.py"], stdout=subprocess.PIPE)
while p.poll() is None: # process is still running
logbox.insert("end", p.stdout.readline())
logbox.see("end")
logbox.insert("end", "--- done ---\n")
# use thread so that it won't block the application
tk.Button(root, text="Run", command=lambda: threading.Thread(target=capture_output, daemon=True).start()).pack()
root.mainloop()
In the text widget, you can insert by using following method:
txtbox.delete('1.0', END) # to remove any preceding text(ignore if empty)
txtbox.insert(END, 'Connected to {ppm_file}') # insert the text you want
txtbox.insert(END, '\n') # additional insertion for example a newline character
So, I used Tkinter to create a widget that allows the user to input some information and click the run button, which will begin running a test that is defined elsewhere. Here is the code. It is far from perfected, this is just a prototype:
from tkinter import*
import controller
root = Tk()
#create labels
label = Label(text = "text you don't need to know")
label.pack()
remind = Label(text = "more text you don't need to know")
remind.pack()
#create text fields
name = Entry(root)
name.pack()
name.insert(0, "Name")
name.focus_set()
testName = Entry(root)
testName.pack()
testName.insert(0, "Test name")
duration = Entry(root)
duration.pack()
duration.insert(0, "Duration in minutes")
def runTest():
controller.main(testName.get(), name.get(), float(duration.get()))
#create run button
run = Button(root, text = "Run", fg = "red", width = 10, command = runTest)
run.pack()
root.mainloop()
So, here is my issue. Once this project is implemented, the duration will likely be set for something like 1-4 hours. So, what I would like to do is have a countdown appear on the widget, so the users can reference that timer at any time to see how long until their data is produced. The problem is that as soon as my test is running, the widget locks up until it is complete. Everything I've tried is put on hold until it finishes running the test, then it does what I wanted. It doesn't help very much at that point.
Anybody have some experience at implementing something like this?
Thanks.
You'll need to fork off your work in runTest. The threading module will be your friend (e.g. from threading import Thread).
Then rewrite your runTest method:
def runTest():
# pack your arguments in a tuple
mainArgs = (testName.get(), name.get(), float(duration.get()))
# create a thread object armed with your function and the args to call it with
thread = Thread(target=controller.main, args=mainArgs)
# launch it
thread.start()
#and remember, never set state (directly or indirectly) from separate threads without taking appropriate precautions!
I'm using tkinter with Python to create a user interface for a program that converts Excel files to CSV.
I created a label to act as a status bar, and set statusBarText as a StringVar() as the textvariable. inputFileEntry and outputFileEntry are textvariables that contain the input and output file paths.
def convertButtonClick():
statusBarText.set('Converting...')
if inputFileEntry.get() == '' or outputFileEntry.get() == '':
statusBarText.set('Invalid Parameters.')
return
retcode = subprocess.('Program.exe' ,shell=true)
if retcode == 0:
statusBarText.set('Conversion Successful!')
else:
statusBarText.set('Conversion Failed!')
This function gets called when you click the convert button, and everything is working fine EXCEPT that the status bar never changes to say 'Converting...'.
The status bar text will get changed to invalid parameters if either the input or output are empty, and it will change to success or failure depending on the return code. The problem is it never changes to 'Converting...'
I've copied and pasted that exact line into the if statements and it works fine, but for some reason it just never changes before the subprocess runs when its at the top of the function. Any help would be greatly appreciated.
Since you're doing all of this in a single method call, the GUI never gets a chance to update before you start your sub process. Check out update_idletasks() call...
from http://infohost.nmt.edu/tcc/help/pubs/tkinter/universal.html
w.update_idletasks()
Some tasks in updating the display, such as resizing and redrawing widgets, are called idle tasks because they are usually deferred until the application has finished handling events and has gone back to the main loop to wait for new events.
If you want to force the display to be updated before the application next idles, call the w.update_idletasks() method on any widget.
How are you creating your Label?
I have this little test setup:
from Tkinter import *
class LabelTest:
def __init__(self, master):
self.test = StringVar()
self.button = Button(master, text="Change Label", command=self.change)
self.button.grid(row=0, column=0, sticky=W)
self.test.set("spam")
self.testlabel = Label(master, textvariable = self.test).grid(row = 0,column = 1)
def change(self):
self.test.set("eggs")
root = Tk()
root.title("Label tester")
calc = LabelTest(root)
root.mainloop()
And it works.
Did you make sure to use "textvariable = StatusBarText" instead of "text=StatusBarText.get()"?
I have created a program that prints results on command line.
(It is server and it prints log on command line.)
Now, I want to see the same result to GUI .
How can I redirect command line results to GUI?
Please, suggest a trick to easily transform console application to simple GUI.
Note that it should work on Linux and Windows.
You could create a script wrapper that runs your command line program as a sub process, then add the output to something like a text widget.
from tkinter import *
import subprocess as sub
p = sub.Popen('./script',stdout=sub.PIPE,stderr=sub.PIPE)
output, errors = p.communicate()
root = Tk()
text = Text(root)
text.pack()
text.insert(END, output)
root.mainloop()
where script is your program. You can obviously print the errors in a different colour, or something like that.
To display subprocess' output in a GUI while it is still running, a portable stdlib-only solution that works on both Python 2 and 3 has to use a background thread:
#!/usr/bin/python
"""
- read output from a subprocess in a background thread
- show the output in the GUI
"""
import sys
from itertools import islice
from subprocess import Popen, PIPE
from textwrap import dedent
from threading import Thread
try:
import Tkinter as tk
from Queue import Queue, Empty
except ImportError:
import tkinter as tk # Python 3
from queue import Queue, Empty # Python 3
def iter_except(function, exception):
"""Works like builtin 2-argument `iter()`, but stops on `exception`."""
try:
while True:
yield function()
except exception:
return
class DisplaySubprocessOutputDemo:
def __init__(self, root):
self.root = root
# start dummy subprocess to generate some output
self.process = Popen([sys.executable, "-u", "-c", dedent("""
import itertools, time
for i in itertools.count():
print("%d.%d" % divmod(i, 10))
time.sleep(0.1)
""")], stdout=PIPE)
# launch thread to read the subprocess output
# (put the subprocess output into the queue in a background thread,
# get output from the queue in the GUI thread.
# Output chain: process.readline -> queue -> label)
q = Queue(maxsize=1024) # limit output buffering (may stall subprocess)
t = Thread(target=self.reader_thread, args=[q])
t.daemon = True # close pipe if GUI process exits
t.start()
# show subprocess' stdout in GUI
self.label = tk.Label(root, text=" ", font=(None, 200))
self.label.pack(ipadx=4, padx=4, ipady=4, pady=4, fill='both')
self.update(q) # start update loop
def reader_thread(self, q):
"""Read subprocess output and put it into the queue."""
try:
with self.process.stdout as pipe:
for line in iter(pipe.readline, b''):
q.put(line)
finally:
q.put(None)
def update(self, q):
"""Update GUI with items from the queue."""
for line in iter_except(q.get_nowait, Empty): # display all content
if line is None:
self.quit()
return
else:
self.label['text'] = line # update GUI
break # display no more than one line per 40 milliseconds
self.root.after(40, self.update, q) # schedule next update
def quit(self):
self.process.kill() # exit subprocess if GUI is closed (zombie!)
self.root.destroy()
root = tk.Tk()
app = DisplaySubprocessOutputDemo(root)
root.protocol("WM_DELETE_WINDOW", app.quit)
# center window
root.eval('tk::PlaceWindow %s center' % root.winfo_pathname(root.winfo_id()))
root.mainloop()
The essence of the solution is:
put the subprocess output into the queue in a background thread
get the output from the queue in the GUI thread.
i.e., call process.readline() in the background thread -> queue -> update GUI label in the main thread. Related kill-process.py (no polling -- a less portable solution that uses event_generate in a background thread).
Redirecting stdout to a write() method that updates your gui is one way to go, and probably the quickest - although running a subprocess is probably a more elegant solution.
Only redirect stderr once you're really confident it's up and working, though!
Example implimentation (gui file and test script):
test_gui.py:
from Tkinter import *
import sys
sys.path.append("/path/to/script/file/directory/")
class App(Frame):
def run_script(self):
sys.stdout = self
## sys.stderr = self
try:
del(sys.modules["test_script"])
except:
## Yeah, it's a real ugly solution...
pass
import test_script
test_script.HelloWorld()
sys.stdout = sys.__stdout__
## sys.stderr = __stderr__
def build_widgets(self):
self.text1 = Text(self)
self.text1.pack(side=TOP)
self.button = Button(self)
self.button["text"] = "Trigger script"
self.button["command"] = self.run_script
self.button.pack(side=TOP)
def write(self, txt):
self.text1.insert(INSERT, txt)
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack()
self.build_widgets()
root = Tk()
app = App(master = root)
app.mainloop()
test_script.py:
print "Hello world!"
def HelloWorld():
print "HelloWorldFromDef!"
Sorry for my bad English. I actually, used a different way to print Command Prompt output into my new Automation tool.
Please find those steps below.
1> Create a Bat File & redirect its output to a LOG file.
Command Prompt command: tasklist /svc
2> Make read that file with Python 3.x.
`processedFile = open('D:\LOG\taskLog.txt', 'r')
3> The Finale step.
ttk.Label(Tab4, text=[ProcessFile.read()]).place(x=0, y=27)
**Hence please be informed that, I have not include scrollbar into this code yet.
Posting Screenshot: