I have made a python program based on Tkinter that analyses a CSV file and returns a PDF file. If there is a big CSV file, the tkinter window will freeze until the process is done. This is not desirable and instead I would like to introduce an indeterminate progress bar that disappears when a question is prompted for the user and then reappears when more processing is followed. I have read that somehow it needs a different thread, but I am quite stuck.
My program looks like:
import threading
from time import sleep
import tkinter as tk
import tkinter.ttk as ttk
root = tk.Tk()
root.title('text')
label = tk.Label(text = 'text')
label.pack()
class myThread(threading.Thread):
def __init__(self, threadID):
threading.Thread.__init__(self)
self.threadID = threadID
def run(self):
func()
def func():
#some processing
#ask the user a question and based on the answer more processing will be done
if True:
#more processing1
sleep(1)
else:
#more processing2
sleep(1)
def check_thread(th):
if not th.isAlive():
root.destroy()
root2.after(100, check_thread, th)
root2 = tk.Tk()
root2.title("New Top Level")
root2.geometry("400x170")
tk.Label(root2, text='Doing some work', justify=tk.CENTER, bg="#CBFDCB").place(x=43, y=30)
progress_bar = ttk.Progressbar(root2, orient="horizontal",
mode="indeterminate", takefocus=True, length=320)
progress_bar.place(x=40, y=80)
progress_bar.start()
thread1 = myThread(1)
thread1.start()
root2.after(100, check_thread, thread1)
root2.mainloop()
Button1 = tk.Button(root, text="sample", padx=10,
pady=5, fg="white", bg="#263D42", command=func)
Button1.pack()
root.mainloop()
Edit1: I have updated it with the version of the progress bar I was trying to use.
Related
I need help using tkinter in python.
I want to use a while loop inside my class so that it keeps printing in the terminal the content of an entry (box1). But the problem is that if I put a loop in my class the entry wont even be created because the root.mainloop() is after my while loop.
The program:
from tkinter import *
from tkinter import ttk
import tkinter as tk
class root(Tk):
def __init__(self):
super(root, self).__init__()
self.minsize(500,400)
self.configure(bg='#121213')
self.createEntry()
def createEntry(self):
self.name1 = StringVar()
self.box1 = ttk.Entry(self, width=2, textvariable = self.name1, font="Calibri 15")
self.box1.place(x=128, y=31)
while True:
print(self.name1.get())
root=root()
root.mainloop()
If I put the loop after the root.mainloop() it won't start printing the contents of name1 as long as the tkinter file is open. So it will print only the final version of name1 in a loop:
The Code:
from tkinter import *
from tkinter import ttk
import tkinter as tk
class root(Tk):
def __init__(self):
super(root, self).__init__()
self.minsize(500,400)
self.configure(bg='#121213')
self.createEntry()
def createEntry(self):
self.name1 = StringVar()
self.box1 = ttk.Entry(self, width=2, textvariable = self.name1, font="Calibri 15")
self.box1.place(x=128, y=31)
while True:
print(self.name1.get())
root=root()
root.mainloop()
while True:
print(root.name1.get())
Video of my problem
Does anyone have any solution?
How about a solution that does not put constant drain on the processing power?
If you add a callback to StringVar, you can trigger a function every time the variable is changed, and only then. Infinite loops don't work very well with UI applications, as in order to avoid stopping the control flow of the app you have to use things like async, threading etc.
def createEntry(self):
self.name1 = StringVar()
self.name1.trace_add("write", lambda name, index, mode: print(self.name1.get()))
self.box1 = ttk.Entry(self, width=2, textvariable = self.name1, font="Calibri 15")
self.box1.place(x=128, y=31)
This piece of code will make given lambda function trigger every time something is written to name1 variable.
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()
I am writing an application which involves a fair amount of data munging at launch. What I'd like to do is create a splash screen that tells the user what stage of the data loading process is happening in real time.
My plan was to create a Label and pass new text to that Label depending on what calculations were going on in that moment. However in my various attempts the best I've done is get the labels to show up only after the munging is complete.
I saw this, which helped me a bit, but still not getting all the way there:
Tkinter Show splash screen and hide main screen until __init__ has finished
Below is my current best attempt (taking all the actual dataframe stuff out to make it minimally executable)
[EDIT TO ADD] Ideally I'd like to do this in a way that doesn't require all the data munging to occur inside the class. IOW, phase 1 launches the splash screen, phase 2 runs the data munging in the main code, phase 3 launches the primary UI
import time
from tkinter import *
class LoadScreen(Toplevel):
def __init__(self, parent):
Toplevel.__init__(self, parent)
self.title('Loading')
self.update()
class UserInterface(Tk):
def __init__(self, parent):
Tk.__init__(self, parent)
self.parent=parent
self.withdraw()
loader = LoadScreen(self)
self.load_label = Label(loader, text='Loading')
self.load_label.grid(row=0, column=0, padx=20, pady=20)
self.stage_label = Label(loader, text='Preparing dataframe')
self.stage_label.grid(row=1, column=0, padx=20, pady=20)
#loader.destroy()
self.main_screen()
def main_screen(self):
self.deiconify()
self.load_label = Label(self, text='Done')
self.load_label.grid(row=0, column=0, padx=20, pady=20)
self.close_button = Button(self, text='Close',
command = lambda: self.destroy())
self.close_button.grid(row=1, column=0, padx=20, pady=20)
ui = UserInterface(None)
#Pretend I'm doing some dataframe munging
print('1')
time.sleep(2)
ui.stage_label['text'] = 'Running some calculations'
print('2')
time.sleep(2)
ui.stage_label['text'] = 'Finishing up'
print('3')
time.sleep(2)
ui.mainloop()
time.sleep will block the main thread. Here's a minimal sample on how I usually do it.
import time
from tkinter import *
root = Tk()
root.withdraw()
Label(root,text="I am main window").pack()
class SplashScreen:
def __init__(self):
self.a = Toplevel()
self.percentage = 0
Label(self.a,text="I am loading screen").pack()
self.load = Label(self.a,text=f"Loading...{self.percentage}%")
self.load.pack()
self.load_bar()
def load_bar(self):
self.percentage +=5
self.load.config(text=f"Loading...{self.percentage}%")
if self.percentage == 100:
self.a.destroy()
root.deiconify()
return
else:
root.after(100,self.load_bar)
SplashScreen()
root.mainloop()
I need help, I am doing a budget calculator and using tkinter for the first time and wondered why it is not working...
When I run it, it will just end straight away and when I put the root = Tk() at the end it comes up with an error.
I really need help, my code is below...
from time import sleep
from tkinter import *
from tkinter import messagebox, ttk, Tk
root = Tk()
class GUI():
def taskbar(self):
menu = Menu()
file = Menu(menu)
file.add_command(label="Exit", command=self.exit_GUI)
file.add_command(label = "Information", command=self.info_popup)
def Main_Menu(self):
topFrame = Frame(root)
topFrame.pack()
bottomFrame = Frame(root)
bottomFrame.pack(side=BOTTOM)
Income_button = Button(topFrame, text="Enter your incomes", command=self.Income)
Expense_button = Button(topFrame, text="Enter your expenses", command=self.Expense)
Total_button = Button(bottomFrame, text="View Results", command=self.Total)
Income_button.pack()
Expense_button.pack()
Total_button.pack()
def Income(self):
pass
def Expense(self):
pass
def Total(self):
pass
def exit_GUI(self):
exit()
def info_popup():
pass
g = GUI()
g.Main_Menu()
g.taskbar()
g.Income()
g.Expense()
g.Total()
g.exit_GUI()
g.info_popup()
root.mainloop()
You are exiting before you ever get to the mainloop with:
g.exit_GUI()
That method is calling the standard exit() and stopping the entire script. Remove or comment out the above call. You will also need to add self as an argument to info_popup to get your script to run:
def info_popup(self):
pass
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()