I have a main tkinter window that can take up to a few seconds to load properly. Because of this, I wish to have a splash screen that shows until the init method of the main class has finished, and the main tkinter application can be shown. How can this be achieved?
Splash screen code:
from Tkinter import *
from PIL import Image, ImageTk
import ttk
class DemoSplashScreen:
def __init__(self, parent):
self.parent = parent
self.aturSplash()
self.aturWindow()
def aturSplash(self):
self.gambar = Image.open('../output5.png')
self.imgSplash = ImageTk.PhotoImage(self.gambar)
def aturWindow(self):
lebar, tinggi = self.gambar.size
setengahLebar = (self.parent.winfo_screenwidth()-lebar)//2
setengahTinggi = (self.parent.winfo_screenheight()-tinggi)//2
self.parent.geometry("%ix%i+%i+%i" %(lebar, tinggi, setengahLebar,setengahTinggi))
Label(self.parent, image=self.imgSplash).pack()
if __name__ == '__main__':
root = Tk()
root.overrideredirect(True)
progressbar = ttk.Progressbar(orient=HORIZONTAL, length=10000, mode='determinate')
progressbar.pack(side="bottom")
app = DemoSplashScreen(root)
progressbar.start()
root.after(6010, root.destroy)
root.mainloop()
Main tkinter window minimum working example:
import tkinter as tk
root = tk.Tk()
class Controller(tk.Frame):
def __init__(self, parent):
'''Initialises basic variables and GUI elements.'''
frame = tk.Frame.__init__(self, parent,relief=tk.GROOVE,width=100,height=100,bd=1)
control = Controller(root)
control.pack()
root.mainloop()
EDIT: I can use the main window until it has finished loading using the .withdraw() and .deiconify() methods. However my problem is that I cannot find a way to have the splash screen running in the period between these two method calls.
a simple example for python3:
#!python3
import tkinter as tk
import time
class Splash(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.title("Splash")
## required to make window show before the program gets to the mainloop
self.update()
class App(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.withdraw()
splash = Splash(self)
## setup stuff goes here
self.title("Main Window")
## simulate a delay while loading
time.sleep(6)
## finished loading so destroy splash
splash.destroy()
## show window again
self.deiconify()
if __name__ == "__main__":
app = App()
app.mainloop()
one of the reasons things like this are difficult in tkinter is that windows are only updated when the program isn't running particular functions and so reaches the mainloop. for simple things like this you can use the update or update_idletasks commands to make it show/update, however if the delay is too long then on windows the window can become "unresponsive"
one way around this is to put multiple update or update_idletasks command throughout your loading routine, or alternatively use threading.
however if you use threading i would suggest that instead of putting the splash into its own thread (probably easier to implement) you would be better served putting the loading tasks into its own thread, keeping worker threads and GUI threads separate, as this tends to give a smoother user experience.
Related
I have been wrestling for a very long time with the issue of creating a Tkinter gui in modular fashion using classes. While there are many examples on this site - and believe me, I have read them all - they have all been too complex for me to understand. In particular, I could not work out how the imported modules could 'talk to' functions in the main application. I have finally had a eureka moment - I created a class in a module that defines a root window and a button. Then, I wrote a main python file that imports the root window / button module, and tests interactions between the imported module and the main app. All of those tests were successful, which is huge progress for me, as far as it goes. Here is the module code, saved as 'fmod.py':
import tkinter as tk
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title('Tkinter titlebar title')
self.geometry('300x50')
# create button within root window
self.button = tk.Button(self, text='Click Me')
self.button.pack()
Here is the python file written to import the module, and test all the interactions I was interested in understanding:
import tkinter as tk
# import module
import fmod
# create gui root window as an instance of imported MainWindow class
MainWin = fmod.MainWindow()
# define a local function for testing purposes
def ButtonClicked():
print("Button clicked")
# alter attributes of imported root/button from within this file
MainWin.geometry("700x400")
MainWin.config(bg = "yellow")
MainWin.button.config(bg="lightblue")
# add a new widget to root from within this file
NewLabel = tk.Label(MainWin,text="Label")
NewLabel.pack()
# connect an imported widget to the above local function
MainWin.button.config(command=ButtonClicked)
# mainloop
MainWin.mainloop()
As mentioned, all of the tests in the above code worked successfully. The question is this: I would rather have the MainWindow class define the root window and nothing else. So rather than including the button in that code, I'd like to write another entirely separate class that simply defines a button, which could be imported into my app separately. Would anyone be kind enough to help me write that code? I tried copying code from the MainWindow class, and it worked, but it opened an entirely new window (probably because of the init / super init code, which I do not fully understand, and don't really need to understand at the moment - it works, and I'm fine with that). I want code for a simple button that I could import into the main app, as a widget, and that I could place in the MainWin window inside the app.
You can define the class in a separate module and then import it, but you will still want to initiate the button inside of your main window like you are doing now. After all the button does belong on the window.
To create a button class it would be similar to how you created the MainWindow...
import tkinter as tk
class MyButton(tk.Button):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
... do something
def buttonClicked(self, *args):
print("Button Clicked")
Then you could just leave you mainwindow the way it is except switch out the class for your class. I also suggest moving a lot of your logic that is in the global scope inside of your MainWindow, and minimizing your use of the global scope as much as possible. For example:
import tkinter as tk
from mybuttonmodule import MyButton
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title('Tkinter titlebar title')
self.geometry('300x50')
# create button within root window
self.button = MyButton(self, text='Click Me')
self.button.pack()
self.button.config(command=self.button.buttonClicked)
self.newlabel = tk.Label(self, text="Label")
self.newlabel.pack()
self.geometry("700x400")
self.config(bg = "yellow")
self.button.config(bg="lightblue")
if __name__ == "__main__":
window = MainWindow()
window.mainloop()
I'm trying to open a Toplevel window with tkinter, from a system tray menu.
from cmath import phase
from tkinter import *
from tkinter import messagebox, messagebox
from tracemalloc import start
from pystray import MenuItem as item
import pystray
from PIL import ImageTk,Image
import pickle
def quit_window(icon, item):
icon.stop()
root.destroy()
exit()
def hidden():
global my_img1
top=Toplevel()
top.title("Secret menu, shhh :^)")
top.overrideredirect(True)
top.attributes('-alpha', 0.9)
w = 1100
h = 450
ws = top.winfo_screenwidth()
hs = top.winfo_screenheight()
x = (ws/2) - (w/2)
y = (hs/3) - (h/2)
top.geometry('%dx%d+%d+%d' % (w, h, x, y))
top.iconbitmap('screen.ico')
my_img1 = ImageTk.PhotoImage(Image.open("ITEXTRA.png"))
label1=Label(top,image=my_img1).place(relx=0.01,rely=0.01)
button2=Button(top,text="Close window",bg='#ff4a65',command=top.destroy, relief=GROOVE).place(relx=0.9,rely=0.9)
# Marks window as used
hiddenwindow=1
pickle.dump(hiddenwindow, open("window.dat", "wb"))
Button(root, text="Developer Options", padx=57, bg="#86b3b3",fg="black", command = hidden).grid(row=3,column=0)
def hide_window():
root.withdraw()
image=Image.open("screen.ico")
menu=(item('Dev window', hidden),item('show window', show_window),item('Exit app', quit_window))
icon=pystray.Icon("ITExtra", image, "Program", menu)
icon.run()
def show_window(icon, item):
icon.stop()
root.after(0,root.deiconify())
root.after(0,root.focus_force)
root = Tk()
root.title("ITextra")
root.geometry("400x400")
root.protocol('WM_DELETE_WINDOW', hide_window)
hidden()
root.mainloop()
But this unfortunately will not work, it won't pull up the toplevel window, nor the main one.
If I then open the root window myself, the toplevel window will open, but be unresponsive.
EDIT
Alright, so I tried adding the topwindow as class, but I keep getting error 'Top' object has no attribute 'tk'.
I pasted the updated code below. Any help is always greatly appreciated!
from cmath import phase
from tkinter import *
from tkinter import messagebox, messagebox
from tracemalloc import start
from pystray import MenuItem as item
import pystray
from PIL import ImageTk,Image
import pickle
class Top():
def __init__(self,master=None):
self.hide = True
def hidden(self):
if self.hide:
global my_img1
self.top=Toplevel(root)
self.top.title("Secret menu, shhh :^)")
self.top.attributes('-alpha', 0.9)
w = 1100
h = 450
ws = self.top.winfo_screenwidth()
hs = self.top.winfo_screenheight()
x = (ws/2) - (w/2)
y = (hs/3) - (h/2)
self.top.geometry('%dx%d+%d+%d' % (w, h, x, y))
self.top.iconbitmap('screen.ico')
my_img1 = ImageTk.PhotoImage(Image.open("ITEXTRA.png"))
label1=Label(self.top,image=my_img1).place(relx=0.01,rely=0.01)
button2=Button(self.top,text="Close window",bg='#ff4a65',command=self.top.destroy, relief=GROOVE).place(relx=0.9,rely=0.9)
# Marks window as used
hiddenwindow=1
pickle.dump(hiddenwindow, open("window.dat", "wb"))
self.top.mainloop()
def somewhereelse():
top.hide = True
top.hidden()
def quit_window(icon, item):
icon.stop()
root.destroy()
exit()
def show_window(icon, item):
icon.stop()
root.after(0,root.deiconify())
root.after(0,root.focus_force)
def hide_window():
root.withdraw()
image=Image.open("screen.ico")
try:
if pickle.load(open("window.dat","rb")) ==1:
menu=(item('Dev window', top.hidden),
item('show window', show_window),
item('Exit app', quit_window))
else:
menu=(item('Exit app', quit_window))
except:
menu=(item('Exit app', quit_window))
icon=pystray.Icon("ITextra", image, "Program", menu)
icon.run()
root = Tk()
root.title("ITextra")
root.geometry("400x400")
top = Top(root) #in main part
root.protocol('WM_DELETE_WINDOW', hide_window)
Button(root, text="Developer Options", padx=57, bg="#86b3b3",fg="black", command =top.hidden).grid(row=3,column=0)
root.mainloop()
Top window still unresponsive
It's not when root is open, but when top is open by itself, again it remains unresponsive. It responds however when I click a button, and drag my mouse. I tried adding a mainloop in top, but neither a self.top.mainloop nor a root.mainloop will work.
I tried using binds, but they also showed the same behaviour.
Am I creating something that won't work?
The app I'm creating is multithreaded, and my question is; would this complicate things with other classes? I am very new to coding, and quite frankly don't know.
I have the whole project in a pastebin here, for anyone who's interested. I think it's quite a mess, but I'm still pretty proud of it for a beginner.
The Toplevel() remains unresponsive because it has no event loop attached (mainloop()) because in this code the Toplevel acts as a standalone main window.
Need to attach this Toplevel to the root - top = Toplevel(root) where root is passed as argument to hidden(root). This way the root event loop works for all widget children such as a Toplevel. This would help towards your main part of the question.
(#added...) so there is no need for top.mainloop() because now that the root is the master/parent top is inside root.mainloop().
The event loop is for checking in to any events that happen on your widget which you would normally program with bind(). eg
top.bind('<Button>',dosomething) where dosomething is a defined function.
(...#added)
If you want a title for top then you need to create your own title baror label if you are using overrideredirect(True) because this removes the platform window manager.
(#added...)
The platform window manager is not so much removed as it is not being used when using overrideredirect(True). This is probably another reason why your window seems unresponsive with this stage of code. Need to code for events attached to the widget yourself - as you have done with the Button widget to close.
(...#added)
For main part of question:
there is nothing that refers to top widget in show_window in this code.
(#added...)
could look at making top a class and instantiate that in the root. The default status of hidden for top could be an attribute of this class. Then you can change the class attribute to hide or show functionally inside the body of the code somewhereelse.
eg skeleton sketch:
class Top():
def __init__(self,master=None):
...
self.hide = True
...
def hidden(self):
if self.hide:
...
def somewhereelse():
top.hide = true
top.hidden()
top = Top(root) #in main part
!!! obviously very brief general idea that needs work here of a way to maintain your design which seems quite good to me. There are several ways to incorporate the Toplevel widget into the class but that digresses a bit from the original question.
(...#added)
added 28Jan...
I recommend to study class more thoroughly rather than only putting in my example. But here is a bit more
class Top():
def __init__(self,master=None):
super().__init__()
self.master = master
self.hide = True
def hidden(self):
...
self.top = Toplevel(self.master)
...
In my words, but please check Python docs, super().__init__() will call the initialising function of the inherited object which in this case goes back to self.master which is root and then back through to tk.__init__ which is called in Tk().
I recommend looking at the code __init__.py file in the Lib\tkinter\ folder in the Python download to get a good understanding of how tkinter works.
I think this is definitely achievable but might need a different GUI - agree it is an excellent start for a beginner and thus not really a mess!!
Using class is not essential to achieving what you want to do but classes are very useful for encapsulating an object so that any extra attributes and methods relevant to that object can be customised for your project. This makes further or future development easier.
...added 28Jan
I want to create a GUI in tkinter that not only executes commands when a button is pressed, but responds to the state of a larger script running in a separate thread.
I have really dug around and tried to find some information on message passing, and I have found some great info on the pickle module, using multiprocessing and its built in tools and also threading, and queuing. I have even dug into David Beazley's lesson on concurrency located here. I just can't get the syntax right on any of those methods.
I have broken down my code into a small functional unit that should launch a little tkinter window like this:
tkinter window
The code below has a "launchGUI" function that launches my tkinter GUI, a "myLoop" function that starts the threads and will also loop to drive my larger program later, right now it just rotates the blink variable. I also have a blinkCheck method in my class that checks the status of the blink variable in the class.
I don't know if I am even putting my message receiver in the right place. In the following example code I am just trying to pass a global variable into the class. I know it is getting into the class, because the blinkCheck() method works even though uncommenting that method crashes the window. However, with the method turned off the label in the GUI never changes. I think the window crashing is the least of my worries, it must be because i have another while loop running.
What is the correct way to get that number in Label to change?
Here is my example code:
import tkinter as tk
from tkinter import Frame, Label
import time
import threading
blink = 0
class MyClass(tk.Frame):
def __init__(self, master):
self.master = master
super().__init__(self.master)
global blink
self.label = Label(master, text=blink)
self.label.pack()
#self.blinkCheck()
def blinkCheck(self):
global blink
while True:
print("blink in blinkCheck method is = {}".format(blink))
time.sleep(2.5)
def launchGUI():
root = tk.Tk()
root.title("My Blinker")
app1 = MyClass(root)
app1.mainloop()
def myLoop():
global blink
t1=threading.Thread(target=launchGUI)
t1.daemon = True
t1.start()
print("blink in blinker function is {}".format(blink))
while True:
if blink == 0:
blink = 1
else:
if blink == 1:
blink = 0
time.sleep(2.5)
if __name__=="__main__":
myLoop()
In your description you have mentioned something about involving buttons. I do not see that in your provided snippet. But with buttons it is possible to configure the label, i.e:
from tkinter import Label, Button
blink = 0
class MyClass(tk.Frame):
def __init__(self, master):
self.master = master
super().__init__(self.master)
global blink
self.label = Label(master, text=blink)
self.button = Button(master, text="Button", command=lambda: foo(self.label))
self.label.pack()
self.button.pack()
#self.blinkCheck()
def blinkCheck(self):
global blink
while True:
print("blink in blinkCheck method is = {}".format(blink))
time.sleep(2.5)
def foo(self, label):
label.config(text=blink)
Conventionally, this would be the most simple way to configure a label within an active thread.
If anyone feels like this answer may not be fully correct, please do edit it because I am new to Stack Overflow!
First, the GUI must run in main thread, and must not blocked by a infinite loop. Use after instead. To communicate, use some appropriate object from threading, e.g. Event:
import tkinter as tk
import time
import threading
class MyClass(tk.Frame):
def __init__(self, master, event):
super().__init__(master)
self.master = master
self.event = event
self.label = tk.Label(master, text='')
self.label.pack()
self.after(100, self.blink_check)
def blink_check(self):
self.label['text'] = self.event.is_set()
self.after(100, self.blink_check)
def blink(event):
while True:
event.set()
time.sleep(2.5)
event.clear()
time.sleep(2.5)
def main():
root = tk.Tk()
root.title("My Blinker")
event = threading.Event()
t = threading.Thread(target=blink, args=(event,))
t.daemon = True
t.start()
frame = MyClass(root, event)
root.mainloop()
if __name__=="__main__":
main()
I'm having some issue with the close button of the interface window with tkinter. My tool displays some video in real time, I do that with an infinite loop with the after function.
When I close the tkinter window by clicking on the cross, the program freezes. However, when I click on the button, the same function is called but it closes properly.
Here is the most simplified code I came up to show you the problem. Does anyone have an explanation and a way to solve it?
(BTW, I'm using Python 2.7.8 on OSX)
from Tkinter import *
from PIL import Image, ImageTk
import numpy as np
class Test():
def __init__(self, master):
self.parent = master
self.frame = Frame(self.parent)
self.frame.pack(fill=BOTH, expand=1)
self.mainPanel = Label(self.frame)
self.mainPanel.pack(fill=BOTH, expand=1)
self.closeButton = Button(self.frame, command=self.closeApp)
self.closeButton.pack(fill=BOTH, expand=1)
def closeApp(self):
print "OVER"
self.parent.destroy()
def task(tool):
print 'ok'
im = Image.fromarray(np.zeros((500, 500, 3)), 'RGB')
tool.tkim = ImageTk.PhotoImage(im)
tool.mainPanel['image'] = tool.tkim
root.after(1, task, tool)
def on_closing():
print "OVER"
root.destroy()
root = Tk()
root.wm_protocol("WM_DELETE_WINDOW", on_closing)
tool = Test(root)
root.after(1, task, tool)
root.mainloop()
Now if you try again with a smaller image (say 100*100), it works. Or if you put a delay of 100 in the after function, it also works. But in my application, I need a really short delay time as I'm displaying a video and my image size is 900px*500px.
Thanks!
Edit (08/19) : I have not found the solution yet. But I may use root.overrideredirect(1) to remove the close button and then recreate it in Tk, and also add drag a window using : Python/Tkinter: Mouse drag a window without borders, eg. overridedirect(1)
Edit (08/20) : Actually, I can not even drag the window. The tool is also freezing!
You probably just need to kill your animation loop. after returns a job id which can be used to cancel pending jobs.
def task():
global job_id
...
job_id = root.after(1, task, tool)
def on_closing():
global job_id
...
root.after_cancel(job_id)
Your code could be a bit cleaner if these functions were methods of the object so you didn't have to use a global variable. Also, you should have one quit function rather than two. Or, have one call the other so you are certain both go through exactly the same code path.
Finally, you shouldn't be calling a function 1000 times a second unless you really need to. Calling it so often will make your UI sluggish.
I found a solution, I'm not sure it is really clean, but at least it is working for what I want to do. I no longer use after but I loop and update the gui at each iteration.
from Tkinter import *
from PIL import Image, ImageTk
import numpy as np
class Test():
def __init__(self, master):
self.parent = master
self.frame = Frame(self.parent)
self.frame.pack(fill=BOTH, expand=1)
self.mainPanel = Label(self.frame)
self.mainPanel.pack(fill=BOTH, expand=1)
self.parent.wm_protocol("WM_DELETE_WINDOW", self.on_closing)
self.close = 0
def on_closing(self):
print "Over"
self.close = 1
def task(self):
print "ok"
im = Image.fromarray(np.zeros((500, 500, 3)), 'RGB')
self.tkim = ImageTk.PhotoImage(im)
self.mainPanel['image'] = self.tkim
root = Tk()
tool = Test(root)
while(tool.close != 1):
tool.task()
root.update()
root.destroy()
I have a tkinter GUI python code that creates a gui interface to my code, in the code later snack sound toolkit is used (which also uses Tk and creates an instance using root = Tk()). As, mainloop of the previously GUI application is already running to everytime snack function is called a new empty default tk window pops up. As this happens quite a lot, there are hundreds of empty tk windows on screen when this code executes. I have tried to close them using numerous methods root.destroy,root.withdraw, WM_DELETE_WINDOW etc. but to no solution.
Is there any way this can be done in tkinter?
import tkSnack
import thread
import Tkinter as tk
class my_gui(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.button = tk.Button(self, text="Get", command=self.on_button)
self.button.grid(row=8)
def on_button(self):
thread1 = thread.start_new_thread(run, (PATH_TO_WAVE_FILE,))
def run(path):
for k in range(10):
PITCH_VALUES = snack_work(path)
print PITCH_VALUES
def snack_work(INPUT_WAVE_FILE):
# initializing the snack tool
root = tk.Tk()
tkSnack.initializeSnack(root)
# root.withdraw()
mysound = tkSnack.Sound()
# processing original wave file
mysound.read(INPUT_WAVE_FILE)
PITCH_VALUES = mysound.pitch()
return PITCH_VALUES
app = my_gui()
app.mainloop()
Make run() and snack_work() into instance methods of your app object, so that they can easily access that object's attributes. To work with a more minimal MCVE that doesn't rely on external libraries or files, I tested the following with simple print() (I'm on Python 3) and after() calls rather than snack stuff, as all I wanted to check was that the other functions could access a tkinter object.
import tkSnack
import thread
import Tkinter as tk
class my_gui(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.button = tk.Button(self, text="Get", command=self.on_button)
self.button.grid(row=8)
def on_button(self):
thread1=thread.start_new_thread(self.run,(PATH_TO_WAVE_FILE,))
def run(self, path):
for k in range(10):
PITCH_VALUES = self.snack_work(path)
print PITCH_VALUES
def snack_work(self, INPUT_WAVE_FILE):
## initializing the snack tool
tkSnack.initializeSnack(self) # use self instead of the separate root object
# self.withdraw()
mysound=tkSnack.Sound()
## processing original wave file
mysound.read(INPUT_WAVE_FILE)
PITCH_VALUES= mysound.pitch()
return PITCH_VALUES
app = my_gui()
app.mainloop()