Tkinter load screen with updating labels - python

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()

Related

Why my tkinter back button does not function?

I made 2 pages in my tkinter. The main page have one button that will bring the user to Classifier_UI page. In the Classifier_UI page, I made a back button that will bring the user back to the main page. The back button works just fine if I run only the Classifier_UI.py. It did bring me back to the main page. However, if I run from the main_page.py, then click on the button that bring me to the Classifier_UI page, and click on the back button, the windows will automatically closed and did not bring me back to the main page. Why is this happening and how can I fix this?
Below is the button code that I made in my main page:
from tkinter import *
class Test:
def __init__(self, tk):
fm = Frame(tk)
self.l1 = Label(tk, text="Welcome", font=("Helvetica", 38, 'bold'), bg='LightGoldenrod2', fg='gray24').place(relx=0.25, rely=0.15)
self.b2 = Button(tk, bg='LightGoldenrod4', text="Classifier 1", font=("Helvetica", '10', 'bold'), foreground="white", width = 24, height = 3, command=self.change).pack(side=LEFT, expand=YES, padx=110, pady=300)
fm.pack(fill=BOTH, expand=YES)
def change(self):
tk.destroy()
import Classifier_UI
img_path = StringVar()
canvas = tk.Canvas(root, height = '700', width= '750', bg='#292828')
canvas.pack(fill=BOTH)
tk = Tk()
tk['bg']='LightGoldenrod2'
tk.toolbar = Frame(tk, bg="white")
width= tk.winfo_screenwidth()
height= tk.winfo_screenheight()
tk.geometry("%dx%d" % (width, height))
tt = Test(tk)
tk.mainloop()
This is code of the button in my second page:
def change():
root.destroy()
import main_page
root = tk.Tk()
root.title("Image Classifier")
browse = tk.Button(text="Back", bg='white', font='courier 10', command=change)
browse.place(relx = 0.75 ,rely = 0.85, relwidth = 0.12, relheight=0.05)
root.mainloop()
The problem is that tk.destroy() and root.destroy() close the window, because you're calling the destroy() method of the window, not the frame one. Try to do self.destroy() when your class is referring to a frame, this will destroy just the frame and not the entire window, but, when the frame is destroyed, you have no other way to make it reapper but to create another instance. Also, you shouldn't import modules inside functions unless you want to make that import optional, put all your imports at the start of the script if it's not the case.
More importantly, in both files you make a root window and destroy it, which is not necessary if you just want to change the displayed frame. I suggest you to put two frame inside the window, one on top of the other, and switch back and forth between the frames when the button is pressed.
Here is a simple example of code were a button switch between two frames (note that I've put the button straight inside the window and not inside a frame to make it indipendent from the change, but you can put one for each frame if you want):
import tkinter as tk
class MainWindow(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.frame_on_top = 1
self.switch_frame_button = tk.Button(self, text='switch', command=self.switch_frame)
self.switch_frame_button.grid(row=0, column=0)
self.container = tk.Frame(self) # This is the position where we will put the frames.
self.container.grid(row=1, column=0)
self.frames = {}
counter = 2
# Now let's make our frames.
# Starting from the second, so that the first frame will be put on top (last in first out).
for fr in (SecondFrame, FirstFrame):
frame = fr(parent=self)
self.frames[str(counter)] = frame
frame.grid(row=1, column=0, sticky='nsew') # Put all the frames in the same spot.
counter -= 1
def switch_frame(self):
if self.frame_on_top == 1:
frame = self.frames['2']
frame.tkraise()
self.frame_on_top = 2
elif self.frame_on_top == 2:
frame = self.frames['1']
frame.tkraise()
self.frame_on_top = 1
class FirstFrame(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
tk.Label(self, text='First frame').pack()
class SecondFrame(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
tk.Label(self, text='Second frame').pack()
root = MainWindow()
root.mainloop()
try making a button that it's command goes back to that function
def BackFunction(self):
pass
def __init__(self):
back = Button(root, command=self.BackFunction)

How to trigger changes in button in a comand in Tkinter?

I'm new to TKinter. I need to change the text of a button and its state when its clicked, then do some actions, and finally change again its text and state.
The problem is the changes only apply once the function has ended, skipping the first change of state and text. It never changes the Buttons text to "loading" and the button is never disabled.
Here is the code for the problem i'm experiencing:
#!/usr/bin/env python
import tkinter as tk
import time
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.pack(fill=tk.BOTH, expand=1)
self.create_widgets()
def create_widgets(self):
self.master.title("CW POS")
cierre = tk.Button(
self.master,
command=self.re_imprimir_ultimo_cierre)
cierre["text"] = "foo"
cierre.pack(fill=tk.BOTH, expand=1)
self._cierre = cierre
salir = tk.Button(self.master, text="quit", command=self.salir)
salir.pack(fill=tk.BOTH, expand=1)
def salir(self):
exit()
def re_imprimir_ultimo_cierre(self):
self._cierre["text"] = "Loading..."
self._cierre["state"] = tk.DISABLED
# TODO: magic
time.sleep(2)
self._cierre["text"] = "foo"
self._cierre["state"] = tk.NORMAL
root = tk.Tk()
root.geometry("240x180")
root.resizable(False, False)
app = Application(root)
root.mainloop()
How do I make the button show text="loading" and state=DISABLED, while the button is doing my calculations?
There is a pretty quick fix to this problem, you just need to update the button, once you change it's text to "Loading" (self._cierre["text"] = "Loading...")
def re_imprimir_ultimo_cierre(self):
self._cierre["text"] = "Loading..."
self._cierre["state"] = tk.DISABLED
self._cierre.update() # This is the line I added
# TODO: magic
time.sleep(2)
self._cierre["text"] = "foo"
self._cierre["state"] = tk.NORMAL
This just simply updates the buttons state after you change the text and state.
From what I understand this is because a button will run all the code within its command, before updating anything on the screen, so you essentially have to force the button to update itself within its command.
Hope this helps :)

Tkinter splash screen & multiprocessing outside of mainloop

I have implemented a splash screen that is shown while my application loads the database from remote cloud storage on startup. The splash screen is kept alive (there's a progressbar on it) with calls to .update() and is destroyed once the separate loading process ends. After this, the mainloop is started and the app runs normally.
The code below used to work fine on my Mac with python 3.6 and tcl/tk 8.5.9. However, after the update to Sierra I was forced to update tk to ActiveTcl 8.5.18. Now, the splash screen is not displayed until the separate process finishes, but then appears and stays on screen together with the root window (even though its .destroy() method is called).
import tkinter as tk
import tkinter.ttk as ttk
import multiprocessing
import time
class SplashScreen(tk.Toplevel):
def __init__(self, root):
tk.Toplevel.__init__(self, root)
self.geometry('375x375')
self.overrideredirect(True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.label = ttk.Label(self, text='My Splashscreen', anchor='center')
self.label.grid(column=0, row=0, sticky='nswe')
self.center_splash_screen()
print('initialized splash')
def center_splash_screen(self):
w = self.winfo_screenwidth()
h = self.winfo_screenheight()
x = w / 2 - 375 / 2
y = h / 2 - 375 / 2
self.geometry("%dx%d+%d+%d" % ((375, 375) + (x, y)))
def destroy_splash_screen(self):
self.destroy()
print('destroyed splash')
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.start_up_app()
self.title("MyApp")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.application_frame = ttk.Label(self, text='Rest of my app here', anchor='center')
self.application_frame.grid(column=0, row=0, sticky='nswe')
self.mainloop()
def start_up_app(self):
self.show_splash_screen()
# load db in separate process
process_startup = multiprocessing.Process(target=App.startup_process)
process_startup.start()
while process_startup.is_alive():
# print('updating')
self.splash.update()
self.remove_splash_screen()
def show_splash_screen(self):
self.withdraw()
self.splash = SplashScreen(self)
#staticmethod
def startup_process():
# simulate delay while implementation is loading db
time.sleep(5)
def remove_splash_screen(self):
self.splash.destroy_splash_screen()
del self.splash
self.deiconify()
if __name__ == '__main__':
App()
I do not understand why this is happening and how to solve it. Can anybody help? Thanks!
Update:
The splash screen is displayed correctly if you outcomment the line self.overrideredirect(True). However, I don't want window decorations and it still stays on screen at the end of the script. It is being destroyed internally though, any further method calls on self.splash (e.g. .winfo_...-methods) result in _tkinter.TclError: bad window path name ".!splashscreen".
Also, this code works fine under windows and tcl/tk 8.6. Is this a bug/problem with window management of tcl/tk 8.5.18 on Mac?
I came across this while looking for an example on how to make a tkinter splash screen that wasn't time dependent (as most other examples are). Sam's version worked for me as is. I decided to make it an extensible stand-alone class that handles all the logic so it can just be dropped into an existing program:
# Original Stackoverflow thread:
# https://stackoverflow.com/questions/44802456/tkinter-splash-screen-multiprocessing-outside-of-mainloop
import multiprocessing
import tkinter as tk
import functools
class SplashScreen(tk.Toplevel):
def __init__(self, root, **kwargs):
tk.Toplevel.__init__(self, root, **kwargs)
self.root = root
self.elements = {}
root.withdraw()
self.overrideredirect(True)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
# Placeholder Vars that can be updated externally to change the status message
self.init_str = tk.StringVar()
self.init_str.set('Loading...')
self.init_int = tk.IntVar()
self.init_float = tk.DoubleVar()
self.init_bool = tk.BooleanVar()
def _position(self, x=.5,y=.5):
screen_w = self.winfo_screenwidth()
screen_h = self.winfo_screenheight()
splash_w = self.winfo_reqwidth()
splash_h = self.winfo_reqheight()
x_loc = (screen_w*x) - (splash_w/2)
y_loc = (screen_h*y) - (splash_h/2)
self.geometry("%dx%d+%d+%d" % ((splash_w, splash_h) + (x_loc, y_loc)))
def update(self, thread_queue=None):
super().update()
if thread_queue and not thread_queue.empty():
new_item = thread_queue.get_nowait()
if new_item and new_item != self.init_str.get():
self.init_str.set(new_item)
def _set_frame(self, frame_funct, slocx=.5, sloxy=.5, ):
"""
Args:
frame_funct: The function that generates the frame
slocx: loction on the screen of the Splash popup
sloxy:
init_status_var: The variable that is connected to the initialization function that can be updated with statuses etc
Returns:
"""
self._position(x=slocx,y=sloxy)
self.frame = frame_funct(self)
self.frame.grid(column=0, row=0, sticky='nswe')
def _start(self):
for e in self.elements:
if hasattr(self.elements[e],'start'):
self.elements[e].start()
#staticmethod
def show(root, frame_funct, function, callback=None, position=None, **kwargs):
"""
Args:
root: The main class that created this SplashScreen
frame_funct: The function used to define the elements in the SplashScreen
function: The function when returns, causes the SplashScreen to self-destruct
callback: (optional) A function that can be called after the SplashScreen self-destructs
position: (optional) The position on the screen as defined by percent of screen coordinates
(.5,.5) = Center of the screen (50%,50%) This is the default if not provided
**kwargs: (optional) options as defined here: https://www.tutorialspoint.com/python/tk_toplevel.htm
Returns:
If there is a callback function, it returns the result of that. Otherwise None
"""
manager = multiprocessing.Manager()
thread_queue = manager.Queue()
process_startup = multiprocessing.Process(target=functools.partial(function,thread_queue=thread_queue))
process_startup.start()
splash = SplashScreen(root=root, **kwargs)
splash._set_frame(frame_funct=frame_funct)
splash._start()
while process_startup.is_alive():
splash.update(thread_queue)
process_startup.terminate()
SplashScreen.remove_splash_screen(splash, root)
if callback: return callback()
return None
#staticmethod
def remove_splash_screen(splash, root):
splash.destroy()
del splash
root.deiconify()
class Screen(tk.Frame):
# Options screen constructor class
def __init__(self, parent):
tk.Frame.__init__(self, master=parent)
self.grid(column=0, row=0, sticky='nsew')
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
### Demo ###
import time
def splash_window_constructor(parent):
"""
Function that takes a parent and returns a frame
"""
screen = SplashScreen.Screen(parent)
label = tk.Label(screen, text='My Splashscreen', anchor='center')
label.grid(column=0, row=0, sticky='nswe')
# Connects to the tk.StringVar so we can updated while the startup process is running
label = tk.Label(screen, textvariable=parent.init_str, anchor='center')
label.grid(column=0, row=1, sticky='nswe')
return screen
def startup_process(thread_queue):
# Just a fun method to simulate loading processes
startup_messages = ["Reticulating Splines","Calculating Llama Trajectory","Setting Universal Physical Constants","Updating [Redacted]","Perturbing Matrices","Gathering Particle Sources"]
r = 10
for n in range(r):
time.sleep(.2)
thread_queue.put_nowait(f"Loading database.{'.'*n}".ljust(27))
time.sleep(1)
for n in startup_messages:
thread_queue.put_nowait(n)
time.sleep(.2)
for n in range(r):
time.sleep(.2)
thread_queue.put_nowait(f"Almost Done.{'.'*n}".ljust(27))
for n in range(r):
time.sleep(.5)
thread_queue.put_nowait("Almost Done..........".ljust(27))
time.sleep(.5)
thread_queue.put_nowait("Almost Done......... ".ljust(27))
def callback(text):
# To be run after the splash screen completes
print(text)
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.callback_return = SplashScreen.show(root=self,
frame_funct=splash_window_constructor,
function=startup_process,
callback=functools.partial(callback,"Callback Done"))
self.title("MyApp")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.application_frame = tk.Label(self, text='Rest of my app here', anchor='center')
self.application_frame.grid(column=0, row=0, sticky='nswe')
self.mainloop()
if __name__ == "__main__":
App()
Apparently this is due to a problem with the window stacking order when windows are not decorated by the window manager after calling overrideredirect(True). It seems to have occurred on other platforms as well.
Running the following code on macOS 10.12.5 with Python 3.6.1 and tcl/tk 8.5.18, toplevel windows do not appear after the button 'open' is clicked:
import tkinter as tk
class TL(tk.Toplevel):
def __init__(self):
tk.Toplevel.__init__(self)
self.overrideredirect(True)
# self.after_idle(self.lift)
tl_label = tk.Label(self, text='this is a undecorated\ntoplevel window')
tl_label.grid(row=0)
b_close = tk.Button(self, text='close', command=self.close)
b_close.grid(row=1)
def close(self):
self.destroy()
def open():
TL()
root = tk.Tk()
label = tk.Label(root, text='This is the root')
label.grid(row=0)
b_open = tk.Button(root, text='open', command=open)
b_open.grid(row=1)
root.mainloop()
Uncommenting the line self.after_idle(self.lift) fixes the problem (simply calling self.lift() does too. But using after_idle()prevents the window from flashing up for a fraction of a second before it is moved to its position and resized, which is another problem I have experienced repeatedly with tkinter and keeps me wondering whether I should move on to learn PyQT or PySide2...).
As to the problem with closing an undecorated window in my original question: calling after_idle(window.destroy()) instead of window.destroy() seems to fix that too. I do not understand why.
In case other people reproduce this and somebody hints me towards where to report this as a bug, I am happy to do so.

tkinter cursor not changing until after action despite update_idletasks()

I am trying to change the cursor in my tkinter program to show the program is working but the cursor only changes to the working cursor until after the work is done, this is as compressed as I can make the code
warning: to demonstrate working it will count to 99,999,999 when you press go to page one
import tkinter as tk # python 3
from tkinter import font as tkfont # python 3
#import Tkinter as tk # python 2
#import tkFont as tkfont # python 2
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
for F in (StartPage, PageOne):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("StartPage")
def show_frame(self, page_name):
'''Show a frame for the given page name'''
frame = self.frames[page_name]
frame.tkraise()
class StartPage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is the start page", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button1 = tk.Button(self, text="Go to Page One",
command=self.go)
button1.pack()
def go(self):
# do something for like 5 seconds to demonstrate working
working(True)
l = [x for x in range(99999999)]
self.controller.show_frame('PageOne')
class PageOne(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self, parent)
self.controller = controller
label = tk.Label(self, text="This is page 1", font=controller.title_font)
label.pack(side="top", fill="x", pady=10)
button = tk.Button(self, text="Go to the start page",
command=self.back)
button.pack()
def back(self):
working(False)
self.controller.show_frame('StartPage')
def working(yesorno):
if yesorno==True:
app.config(cursor='wait')
else:
app.config(cursor='')
app.update_idletasks()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Edit: I would like to thank Switch between two frames in tkinter for this app layout example
This code was tested in windows 10 and Python 3. I found the cursor would not change until control was returned to mainloop. The code here outlines how to consistently display the busy cursor during a long running task. Further, this code demonstrates how to retrieve the data from the long running task (like results from a database query).
#! python3
'''
Everything you need to run I/O in a separate thread and make the cursor show busy
Summary:
1. Set up to call the long running task, get data from windows etc.
1a. Issue a callback to the routine that will process the data
2. Do the long running task - absolutely no tkinter access, return the data
3. Get the data from the queue and process away. tkinter as you will
'''
import tkinter as tk
import tkinter.ttk as ttk
from threading import Thread
from threading import Event
import queue
class SimpleWindow(object):
def __init__(self):
self._build_widgets()
def _build_widgets(self):
# *************************************************************************************************
# * Build buttons and some entry boxes
# *************************************************************************************************
g_col = 0
g_row = 0
WaiterFrame = ttk.Frame()
WaiterFrame.pack( padx=50)
i = 0
g_row += 1
longWaitButton = ttk.Button(WaiterFrame, text='Long Wait',command=self.setup_for_long_running_task)
longWaitButton.grid(row = g_row, column = i, pady=4, padx=25)
i += 1
QuitButton = ttk.Button(WaiterFrame, text='Quit', command=self.quit)
QuitButton.grid(row = g_row, column = i,pady=4, padx=25)
i += 1
self.Parm1Label = ttk.Label(WaiterFrame, text="Parm 1 Data")
self.Parm1Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm1 = ttk.Entry(WaiterFrame)
self.Parm1.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm2Label = ttk.Label(WaiterFrame, text="Parm 2 Data")
self.Parm2Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm2 = ttk.Entry(WaiterFrame)
self.Parm2.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm3Label = ttk.Label(WaiterFrame, text="Parm 3 Data")
self.Parm3Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm3 = ttk.Entry(WaiterFrame)
self.Parm3.grid(row = g_row, column = i, pady=4, padx=2)
i += 1
self.Parm4Label = ttk.Label(WaiterFrame, text="Parm 4 Data")
self.Parm4Label.grid(row = g_row-1, column = i, pady=4, padx=2)
self.Parm4 = ttk.Entry(WaiterFrame)
self.Parm4.grid(row = g_row, column = i, pady=4, padx=2)
def quit(self):
root.destroy()
root.quit()
def setup_for_long_running_task(self):
# ********************************************************************************************************
# * Do what needs to be done before starting the long running task in a thread
# ********************************************************************************************************
Parm1, Parm2, Parm3, Parm4 = self.Get_Parms()
root.config(cursor="wait") # Set the cursor to busy
# ********************************************************************************************************
# * Set up a queue for thread communication
# * Invoke the long running task (ie. database calls, etc.) in a separate thread
# ********************************************************************************************************
return_que = queue.Queue(1)
workThread = Thread(target=lambda q, w_self, p_1, p_2, p_3, p_4: \
q.put(self.long_running_task(Parm1, Parm2, Parm3, Parm4)),
args=(return_que, self, Parm1, Parm2, Parm3, Parm4))
workThread.start()
# ********************************************************************************************************
# * Busy cursor won't appear until this function returns, so schedule a callback to accept the data
# * from the long running task. Adjust the wait time according to your situation
# ********************************************************************************************************
root.after(500,self.use_results_of_long_running_task,workThread,return_que) # 500ms is half a second
# ********************************************************************************************************
# * This is run in a thread so the cursor can be changed to busy. NO tkinter ALLOWED IN THIS FUNCTION
# ********************************************************************************************************
def long_running_task(self, p1,p2,p3,p4):
Event().wait(3.0) # Simulate long running task
p1_out = f'New {p1}'
p2_out = f'New {p2}'
p3_out = f'New {p3}'
p4_out = f'New {p4}'
return [p1_out, p2_out, p3_out, p4_out]
# ********************************************************************************************************
# * Waits for the thread to complete, then gets the data out of the queue for the listbox
# ********************************************************************************************************
def use_results_of_long_running_task(self, workThread,return_que):
ThreadRunning = 1
while ThreadRunning:
Event().wait(0.1) # this is set to .1 seconds. Adjust for your process
ThreadRunning = workThread.is_alive()
while not return_que.empty():
return_list = return_que.get()
self.LoadWindow(return_list)
root.config(cursor="") # reset the cursor to normal
def LoadWindow(self, data_list):
self.Parm1.delete(0, tk.END)
self.Parm2.delete(0, tk.END)
self.Parm3.delete(0, tk.END)
self.Parm4.delete(0, tk.END)
i=0; self.Parm1.insert(0,data_list[i])
i+=1; self.Parm2.insert(0,data_list[i])
i+=1; self.Parm3.insert(0,data_list[i])
i+=1; self.Parm4.insert(0,data_list[i])
# ********************************************************************************************************
# * The long running task thread can't get to the tkinter self object, so pull these parms
# * out of the window and into variables in the main process
# ********************************************************************************************************
def Get_Parms(self):
p1 = self.Parm1Label.cget("text")
p2 = self.Parm2Label.cget("text")
p3 = self.Parm3Label.cget("text")
p4 = self.Parm4Label.cget("text")
return p1,p2,p3,p4
def WaitForBigData():
global root
root = tk.Tk()
root.title("Wait with busy cursor")
waitWindow = SimpleWindow()
root.mainloop()
if __name__ == '__main__':
WaitForBigData()
I suspect that all events need to be proceeded to change a cursor's look, because cursor depends on operating system and there're some events to handle (I assume that), since update_idletask has no effect - your cursor really change look only when code flow reaches a mainloop. Since you can treat an update as mainloop(1) (very crude comparison) - it's a good option if you know what you doing, because noone wants an endless loop in code.
Little snippet to represent idea:
try:
import tkinter as tk
except ImportError:
import Tkinter as tk
import time
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.button = tk.Button(self, text='Toggle cursor', command=self.toggle_business)
self.button.pack()
def toggle_business(self):
if self['cursor']:
self.config(cursor='')
else:
self.config(cursor='wait')
# self.update_idletasks() # have no effect at all
# self.update() # "local" mainloop(1)
# simulate work with time.sleep
# time.sleep(3)
# also your work can be scheduled so code flow can reach a mainloop
# self.after(500, lambda: time.sleep(3))
app = App()
app.mainloop()
To overcome this problem you can use:
update method (note warnings)
after method for scheduled work (opportunity to reach a mainloop for a code flow)
threading for "threaded" work (another opportunity, but GUI is responsive, you can handle other events and even simulate unresponsiveness, in other hand threading adds complexity, so use it if you really need it).
Note: There's no difference in behaviour between universal and native cursors on Windows platform.

IntVar().trace() not working

I'm just getting started coding in Python/Tkinter for a small Pymol plugin. Here I'm trying to have a toggle button and report its status when it is clicked. The button goes up and down, but toggleAVA never gets called. Any ideas why?
from Tkinter import *
import tkMessageBox
class AVAGnome:
def __init__(self, master):
# create frames
self.F1 = Frame(rootGnome, padx=5, pady=5, bg='red')
# checkbuttons
self.AVAselected = IntVar()
self.AVAselected.trace("w", self.toggleAVA)
self.AVAbutton = Checkbutton(self.F1, text='AVA', indicatoron=0, variable=self.AVAselected)
# start layout procedure
self.layout()
def layout(self):
self.F1.pack(side=TOP, fill=BOTH, anchor=NW)
#entry and buttons
self.AVAbutton.pack(side=LEFT)
def toggleAVA(self, *args):
if (self.AVAselected.get()):
avastatus = "selected"
else:
avastatus = "unselected"
tkMessageBox.showinfo("AVA status", avastatus)
def __init__(self):
open_GnomeUI()
def open_GnomeUI():
# initialize window
global rootGnome
rootGnome = Tk()
rootGnome.title('AVAGnome')
global gnomeUI
gnomeUI = AVAGnome(rootGnome)
I tested your code with Pymol.
Problem is because you use Tk() to create your window. You have to use Toplevel() and then it will work correctly with trace() or with command=.
Pymol is created with tkinter which can have only one window created with Tk() - it is main window in program. Every other window has to be created with Toplevel().
I have attached a working version of your code below. You can refer to it to learn where you went wrong. Generally, you have to mind how you structure your code if you are using a class format.This will help you visualize your code and debug better. You can read this discussion to help you.
from Tkinter import *
import tkMessageBox
class AVAGnome(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
# create frames
self.F1 = Frame(self, padx=5, pady=5, bg='red')
# checkbutton
self.AVAselected = IntVar()
self.AVAselected.trace("w", self.toggleAVA)
self.AVAbutton = Checkbutton(
self.F1, text='AVA', indicatoron=0, width=10,
variable=self.AVAselected)
# start layout procedure
self.F1.pack(side=TOP, fill=BOTH, anchor=NW)
self.AVAbutton.pack(side=LEFT) #entry and buttons
def toggleAVA(self, *args):
if (self.AVAselected.get()):
avastatus = "selected"
else:
avastatus = "unselected"
tkMessageBox.showinfo("AVA status", avastatus)
if __name__ == '__main__':
rootGnome = Tk()
rootGnome.title('AVAGnome')
gnomeUI = AVAGnome(rootGnome)
gnomeUI.pack(fill="both", expand=True)
gnomeUI.mainloop()
Update: The above code structure is for standalone tkinter programme. I am attempting to convert this working code to follow Pymol plugin example. Revised code is posted below and is susceptible to further revision.
# https://pymolwiki.org/index.php/Plugins_Tutorial
# I adapted from the example in the above link and converted my previous code to
#
from Tkinter import *
import tkMessageBox
def __init__(self): # The example had a self term here.
self.open_GnomeUI()
class AVAGnome(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
# create frames
self.F1 = Frame(self, padx=5, pady=5, bg='red')
# checkbutton
self.AVAselected = IntVar()
self.AVAselected.trace("w", self.toggleAVA)
self.AVAbutton = Checkbutton(
self.F1, text='AVA', indicatoron=0, width=10,
variable=self.AVAselected)
# start layout procedure
self.F1.pack(side=TOP, fill=BOTH, anchor=NW)
self.AVAbutton.pack(side=LEFT) #entry and buttons
def toggleAVA(self, *args):
if (self.AVAselected.get()):
avastatus = "selected"
else:
avastatus = "unselected"
tkMessageBox.showinfo("AVA status", avastatus)
# Note, I added a "self" term throughout function.
# Try w/ & w/o "self" to see which works.
def open_GnomeUI(self):
self.rootGnome = Tk()
self.rootGnome.title('AVAGnome')
self.gnomeUI = AVAGnome(self.rootGnome)
self.gnomeUI.pack(fill="both", expand=True)
self.gnomeUI.mainloop()

Categories