I wrote a CLI with click. Now I'm wondering if it would be possible to integrate this into a GUI with Tkinter.
I know there are ways to embed a console into Tkinter, but I want to specifically know whether it is possible my CLI in it (with 'reasonable' effort).
So I have a Script:
#cli.command()
def myfunction()
print("My Stuff")
And now I want to build a Tkinter GUI, where I have a command line, so that if I type myfunction there it calls and executes this myfunction
Is that possible or should I just take the code underneath the CLI and build a Tkinter App with it, that is independent from the CLI?
For fun, I just made this tkinterify, which does what you're asking.
import tkinter
import click
import sys
from io import StringIO
def tkinterify(cli_group, app_name="Tkinterified App"):
# Create and configure root
root = tkinter.Tk()
root.wm_title(app_name)
tkinter.Grid.rowconfigure(root, 0, weight=1)
tkinter.Grid.columnconfigure(root, 0, weight=1)
# Create and configure frame
frame = tkinter.Frame(root)
frame.grid(row=0, column=0, sticky="nsew")
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.columnconfigure(2, weight=1)
frame.rowconfigure(0, weight=1)
frame.rowconfigure(1, weight=1)
initial_output = "Valid commands:\n"
initial_command_name_list = list(cli_group.commands.keys())
for available_command_name in initial_command_name_list:
initial_output = initial_output + " " + available_command_name + "\n"
initial_output = initial_output + "Ready for input."
# Some GUI widgets
run_string = tkinter.StringVar()
entry_run = tkinter.Entry(root, textvariable=run_string, width=50)
scrollbar_widget = tkinter.Scrollbar(root)
text_widget = tkinter.Text(root)
def clear_callback():
# Because the text widget is usually disabled, we have to explicitly enable it before we can write to it.
text_widget.config(state='normal')
text_widget.delete(1.0, tkinter.END)
text_widget.insert(tkinter.END, initial_output)
text_widget.config(state='disabled')
def run_callback():
command_args = []
try:
command_parts = run_string.get().split()
command_name = command_parts[0]
except IndexError:
return
if len(command_parts) > 1:
command_args = command_parts[1:]
if command_name:
try:
# Redirect stdout so we can read the output into a string for display within out GUI
real_stdout = sys.stdout
fake_stdout = StringIO()
sys.stdout.flush()
sys.stdout = fake_stdout
# Obtain list of available commands
available_commands = cli_group.commands
command_name_list = list(cli_group.commands.keys())
if command_name in command_name_list:
try:
# Make a fake context in which to run the command
context = available_commands[command_name].make_context("tkinter", command_args)
# Invoke the command within the fake context
available_commands[command_name].invoke(context)
except click.exceptions.UsageError as e:
print(e)
print(initial_output)
else:
print("Command not found.\n")
print(initial_output)
# Put stdout back
sys.stdout.flush()
sys.stdout = real_stdout
sys.stdout.flush()
output_string = fake_stdout.getvalue()
fake_stdout.close()
# Update the text output widget
text_widget.config(state='normal')
text_widget.delete(1.0, tkinter.END)
text_widget.insert(tkinter.END, output_string)
text_widget.config(state='disabled')
except IndexError:
pass
# More GUI widgets
button_run = tkinter.Button(root, text="Run", command=run_callback)
button_clear = tkinter.Button(root, text="Clear", command=clear_callback)
text_widget.delete(1.0, tkinter.END)
text_widget.insert(tkinter.END, initial_output)
entry_run.grid(row=0, column=0, sticky="new")
button_run.grid(row=0, column=1, sticky="n")
button_clear.grid(row=0, column=2, sticky="n")
text_widget.grid(row=1, column=0, columnspan=2, sticky="nsew")
scrollbar_widget.grid(row=1, column=2, sticky="ns")
scrollbar_widget.config(command=text_widget.yview)
text_widget.config(yscrollcommand=scrollbar_widget.set)
text_widget.config(state='disabled')
root.mainloop()
I've put it on github as https://github.com/rbricheno/tkinterify with an example as follows:
import click
from tkinterify import tkinterify
#click.group()
def cli():
pass
#click.command()
def my_function():
print("My Stuff")
#click.command()
def my_other_function():
print("More Things")
cli.add_command(my_function)
cli.add_command(my_other_function)
tkinterify(cli)
You may need to modify it if your scripts have large output or are complicated in other ways. I haven't tested how it handles arguments. Hopefully this gives you an idea how to do what you want.
The main trick is to add our click commands to a Group. Once they're added, we can easily get a reference to them using the dictionary in the_group.commands
Related
I'm writing an app using tkinter for the GUI, and I want an indeterminate progress bar to be going back and forth while the main function is running, which sometimes takes a few seconds, depending on user input. Normally, the whole program freeze while the main function is running, so I am trying to establish a threaded process for the progress bar so it moves while main() is doing its thing (both functions are called withinscan_and_display()).
from tkinter import filedialog
from tkinter import *
from tkinter.ttk import Progressbar
from main import main
import graphs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from PIL import ImageTk, Image
import threading
root = Tk()
launch_frame = Frame(root)
button_frame = Frame(root)
graph_frame = Frame(root, height=1000, width=1200)
# log directory button/text
logDirButton = Button(master=launch_frame, text='Select log storage location...',
command=lambda: get_directory(log_text), width=22)
log_text = Text(master=launch_frame, height=1, width=25)
logDirButton.grid(row=1, column=0)
log_text.grid(row=1, column=1)
# scan directory button/text
dirButton = Button(master=launch_frame, text="Select scan directory...", command=lambda: get_directory(t), width=22)
t = Text(master=launch_frame, height=1, width=25)
dirButton.grid(row=2, column=0)
t.grid(row=2, column=1)
# main scan button
mainButton = Button(master=launch_frame, text="SCAN!", state=DISABLED,
width=50, height=10, bg='#27b355')
mainButton.grid(row=3, column=0, columnspan=2)
# progress bar
progress = Progressbar(launch_frame, orient=HORIZONTAL, length=100, mode='indeterminate')
progress.grid(row=4, column=0, columnspan=2)
launch_frame.grid(row=0, column=0, sticky=NW)
def get_directory(text):
# first clear form if it already has text
try:
text.delete("1.0", END)
except AttributeError:
pass
directory = filedialog.askdirectory()
# store the first directory for later specific reference
text.insert(END, directory)
# disable scan button until user has given necessary info to run (log storage location, scan directory)
enable_scan_button(log_text, t)
return directory
def enable_scan_button(logText, dirText):
if logText.get("1.0", END) != '\n' and dirText.get('1.0', END) != '\n':
mainButton['state'] = NORMAL
mainButton['command'] = lambda: scan_and_display()
else:
mainButton['state'] = DISABLED
def scan_and_display():
threading.Thread(target=bar_start).start()
# get scan directory and log directory from text fields
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
def bar_start():
print("BAR START CALLED")
progress.start(10)
With this setup (and various other configurations I've trieD), the bar still freezes while main() is doing it's thing, and I need it to move to indicate to the user that something is happening.
You need to thread the long-running function, not the progressbar.
def scan_and_display():
# get scan directory and log directory from text fields
# It's best to do all tkinter calls in the main thread
log_directory = log_text.get("1.0", END)[:-1]
scan_directory = t.get("1.0", END)[:-1]
th = threading.Thread(target=bar_start, args=(log_directory, scan_directory))
th.start()
progress.start()
def bar_start(log_directory, scan_directory):
print("BAR START CALLED")
# store the initial scan directory for later reference
top_dir = scan_directory
# runs the main scan function. Passes scan_directory and log_directory arguments
data, scanDate = main(log_directory, scan_directory)
display(scan_directory, data, scanDate, top_dir)
progress.stop()
EDIT: here's a MCVE:
import tkinter as tk
from tkinter.ttk import Progressbar
import time
from threading import Thread
def long_running_function(arg1, arg2, result_obj):
"""accept some type of object to store the result in"""
time.sleep(3) # long running function
result_obj.append(f'DONE at {int(time.time())}')
root.event_generate("<<LongRunningFunctionDone>>") # trigger GUI event
def long_func_start():
x = 'spam'
y = 'eggs'
t = Thread(target=long_running_function, args=(x,y,data))
t.start()
result.config(text='awaiting result')
progress.start()
def long_func_end(event=None):
progress.stop()
result.config(text=f"process finished with result:\n{data[-1]}")
root = tk.Tk()
root.geometry('200x200')
btn = tk.Button(root, text='start long function', command=long_func_start)
btn.pack()
progress = Progressbar(root, orient=tk.HORIZONTAL, length=100, mode='indeterminate')
progress.pack()
result = tk.Label(root, text='---')
result.pack()
root.bind("<<LongRunningFunctionDone>>", long_func_end) # tell GUI what to do when thread ends
data = [] # something to store data
root.mainloop()
For my work, I frequently have to collect reasonably large datasets from a MySQL database, e.g. several parameters for several locations, and store that data in a CSV file per location. For this, I've written a small GUI. Since, the data has to be stored per location, I thought I'd take advantages of my 8-thread CPU and use the multiprocessing package to query the database per location. This works just fine, but I also want to keep track of how far the data retrieval and file writing is.
The trick with using multiprocessing together with Tkinter was to put the function that is called in the multiprocessing outside of the GUI class, but how do I get information from that function back into the class?
My code so far:
from multiprocessing import Process
from tkinter import *
import os
import pandas
import pymysql
class App:
def __init__(self, master):
self.master = master
self.stations = None
self.variables = None
self.startdtg = None
self.enddtg = None
self.outputlocation = "C:/Users/test"
self.processes = []
Label(master, text="Locations:").grid(row=0, column=0, sticky=W, columnspan=3)
self.locationEntry = Entry(master)
self.locationEntry.grid(row=0, column=1, sticky=EW, columnspan=3)
Label(master, text="Enter variables:").grid(row=1, column=0, sticky=W)
self.varEntry = Entry(master)
self.varEntry.grid(row=1, column=1, sticky=EW, columnspan=3)
Label(master, text="Start DTG:").grid(row=2, column=0, sticky=W)
self.startEntry = Entry(master)
self.startEntry.grid(row=2, column=1, sticky=EW)
Label(master, text="End DTG:").grid(row=2, column=2, sticky=W)
self.endEntry = Entry(master)
self.endEntry.grid(row=2, column=3, sticky=EW)
Label(master, text="Output location:").grid(row=3, column=0, sticky=W)
self.outputEntry = Entry(master)
self.outputEntry.grid(row=3, column=1, columnspan=2, sticky=EW)
self.startButton = Button(master, text="Start", command=self.get_data)
self.startButton.grid(row=5, column=1, sticky=EW)
def get_data(self):
self.update_variables()
self.collect_data()
def update_variables(self):
self.stations = [station.strip() for station in self.locationEntry.get().split(",")]
self.variables = [variable.strip() for variable in self.varEntry.get().split(",")]
self.startdtg = self.startEntry.get()
self.enddtg = self.endEntry.get()
self.outputlocation = os.path.join(self.outputlocation, self.outputEntry.get())
def collect_data(self):
for station in self.stations:
p = Process(target=query_database, args=(station, self.variables, self.startdtg, self.enddtg, self.outputlocation))
self.processes.append(p)
p.start()
def query_database(station, variables, startdtg, enddtg, outputlocation):
""""Function that collects and writes data to local drive"""
if __name__ == "__main__":
root = Tk()
app = App(root)
root.mainloop()
To be clear: this code works fine. It produces this GUI:
What I want, is a GUI like this:
With the part showing the progress of the query_database function, meaning it has to update when a step in that function has been completed.
How would I approach this? Also, feel free to give any comments about my coding, I'm still learning the basics of GUIs and setting up classes.
Let's sum what was said in comments:
To get information from function, which executed in another process,
you should communicate with function's process with either a Queue or a Pipe.
While you have a channel to communicate with - keep checking
continiously for messages via self-scheduling
after.
Keep in mind that idea to pass a Label or anything tk-related to
that process isn't an option, since it isn't a thread/process-safe
practice.
After all of this you should come with something similar to this approach:
try:
import Tkinter as tk # Python 2
import ttk
import Queue as queue
except ImportError:
import tkinter as tk # Python 3
import tkinter.ttk as ttk
import queue
import multiprocessing as mp
import time
class App(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.minsize(width=400, height=25)
self.label = tk.Label(self, text='Waiting for "work"')
self.label.pack(expand=True, fill='both')
self.progressbar = ttk.Progressbar(self, orient='horizontal', value=0, maximum=3, mode='determinate')
self.progressbar.pack(fill='x')
self.button = tk.Button(self, text='Start', command=self.start_work)
self.button.pack(fill='x')
self.queue = mp.Queue()
self.process = None
def start_work(self):
self.process = mp.Process(target=work, args=(self.queue,))
self.button.configure(state='disabled')
self.process.start()
self.periodic_call()
def periodic_call(self):
# check a queue once
self.check_queue()
# if exit code is None - process is on the run and we should re-schedule check
if self.process.exitcode is None:
self.after(100, self.periodic_call)
# things are executed
else:
self.process.join()
self.button.configure(state='normal')
self.label.configure(text='Waiting for "work"')
self.progressbar.configure(value=0)
def check_queue(self):
# common check of the queue
while self.queue.qsize():
try:
self.label.configure(text=self.queue.get(0))
self.progressbar.configure(value=self.progressbar['value'] + 1)
except queue.Empty:
pass
def work(working_queue):
for type_of_work in ['Combobulationg Discombobulator', 'Pointing towards space',
'Calculating Ultimate Answer']:
working_queue.put(type_of_work)
time.sleep(1.5)
if __name__ == '__main__':
app = App()
app.mainloop()
Here's my problem, when keyboard focus is on KeyboardEntry, KeysPressed() should happen, when focus leaves, toggle() should happen.
KeysPressed has a while loop such as:
while Flag:
#Whatever
toggle just toggles the flag
Flag= not Flag
The idea is when focus leaves KeyComboEntry, KeysPressed() stops running
This does not happen, at all matter of fact, toggle is not called
my
# Program by Fares Al Ghazy started 20/5/2017
# Python script to assign key combinations to bash commands, should run in the background at startup
# this program is meant to release bash code, it is obviously non-system agnostic and only works linux systems that use BASH
# is one file which only creates the GUI, another file is needed to use the info taken by this program
FileName = 'BinderData.txt'
import tkinter as tk
from ComboDetect import ComboDetector
from _thread import start_new_thread
# Create a class to get pressed keys and print them
KeyManager = ComboDetector()
# Class that creates GUI and takes info to save in file
class MainFrame(tk.Tk):
# variable to store pressed keys
KeyCombination = ""
testcounter = 1
Flag = True
#function to toggle Flag
def Toggle(self):
print("toggle")
self.Flag = not self.Flag
# function to write to file
def SaveFunction(self, e1, e2, FileName):
file = open(FileName, "a")
combo = e1.get() + '\n'
performed = e2.get() + '\n'
file.write(combo)
file.write(performed)
file.close()
def KeysPressed(self, Entry, KeyCombination):
Entry.config(state="normal")
#Entry.insert(tk.END, "Test")
while self.Flag:
print("test "+str(self.testcounter))
self.testcounter = self.testcounter + 1
KeyCombination = str(KeyManager.getpressedkeys())
Entry.delete(0, tk.END)
Entry.insert(tk.END, KeyCombination)
# constructor
def __init__(self, FileName, **kwargs):
tk.Tk.__init__(self, **kwargs)
# create GUI to take in key combinations and bash codes, then save them in file
root = self # create new window
root.wm_title("Key binder") # set title
# create labels and text boxes
KeyComboLabel = tk.Label(root, text="Key combination = ")
KeyComboEntry = tk.Entry(root)
# Bind function to entry
KeyComboEntry.bind('<FocusIn>', lambda e: start_new_thread(self.KeysPressed, (KeyComboEntry, self.KeyCombination)))
KeyComboEntry.bind('<FocusOut>', lambda f:self.toggle, ())
ActionLabel = tk.Label(root, text="Command to be executed = ")
ActionEntry = tk.Entry(root)
# place widgets in positions
KeyComboLabel.grid(row=0, column=0, sticky=tk.E)
ActionLabel.grid(row=1, column=0, sticky=tk.E)
KeyComboEntry.grid(row=0, column=1)
ActionEntry.grid(row=1, column=1)
# create save button
SaveButton = tk.Button(root, text="save",
command=lambda: self.SaveFunction(KeyComboEntry, ActionEntry, FileName))
SaveButton.grid(row=2, column=2, sticky=tk.E)
app = MainFrame(FileName)
app.mainloop()
I am trying to find a way Tkinter to make the Start button stay pressed until I press the Stop button.
from Tkinter import *
import tkMessageBox
class MainWindow(Frame):
def __init__(self):
Frame.__init__(self)
self.master.title("input")
self.master.minsize(250, 150)
self.grid(sticky=E+W+N+S)
top=self.winfo_toplevel()
top.rowconfigure(0, weight=1)
top.columnconfigure(0, weight=1)
for i in range(2):self.rowconfigure(i, weight=1)
self.columnconfigure(1, weight=1)
self.button0 = Button(self, text="Start", command=self.save, activeforeground="red")
self.button0.grid(row=0, column=0, columnspan=2, pady=2, padx=2, sticky=E+W+N+S)
self.button1 = Button(self, text="Stop", command=self.stop, activeforeground="red")
self.button1.grid(row=1, column=0, columnspan=2, pady=2, padx=2, sticky=E+W+N+S)
def save(self):
pass
def stop(self):
pass
if __name__=="__main__":
d=MainWindow()
d.mainloop()
So you can set the relief of the button using its config, this makes it look like it is pressed.
def save(self):
self.button0.config(relief=SUNKEN)
# if you also want to disable it do:
# self.button0.config(state=tk.DISABLED)
#...
def stop(self):
self.button0.config(relief=RAISED)
# if it was disabled above, then here do:
# self.button0.config(state=tk.ACTIVE)
#...
EDIT
This doesn't work on Mac OSx apparently. This link shows how in should look: http://www.tutorialspoint.com/python/tk_relief.htm
If Tkinter.Button doesn't allow to configure its relief property on your system then you could try ttk.Button-based code instead:
try:
import Tkinter as tk
import ttk
except ImportError: # Python 3
import tkinter as tk
import tkinter.ttk as ttk
SUNKABLE_BUTTON = 'SunkableButton.TButton'
root = tk.Tk()
root.geometry("400x300")
style = ttk.Style()
def start():
button.state(['pressed', 'disabled'])
style.configure(SUNKABLE_BUTTON, relief=tk.SUNKEN, foreground='green')
def stop():
button.state(['!pressed', '!disabled'])
style.configure(SUNKABLE_BUTTON, relief=tk.RAISED, foreground='red')
button = ttk.Button(root, text ="Start", command=start, style=SUNKABLE_BUTTON)
button.pack(fill=tk.BOTH, expand=True)
ttk.Button(root, text="Stop", command=stop).pack(fill=tk.BOTH, expand=True)
root.mainloop()
I know its too late and you probably have got an solution, but this this answer might be helpful for others.
The solution is to use Radio Button.The main motive for creation of Radio Button is same as your question.
Here's an example from GeeksforGeeks website:
# Importing Tkinter module
from tkinter import *
# from tkinter.ttk import *
# Creating master Tkinter window
master = Tk()
master.geometry("175x175")
# Tkinter string variable
# able to store any string value
v = StringVar(master, "1")
# Dictionary to create multiple buttons
values = {"RadioButton 1" : "1",
"RadioButton 2" : "2",
"RadioButton 3" : "3",
"RadioButton 4" : "4",
"RadioButton 5" : "5"}
# Loop is used to create multiple Radiobuttons
# rather than creating each button separately
for (text, value) in values.items():
Radiobutton(master, text = text, variable = v,
value = value, indicator = 0,
background = "light blue").pack(fill = X, ipady = 5)
# Infinite loop can be terminated by
# keyboard or mouse interrupt
# or by any predefined function (destroy())
mainloop()
Further, You can create Button Functions and achieve your desired output.
I have developed a simple app in Python (2.7) with Tkinter. But my status bar is only sort of working. Here's the stripped down code:
from Tkinter import *
import os
import sys
def fixFiles():
inputFilePath= input_dir.get()
#Build a list of files in a directory
fileList = os.listdir(inputFilePath)
#Loop through those files, open the file, do something, close the file
for filename in fileList:
infile = open(inputfilepath + "/" + filename,'r')
#Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
status.set(status_string)
for line in infile:
#Do some stuff here
infile.close()
class App:
def __init__(self, master):
i = 0
status.set("Status: Press 'Fix Files!'")
statuslabel = Label(master, textvariable=status, relief = RIDGE, width = 65, pady = 5, anchor=W)
bFixFiles = Button(root, text='Fix Files!', command = fixFiles)
bQuit = Button(root, text='Quit', command = root.destroy)
statuslabel.grid(row=i, column = 0, columnspan = 2)
bFixFiles.grid(row=i, column=2, sticky=E)
bQuit.grid(row=i, column=3, sticky=W)
root = Tk()
root.title("FIX Files")
input_dir = StringVar()
status = StringVar()
choice = IntVar()
app = App(root)
root.mainloop()
Currently what's happening is that the status bar reads "Status: Press 'Fix Files!'" until the program is finished looping through the files, at which point it reads "Status: Working on file: XXXXX.txt" (which is the name of the last file to be opened and closed by the program.
I would like the status bar to update with the file name each time the program opens a new file. Any help is appreciated!
The goofy way is to use root.update_idletasks():
#Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
status.set(status_string)
root.update_idletasks()
To its credit, it is simple, but it does not really work -- although the statuslabel gets updated, the Quit button is frozen until fixFiles is completed. That's not very GUI-friendly. Here are some more reasons why update and update_idletasks are considered harmful.
So how should we run a long-running task without freezing the GUI?
The key is to make your callback functions end quickly. Instead of having a long-running for-loop, make a function that runs through the innards of the for-loop once. Hopefully that ends quickly enough for the user to not feel the GUI has been frozen.
Then, to replace the for-loop, you could use calls to root.after to call your quick-running function multiple times.
from Tkinter import *
import tkFileDialog
import os
import sys
import time
def startFixFiles():
inputFilePath = tkFileDialog.askdirectory()
# inputFilePath= input_dir.get()
# Build a list of files in a directory
fileList = os.listdir(inputFilePath)
def fixFiles():
try:
filename = fileList.pop()
except IndexError:
return
try:
with open(os.path.join(inputFilePath, filename), 'r') as infile:
# Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
status.set(status_string)
for line in infile:
# Do some stuff here
pass
except IOError:
# You might get here if file is unreadable, you don't have read permission,
# or the file might be a directory...
pass
root.after(250, fixFiles)
root.after(10, fixFiles)
class App:
def __init__(self, master):
i = 0
status.set("Status: Press 'Fix Files!'")
statuslabel = Label(
master, textvariable=status, relief=RIDGE, width=65,
pady=5, anchor=W)
bFixFiles = Button(root, text='Fix Files!', command=startFixFiles)
bQuit = Button(root, text='Quit', command=root.destroy)
statuslabel.grid(row=i, column=0, columnspan=2)
bFixFiles.grid(row=i, column=2, sticky=E)
bQuit.grid(row=i, column=3, sticky=W)
root = Tk()
root.title("FIX Files")
input_dir = StringVar()
status = StringVar()
choice = IntVar()
app = App(root)
root.mainloop()
The above begs the question, What should we do if our long-running task has no loop? or if even one pass through the loop requires a long time?
Here is a way to run the long-running task in a separate process (or thread), and have it communicate information through a queue which the main process can periodically poll (using root.after) to update the GUI status bar. I think this design is more easily applicable to this problem in general since it does not require you to break apart the for-loop.
Note carefully that all Tkinter GUI-related function calls must occur from a single thread. That is why the long-running process simply sends strings through the queue instead of trying to call status.set directly.
import Tkinter as tk
import multiprocessing as mp
import tkFileDialog
import os
import Queue
sentinel = None
def long_running_worker(inputFilePath, outqueue):
# Build a list of files in a directory
fileList = os.listdir(inputFilePath)
for filename in fileList:
try:
with open(os.path.join(inputFilePath, filename), 'r') as infile:
# Update the status with the filename
status_string = 'Status: Working on file: ' + str(filename)
outqueue.put(status_string)
for line in infile:
# Do some stuff here
pass
except IOError:
# You might get here if file is unreadable, you don't have read permission,
# or the file might be a directory...
pass
# Put the sentinel in the queue to tell update_status to end
outqueue.put(sentinel)
class App(object):
def __init__(self, master):
self.status = tk.StringVar()
self.status.set("Status: Press 'Fix Files!'")
self.statuslabel = tk.Label(
master, textvariable=self.status, relief=tk.RIDGE, width=65,
pady=5, anchor='w')
bFixFiles = tk.Button(root, text='Fix Files!', command=self.startFixFiles)
bQuit = tk.Button(root, text='Quit', command=root.destroy)
self.statuslabel.grid(row=1, column=0, columnspan=2)
bFixFiles.grid(row=0, column=0, sticky='e')
bQuit.grid(row=0, column=1, sticky='e')
def update_status(self, outqueue):
try:
status_string = outqueue.get_nowait()
if status_string is not sentinel:
self.status.set(status_string)
root.after(250, self.update_status, outqueue)
else:
# By not calling root.after here, we allow update_status to truly end
pass
except Queue.Empty:
root.after(250, self.update_status, outqueue)
def startFixFiles(self):
inputFilePath = tkFileDialog.askdirectory()
# Start long running process
outqueue = mp.Queue()
proc = mp.Process(target=long_running_worker, args=(inputFilePath, outqueue))
proc.daemon = True
proc.start()
# Start a function to check a queue for GUI-related updates
root.after(250, self.update_status, outqueue)
root = tk.Tk()
root.title("FIX Files")
app = App(root)
root.mainloop()