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()
Related
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()
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.
The function works when called inside the GUI by just placing R.loop(). However, it doesn't run with a button click. I've tried using .configure, but that doesn't work. The module works fine on its own inside the source code as well. I thought I heard somewhere that Tkinter doesn't like globals, but I'm not sure.
#creating a global variable for running loop
global Run
Run=False
#begin GUI CODE
main = tk.Tk()
main.title('REGENERATOR')
main.geometry('800x800')
...
#THIS IS THE CALL OF FUNCTION SETUP
running_setup=R.setup()
#Funtion to initialize the motor
def motor():
if Run(True):
R.loop()
#This is the start button
def startmotor(): #Function to start motor
global Run
Run = True
#Button_run.configure()
Button_run=Button(frame5, text="run", command=lambda:startmotor, bg="white", fg="black", padx=5, pady=5, font=("Times", 16))
Button_run.grid(row="2", column="0")
#This is the stop button
def stopmotor():
global Run
Run = False
#Button_stop.comfigure()
Button_stop=Button(frame5, text="stop", command=lambda:stopmotor, bg="white", fg="black", padx=5, pady=5, font=("Times", 16))
Button_stop.grid(row="2", column="1")
I'm trying to make a button where it will start a program. So first, a button called 'Run' will appear, subsequently after 3 seconds it should come up with a new button that says 'Stop'.
The reason I want it that way. That it's because I have tried and add two buttons on an interface panel, but the problem is then, every time I run an application, the interface freezes, so it was not possible to 'Stop' the program. So, I was wondering if it would be possible to do something like that?
What I've done:
from tkinter import *
tkWindow = Tk()
tkWindow.geometry('150x50')
tkWindow.title('Tkinter Example')
print("Tkinter button is appearing...")
def Action():
from Launch import Launch
run = Launch()
run
def Off():
import sys
sys.exit()
button = Button(tkWindow,
text='Start',
command=Action)
button1 = Button(tkWindow,
text='Stop',
command=Off)
button.pack()
button1.pack()
tkWindow.mainloop()
Try something like this:
from tkinter import *
from threading import Thread
from Launch import Launch
tkWindow = Tk()
tkWindow.geometry('150x50')
tkWindow.title('Tkinter Example')
print("Tkinter button is appearing...")
def Action():
thread = Thread(target=Launch, daemon=True)
thread.start()
# If you want to disable the button use:
# button.config(state="disabled")
button = Button(tkWindow,
text='Start',
command=Action)
# Here I am going to use the built-in `exit` function as per #Matiiss' suggestion
button1 = Button(tkWindow,
text='Stop',
command=exit)
button.pack()
button1.pack()
tkWindow.mainloop()
It starts a new thread when the "Start" button is pressed. That new thread calls Launch, leaving the main thread for tkinter. Please make sure that there isn't any references to your main GUI in your Launch function.
The following is a program that does not make sense, but I want to do something similar in a larger code. One function is called and I want it to wait for a change in a parameter, where the change is triggered by a button. Running this code, when the OK button is pressed it doesn't allow for another button to be pressed and it freezes. Also before anything, I get the error: name 'boton' is assigned to before global declaration. Thanks for reading. Saludos.
from Tkinter import *
import time
master = Tk()
def callback():
w1['text']="this"
while boton==0:
time.sleep(1)
w1['text']="and then this"
def switch():
if boton==0:
global boton
boton=1
if boton==1:
global boton
boton=0
boton=0
w1 = Label(master, text="")
w1.grid(row=0, column=0)
e1 = Entry(master)
e1.grid(row=0, column=1)
b = Button(master, text="OK", command=callback)
b.grid(row=1, column=0)
b2 = Button(master, text="switch", command=switch)
b2.grid(row=1, column=1)
mainloop()
You have a few problems and the two big ones are interrupting the Tkinter mainloop().
When you press OK your program gets stuck in a while loop that can never be broken. Keep in mind Tkinter runs on a single thread and any time you create a loop then it blocks the mainloop() form working until that loop is broken. To get around something like this we can use the after() method to schedule and event to happen as part of the mainloop() instead. The second problem that is blocking the mainloop() is the sleep() method. This will have the same affect until the time is up.
We also need to make sure you are using if/else statements because your Switch() your if statements will always result in the 2nd text.
With that taken care of all we need to do now is a little cleanup.
Instead of doing from Tkinter import * we should do import Tkinter as Tk. This will keep us from overriding any methods from other imports or from variables we create.
You do not need to do global in each if statement. You just need it at the top of your function.
take a look at the below code.
import Tkinter as tk
master = tk.Tk()
def callback():
global boton, w1
w1.config(text="this")
if boton == 0:
master.after(1000, callback)
else:
w1.config(text="and then this")
def switch():
global boton
if boton==0:
boton=1
else:
boton=0
boton=0
w1 = tk.Label(master, text="")
w1.grid(row=0, column=0)
e1 = tk.Entry(master)
e1.grid(row=0, column=1)
tk.Button(master, text="OK", command=callback).grid(row=1, column=0)
tk.Button(master, text="switch", command=switch).grid(row=1, column=1)
master.mainloop()