This is a simple gui.
I am taking url from the user in the Entry. When the button is pressed, url is saved in a file and another function is called to start another process through call (within subprocess).
while the process runs, I want to show the indeterminate progress bar (until the button is hit the bar needs to be hidden) and when the process is completed a showinfo message displays to destroy the gui.
Problem: The bar doesn't show up until the process is finished. After the showinfo dialog is displayed, only then it starts progressing. Means, the bar starts progressing the moment it should actually get destroyed.
What is wrong with my code?
import scrapy
import tkinter as tk
from tkinter import messagebox as tkms
from tkinter import ttk
import shlex
from subprocess import call
def get_url():
# get value from entry and write to a file
def scrape():
progress_bar = ttk.Progressbar(root, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
progress_bar.grid(row=3, column=2)
progress_bar.start(10)
command_line = shlex.split('scrapy runspider /media/mayank/Local/Coding/Lab/Scraping/Practices/img.py')
call(command_line)
mes = tkms.showinfo(title='progress', message='Scraping Done')
if mes == 'ok':
root.destroy()
root = tk.Tk()
root.title("Title")
entry1 = tk.Entry(root, width=90, textvariable=url)
entry1.grid(row=0, column=0, columnspan=3)
my_button = tk.Button(root, text="Process", command=lambda: [get_url(), scrape()])
my_button.grid(row=2, column=2)
root.mainloop()
----Updated Code ---
import scrapy
import tkinter as tk
from tkinter import messagebox as tkms
from tkinter import ttk
import shlex
from subprocess import call
def get_url():
# get value from entry and write to a file
scrapy = None
def watch():
global scrapy
if scrapy:
if scrapy.poll() != None:
# Update your progressbar to finished.
progress_bar.stop()
progress_bar.destroy()
# Maybe report scrapy.returncode?
print(f'scrapy return code =--######==== {scrapy.returncode}')
scrapy = None
else:
# indicate that process is running.
progress_bar.start(10)
print(f'scrapy return code =--######==== {scrapy.returncode}')
# Re-schedule `watch` to be called again after 0.1 s.
root.after(100, watch)
def scrape():
global scrapy
command_line = shlex.split('scrapy runspider ./img.py')
scrapy = Popen(command_line)
watch()
mes = tkms.showinfo(title='progress', message='Scraping Done')
if mes == 'ok':
root.destroy()
root = tk.Tk()
root.title("Title")
url = tk.StringVar(root)
entry1 = tk.Entry(root, width=90, textvariable=url)
entry1.grid(row=0, column=0, columnspan=3)
my_button = tk.Button(root, text="Process", command=lambda: [get_url(), scrape()])
my_button.grid(row=2, column=2)
progress_bar = ttk.Progressbar(root, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
progress_bar.grid(row=3, column=2)
root.mainloop()
Using subprocess.call interrupts the current process until the called process is finised.
So the GUI won't update until the call is finished.
Important takeaway: Never call subprocess.run, subprocess.call or one of the other convenience functions from the main thread of a tkinter program. Doing so will freeze the GUI. You should only create subprocess.Popen objects from the main thread.
What you should do instead is create a Popen object, while at the same time disabling the start button.
To track the progress, define a function that is periodically called with root.after(), say every 0.1 s.
In this function you could call the poll() method to check if the subprocess has finished.
Alternatively, you could set stdout=subprocess.PIPE and read the data from the subprocess from the stdout attribute of the Popen object.
The code below is a working (for me) example based on your updated question.
Note that I have replaced scrapy (which I don't have) with a relative long-running command on my UNIX machine.
Since you are running scrapy as a subprocess, you should not need import scrapy.
import tkinter as tk
from tkinter import messagebox as tkms
from tkinter import ttk
from subprocess import Popen
proc = None
def watch():
global proc
if proc:
if proc.poll() is not None:
# Update your progressbar to finished.
progress_bar.stop()
progress_bar.destroy()
# Maybe report proc.returncode?
print(f'proc return code =--######==== {proc.returncode}')
proc = None
mes = tkms.showinfo(title='progress', message='Scraping Done')
if mes == 'ok':
root.destroy()
else:
# indicate that process is running.
progress_bar.start(10)
# print(f'proc return code =--######==== {proc.returncode}')
# Re-schedule `watch` to be called again after 0.1 s.
root.after(100, watch)
def scrape():
global proc
command_line = ['netstat']
proc = Popen(command_line)
watch()
root = tk.Tk()
root.title("Title")
url = tk.StringVar(root)
entry1 = tk.Entry(root, width=90, textvariable=url)
entry1.grid(row=0, column=0, columnspan=3)
my_button = tk.Button(root, text="Process", command=lambda: [get_url(), scrape()])
my_button.grid(row=2, column=2)
progress_bar = ttk.Progressbar(root, orient=tk.HORIZONTAL, length=300, mode='indeterminate')
progress_bar.grid(row=3, column=2)
root.mainloop()
Related
I m sorry if it's a little confusing I will try to explain as simple as possible way, I have been trying to solve this threading issue for the past 2 days.
In short, what I m doing is running a big script on a separate thread once I hit the run button from the first screen, it reads data from the JSON file I have stored in the directory and Run a job.
Which gradually after completing steps increases the progress bar value and updates the label status from the run_script function.
And the error is after completing the first cycle when it comes back to first screen and if I run another job it doesnt work because of progressbar and label.
What I've tried so far is destroying the window from script_function and recreating it on the first screen. (2nd job worked but it didn't destroy the first job window, and after completing 2nd job it completely shutdown both windows)
without mttkinter, I got tcl run time error because I m trying to destroy the window from another thread.
from mttkinter import mtTkinter as tk
from tkinter import ttk
import threading
#other imports
window = Tk()
def screen1():
##show the screen with json data labels and buttons#
window.geometry("1215x770")
window.configure(bg="#FFFFFF")
b0 = Button(window, text='Run job', command=do_something) #samplebutton
window.resizable(False, False)
window.mainloop()
def do_something():
##running big script with progressbar and status label.
window.geometry("1215x770")
window.configure(bg="#FFFFFF")
progress = ttk.Progressbar(window, style='red.Horizontal.TProgressbar', orient=HORIZONTAL,
length=443, mode='determinate')
progress.pack()
label = Label("Completed tables 0/{totaltables}") #samplelabel
label.pack()
if data['foo'] == 'bar':
threading.Thread(target=run_script, args=(arg1, arg2,)).start() #run script in seperate thread this script also has a function that runs on multiple threads.
window.resizable(False, False)
window.mainloop()
def run_script(arg1, arg2):
#running the script#
for i in range(5): #running each i on new thread followed by starting and joining.
print(i)
#main problem#
#once script finishes#
response = messagebox.showinfo("Task", "Did something!")
if response:
screen1()
What is the possible way to accomplish this?
I want to keep running jobs without having to open the app again and again.
the script should be on a separate thread because if it's not the app goes not responding.
I want to keep showing the progress bar and status and once it's complete go back to the first screen.
How do I kill the main Tkinter thread after completing the first job?
or How do I destroy the Tkinter window from another thread?
If you have a tcl/tk that is built with multithreading support, and you are using Python 3, you should be able to call tk functions from a second thread.
This is an example program, where a second thread modifies widgets:
import os
import sys
import threading
import time
import tkinter as tk
import tkinter.font as tkfont
import types
__version__ = "2022.02.02"
# Namespace for widgets that need to be accessed by callbacks.
widgets = types.SimpleNamespace()
# State that needs to be accessed by callbacks.
state = types.SimpleNamespace()
def create_widgets(root, w):
"""
Create the window and its widgets.
Arguments:
root: the root window.
w: SimpleNamespace to store widgets.
"""
# General commands and bindings
root.wm_title("tkinter threading v" + __version__)
root.columnconfigure(2, weight=1)
root.rowconfigure(3, weight=1)
root.resizable(False, False)
# First row
tk.Label(root, text="Thread status: ").grid(row=0, column=0, sticky="ew")
w.runstatus = tk.Label(root, text="not running", width=12)
w.runstatus.grid(row=0, column=1, sticky="ew")
# Second row
tk.Label(root, text="Timer: ").grid(row=1, column=0, sticky="ew")
w.counter = tk.Label(root, text="0 s")
w.counter.grid(row=1, column=1, sticky="ew")
# Third row
w.gobtn = tk.Button(root, text="Go", command=do_start)
w.gobtn.grid(row=2, column=0, sticky="ew")
w.stopbtn = tk.Button(root, text="Stop", command=do_stop, state=tk.DISABLED)
w.stopbtn.grid(row=2, column=1, sticky="ew")
def initialize_state(s):
"""
Initialize the global state.
Arguments:
s: SimpleNamespace to store application state.
"""
s.worker = None
s.run = False
s.counter = 0
def worker():
"""
Function that is run in a separate thread.
This function *does* update tkinter widgets. In Python 3, this should be
safe if a tkinter is used that is built with threads.
"""
# Initialization
widgets.runstatus["text"] = "running"
# Work
while state.run:
time.sleep(0.25)
state.counter += 0.25
widgets.counter["text"] = f"{state.counter:.2f} s"
# Finalization
state.counter = 0.0
widgets.counter["text"] = f"{state.counter:g} s"
widgets.runstatus["text"] = "not running"
def do_start():
"""Callback for the “Go” button"""
widgets.gobtn["state"] = tk.DISABLED
widgets.stopbtn["state"] = tk.NORMAL
state.run = True
state.worker = threading.Thread(target=worker)
state.worker.start()
def do_stop():
"""Callback for the “Stop” button"""
widgets.gobtn["state"] = tk.NORMAL
widgets.stopbtn["state"] = tk.DISABLED
state.run = False
state.worker = None
# Main program starts here.
if __name__ == "__main__":
# Detach from the command line on UNIX systems.
if os.name == "posix":
if os.fork():
sys.exit()
# Initialize global state
initialize_state(state)
# Create the GUI window.
root = tk.Tk(None)
# Set the font
default_font = tkfont.nametofont("TkDefaultFont")
default_font.configure(size=12)
root.option_add("*Font", default_font)
create_widgets(root, widgets)
root.mainloop()
Note how the worker thread monitors state.run and exits when it becomes False.
In general I find using multiple top-level windows confusing, so I don't use that pattern.
I would rather create a grid with rows of a progressbar and stop button for every thread.
Here is what I coded...
import tkinter as tk
import subprocess
import sys
import time
import os
import tkinter.font as font
from tkinter.ttk import *
app = tk.Tk()
app.geometry("400x400")
app.configure(bg='gray')
photo = tk.PhotoImage(file=r"C:\Users\ex\ex_button_active.png")
myFont = font.Font(family='Helvetica', size=20, weight='normal')
tk.Label(app, text='EX', bg='gray', font=(
'Verdana', 15)).pack(side=tk.TOP, pady=10)
app.iconbitmap(r'C:\Users\ex\ex_icon.ico')
start = time.time()
cmd = sys.executable + " -c 'import time; time.sleep(2)' &"
subprocess.check_call(cmd, shell=True)
assert (time.time() - start) < 1
p = subprocess.Popen(cmd, shell=True)
def ex_activation():
#Python Code
#Python Code...
def ex_stop():
sys.exit(ex_activation) #This area is basically where I have a button to terminate the other script running.
#I have tried sys.exit() and had the same result
ex_activation_button = tk.Button(app,
bg='black',
image=photo,
width=120,
height=120,
command=ex_activation)
ex_stop_button = tk.Button(app,
bg='Gray',
text='ex',
width=12,
command=ex_stop
height=3)
ex_stop_button['font'] = myFont
app.title("Example")
ex_activation_button.pack(side=tk.TOP)
ex_stop_button.pack(side=tk.LEFT)
app.mainloop()
I am looking for a way to get my program to stop the program the other button runs. I realized that this maybe be a "self destruct button" but I don't know how to do this with the script the other button runs. Any help greatly appreciated! I tried killing the code by putting the def ex_activation in the p.kill
This did not work...
If the other python script is made to run forever (has some kind of while True:), you can't run it on the command line as you did, because it will freeze your window while that script is running.
In order to run a python script on background you will need to do it with the subprocess library. (Find out here)
I also found an answer of another question that uses check_ouput() in order to know when the python program has finished. This can also be useful if you want to send a status to the tkinter app: you can print("33% Complete"), for example. You could add this in tkinter's main loop, so you always know if your program is running or not.
And last but not least, to kill that process (using the stop button), you should do it using os, and looking for the subprocess' ID. Here you can also find a good example.
I would try something like this:
cmd = "exec python file.py"
p = subprocess.Popen(cmd, shell=True)
# Continue running tkinter tasks.
tk.update()
tk.update_idletasks() # These both lines should be inside a while True
# Stop secondary program
p.kill()
EDIT
Example code using your question's code. WARNING: I have changed the png file location for testing, commented the app icon, and tested ONLY on Windows.
It's important to remove the mainloop() on the main file and put update...() in order to catch the keyboardInterrupt that (I don't know why) is killing both parent and child process.
I invite you to try it and be as happy as I have been when it was working after half an hour of testing!!
File 1: daemon.py - this file will run forever.
from time import sleep
from sys import exit
while True:
try:
print("hello")
sleep(1)
except KeyboardInterrupt:
print("bye")
exit()
File 2: tkinterapp.py - The name is self-explainatory
import tkinter as tk
import subprocess
import sys
import time
import os
import tkinter.font as font
from tkinter.ttk import *
app = tk.Tk()
app.geometry("400x400")
app.configure(bg='gray')
photo = tk.PhotoImage(file=r"C:\Users\royal\github\RandomSketches\baixa.png")
myFont = font.Font(family='Helvetica', size=20, weight='normal')
tk.Label(app, text='EX', bg='gray', font=(
'Verdana', 15)).pack(side=tk.TOP, pady=10)
# app.iconbitmap(r'C:\Users\ex\ex_icon.ico')
def ex_activation():
global pro
print("running!")
pro = subprocess.Popen("python daemon.py", shell=True)
def ex_stop():
global pro
print("stopping!")
os.kill(pro.pid, 0)
ex_activation_button = tk.Button(app,
bg='black',
image=photo,
width=120,
height=120,
command=ex_activation)
ex_stop_button = tk.Button(app,
bg='Gray',
text='ex',
width=12,
command=ex_stop, # BE CAREFUL You were missing a "," here !!!
height=3)
ex_stop_button['font'] = myFont
app.title("Example")
ex_activation_button.pack(side=tk.TOP)
ex_stop_button.pack(side=tk.LEFT)
# app.mainloop()
while True:
try:
app.update()
app.update_idletasks()
except KeyboardInterrupt:
pass
import tkinter as tk
from tkinter import filedialog, Text
from subprocess import call
import os
root = tk.Tk()
def buttonClick():
print('Button is clicked')
def openAgenda():
call("cd '/media/emilia/Linux/Programming/PycharmProjects/SmartschoolSelenium' && python3 SeleniumMain.py",
shell=True)
return
canvas = tk.Canvas(root, height=700, width=700, bg='#263D42')
canvas.pack()
frame = tk.Frame(root, bg='white')
frame.place(relwidth=0.8, relheight=0.8, relx=0.1, rely=0.1)
openFile = tk.Button(root, text='Open file', padx=10,
pady=5, fg="white", bg='#263D42', command=openAgenda)
openFile.pack()
root.mainloop()
the script it calls opens a new browser window, after finishing entering text in that window, it opens a new browser windows and loops.
meanwhile the tkinter button stays clicked, visually.
the reason your Tk GUI freezes is because you have everything running on 1 thread. The mainloop is haulted by the submit function call which must be taking a "long time", so you probably see "Not Responding" appear in your Tk window when you click the button. To fix this, you need spawn a separate thread for submit to run in, so that the mainloop can keep doing it's thing and keep your Tk window from freezing.
this is done using threading. Instead of your button directly calling submit, have the button call a function that starts a new thread which then starts submit. Then create another functions which checks on the status of the submit thread. You can add a status bar too
import tkinter as tk
from tkinter import filedialog, Text
from subprocess import call
import os
import threading
root = tk.Tk()
def buttonClick():
print('Button is clicked')
def openAgenda():
call("cd ' /media/emilia/Linux/Programming/PycharmProjects/SmartschoolSelenium' && python3 SeleniumMain.py",
shell=True)
canvas.update()
return
def start_Agenda_thread(event):
global Agenda_thread
Agenda_thread = threading.Thread(target=openAgenda)
Agenda_thread.daemon = True
Agenda_thread.start()
canvas = tk.Canvas(root, height=700, width=700, bg='#263D42')
canvas.pack()
frame = tk.Frame(root, bg='white')
frame.place(relwidth=0.8, relheight=0.8, relx=0.1, rely=0.1)
openFile = tk.Button(root, text='Open file', padx=10,
pady=5, fg="white", bg='#263D42', command=lambda:start_Agenda_thread(None))
openFile.pack()
root.mainloop()
Tkinter is single-threaded: it can only do one thing at a time. While the script is running, the GUI will be frozen. You'll need to do threading, multiprocessing, or find some other way to incorporate that other script in your GUI.
I have a program (say p1.py) which calls another python script (say p2.py) on click of a button. I would like to have a button which stops the execution of the p2.py but all the buttons freeze when it is running.
The only way to stop it is to use a keyboard interrupt in the console. I have read about the after() function but do I have to implement it in p1 or p2? Or is there any other way to do it without the after() function?
import tkinter
import os
window = tkinter.Tk()
window.title("Detecting")
def clicked():
os.system('python extract_frames.py')
bt = tkinter.Button(window,text="Start",command=clicked)
bt.pack()
stop = tkinter.Button(window,text="Stop",command="break") #also what command should I use for the interrupt?
stop.pack()
window.geometry('400x400')
window.mainloop()
You should use subprocess.Popen() instead of os.system():
import tkinter
import subprocess
proc = None
def clicked():
global proc
proc = subprocess.Popen(['python', 'extract_frames.py'])
def kill_task():
if proc and proc.poll() is None:
print('killing process ...')
proc.kill()
window = tkinter.Tk()
window.geometry('400x400')
window.title("Detecting")
bt = tkinter.Button(window, text="Start", command=clicked)
bt.pack()
stop = tkinter.Button(window, text="Stop", command=kill_task)
stop.pack()
window.mainloop()
there is following script:
import sys, Tkinter
def myScript():
...
...
def runScript():
while 1:
myScript()
i want to manage it using GUI "button" from Tkinter module
if __name__ == '__main__':
win = Frame ()
win.pack ()
Label(win, text='Choose following action', font=("Helvetica", 16), width=70, height=20).pack(side=TOP)
Button(win, text='Start script', width=20, height=3, command=runScript).pack(side=LEFT)
Button(win, text='Stop script', width=20, height=3, command=sys.exit).pack(side=LEFT)
Button(win, text='Quit', width=15, height=2, command=win.quit).pack(side=RIGHT)
mainloop()
when i type "Start script" button my script successfully started and working (infinite loop), but then i want to stop execution using "Stop script" button i can not do this, since main window with buttons is unavailable ("not responding")
What must i change in order to use both buttons correctly?
The problem is that the execution of the script is considered blocked, so while it continually runs the control is never returned back to the GUI to be able to continue with any external commands to stop it. To adjust this you will need to use threading. The best way to do this would to subclass your script method with threading.Thread and overloading the .run() method with your script execution. Doing so would look like this:
import threading
class MyScript(threading.Thread):
def __init__(self):
super(MyScript, self).__init__()
self.__stop = threading.Event()
def stop(self):
self.__stop.set()
def stopped(self):
return self.__stop.isSet()
def run(self):
while not self.stopped():
# Put your script execution here
print "running"
From there you can setup a global variable or class variable to keep track of if you currently have a thread running (you may want to do this differently if you want a user to run multiple instances of the script) and methods to start and stop it. I'd recommend a class variable with your application being a class itself but that's up to you.
script_thread = None
def startScript():
global script_thread
# If we don't already have a running thread, start a new one
if not script_thread:
script_thread = MyScript()
script_thread.start()
def stopScript():
global script_thread
# If we have one running, stop it
if script_thread:
script_thread.stop()
script_thread = None
From there you can bind those methods to your buttons. I'm not sure how you have your application structure setup (it seems to me you imported everything from Tkinter or sub-classed the Tkinter.Tk() instance). However in order to do what you propose you will need to use threading to prevent a blocking situation.
Use this:
import sys
from Tkinter import *
import tkMessageBox as tkmsg
win = None
def myScript():
pass
def runScript():
global win
while 1:
win.update()
pass
def btnStop_Click():
tkmsg.showinfo("Stopped", "Stopped")
sys.exit
if __name__ == '__main__':
global win
win = Frame ()
win.pack ()
Label(win, text='Choose following action', font=("Helvetica", 16), width=70, height=20).pack(side=TOP)
Button(win, text='Start script', width=20, height=3, command=runScript).pack(side=LEFT)
Button(win, text='Stop script', width=20, height=3, command=btnStop_Click).pack(side=LEFT)
Button(win, text='Quit', width=15, height=2, command=win.quit).pack(side=RIGHT)
mainloop()