customize tkinter menu while menu is open - python

I want to have less popular menu items fade in gradually from white to black. Is there any way to have the colors update while the menu is still open? I've experimented with postcommand and threads:
def update():
def color(c):
animal_menu.config(fg=c)
root.update()
print c
def adapt():
color('white')
root.after(100, color('#6E6E6E'))
root.after(100, color('black'))
## adapt() ##attempt no.1
## thread.start_new_thread(adapt, ()) ##attempt no.2
root = Tk()
menubutton = Menubutton(root, text="Animals")
animal_menu = Menu(menubutton, tearoff=0, postcommand=update)
animal_menu.add_command(label="Lion", command=print_time)
animal_menu.add_command(label="Tiger", command=print_time)
animal_menu.add_command(label="Bear", command=print_time)
menubutton.menu = animal_menu
menubutton["menu"] = menubutton.menu
menubutton.pack()
root.config()
root.mainloop()
So far, the first attempt runs completely before the menu appears (which makes sense as postcommand is called before posting the menu), and the second attempt runs only when the menu is not open (which I don't understand) as evidenced by the print statements.
Could anybody give me a pointer on how to make the color properly dynamically change to have the items fade in while the menu is open?

There's a couple of problems with the after method in the callback:
def update():
def color(c):
animal_menu.config(fg=c)
root.update()
print c
def adapt():
color('white')
root.after(100, color('#6E6E6E'))
root.after(100, color('black'))
adapt() ##attempt no.1
First, if you're passing arguments to the function being called in the after, you have to use a lambda expression or just split them by a comma:
root.after(100, color, 'black')
Otherwise, the parenthesis will make that function be evaluated first.
Second, after doesn't work with the typical control flow you're probably used to--it's not evaluated one, then the next--you're setting both after calls to be evaluated after 100ms, so that's what's going to happen.
Here's a working example of a fadein callback:
from Tkinter import *
def fadein(color):
if color < 111111:
animal_menu.config(fg='#000000')
else:
print color
animal_menu.config(fg='#' + str(color))
root.after(100, fadein, color - 111111)
root = Tk()
menubutton = Menubutton(root, text="Animals")
animal_menu = Menu(menubutton, tearoff=0, postcommand=lambda: fadein(999999))
animal_menu.add_command(label="Lion")
animal_menu.add_command(label="Tiger")
animal_menu.add_command(label="Bear")
menubutton.menu = animal_menu
menubutton["menu"] = menubutton.menu
menubutton.pack()
root.config()
root.mainloop()
Note the lambda expression, in postcommand, which is needed to pass the argument to fadein().
More info: http://effbot.org/tkinterbook/widget.htm#Tkinter.Widget.after-method

Related

How can I pack up multiple functions in a single widget?

I want to pack some func. in a single widget so that I can interact with that particular widget using bind func.
There's Frame widget which packs up widgets in it and canvas.create_window func. in Canvas widget which also does the same as Frame.
Following program generates sticman after every 3 sec. And when user click the stickman, it disappears.
I tried using Frame to pack functions 🙁...
from Tkinter import *
root = Tk()
hitbox = Frame(root, height = 100, width= 100)
hitbox.pack()
can = Canvas(hitbox, height= 450, width= 1000) # Canvas inside hitbox
can.pack()
def stickman (a,b,c,d):
# Code that makes stickman according to coordinates
def HP(p,q):
# Code that makes Progressbar widget inside hitbox which act as healthbar
counter = 0
def init():
if counter == 10:
pass
else:
counter +=1
stickman(100,100,130,130)
HP(90, 120)
root.after(3000, init) # Stickman respawns after 3 seconds
def kill():
hitbox.destroy()
hitbox.bind('<Button-1>', kill)
root.mainloop()
Stickman respawns after every 3 seconds but bind func. with frame does not seems to be working when running code. Stickman doesn't disappears when clicked.
I think what you are asking for is to destroy the Tkinter frame when Button 1 or the left button on the mouse is clicked. The stickman doesn't disappear because the frame isn't actively listening for the key click unlike a label or an entry field would do.
So, there are 2 easy fixes for this problem:
1. Binding to Root Window
Binding the keybind to the root window would solve the problem mostly because the root window is always actively listening for keybinds unlike the frame or an input field. The only problem with this approach is that the user can click anywhere on the window to destroy the frame.
2. Binding to the Stickman Itself
Binding the keybind to the stickman itself is the cleanest approach because it would be just the same as the first solution but this time the user can only click on the stickman to destroy the frame. This is probably the solution you were looking for. To implement this solution just replace the root.bind('<Button-1>', kill) with stickman.bind('<Button-1>', kill) (or whatever the name is of your stickman is) after defining the stickman but before packing it.
I atatched a modified version of your code down below for the first option:
from tkinter import *
root = Tk()
def kill(event=None):
hitbox.destroy()
root.bind('<Button-1>', kill)
hitbox = Frame(root, height = 100, width= 100)
hitbox.pack()
can = Canvas(hitbox, height= 450, width= 1000) # Canvas inside hitbox
can.pack()
def stickman (a,b,c,d):
pass
# Code that makes stickman according to coordinates
def HP(p,q):
pass
# Code that makes Progressbar widget inside hitbox which act as healthbar
counter = 0
def init():
if counter == 10:
pass
else:
counter +=1
stickman(100,100,130,130)
HP(90, 120)
root.after(3000, init) # Stickman respawns after 3 seconds
root.mainloop()

Get dynamically which button uses function

I have a question about buttons and binds, but it's better if I show you.
from tkinter import Tk,Button
root = Tk()
startbutton = Button(root,text="start button")
pingbutton = Button(root,text="ping button")
startbutton.pack()
pingbutton.pack()
def startenterbind(e):
startbutton.config(relief='sunken')
def startleavebind(e):
startbutton.config(relief='raised')
def pingenterbind(e):
pingbutton.config(relief='sunken')
def pingleavebind(e):
pingbutton.config(relief='raised')
startbutton.bind("<Enter>", startenterbind)
startbutton.bind("<Leave>", startleavebind)
pingbutton.bind("<Enter>", pingenterbind)
pingbutton.bind("<Leave>", pingleavebind)
root.mainloop()
This is my code, now I am wondering, is there a better way to do this?
Maybe it's possible to get which button was hovered dynamically, to then change the button that was hovered?
This is so I can use one function for multiple buttons, while only affecting the one being <Enter>'d or <Leave>'d?
You can reuse an event handler function by making use of the event object they are passed which has an attribute telling you the widget that triggered it.
from tkinter import Tk,Button
root = Tk()
startbutton = Button(root,text="start button")
pingbutton = Button(root,text="ping button")
startbutton.pack()
pingbutton.pack()
def startenterbind(event):
event.widget.config(relief='sunken')
def startleavebind(event):
event.widget.config(relief='raised')
startbutton.bind("<Enter>", startenterbind)
startbutton.bind("<Leave>", startleavebind)
pingbutton.bind("<Enter>", startenterbind)
pingbutton.bind("<Leave>", startleavebind)
root.mainloop()
You could go a bit further by writing a single function that simply toggled the state of the button whenever it was called. One way that could be accomplished is by making the new relief type depend on what it currently is which can be determined by calling the universal widget cget() method:
def enterleavebind(event):
new_relief = 'sunken' if event.widget.cget('relief') == 'raised' else 'raised'
event.widget.config(relief=new_relief)
startbutton.bind("<Enter>", enterleavebind)
startbutton.bind("<Leave>", enterleavebind)
pingbutton.bind("<Enter>", enterleavebind)
pingbutton.bind("<Leave>", enterleavebind)

I'm trying to make a rainbow tkinter button

def RainbowButton():
btn.config(bg=red)
btn.config(bg=purple)
btn.config(bg=yellow)
btn.config(bg=orange)
btn.config(bg=blue)
btn.config(bg=lightblue)
btn.config(bg=green)
btn.config(bg=black)
def ButtonUpdate():
RainbowButton()
window.after(10, ButtonUpdate)
ButtonUpdate()
This is the code I came up with, but it's not working. The button is just black when I run the program, no colors are changing.
The trick is to feed the colors to the button one at a time, once for each time the ButtonUpdate() function runs. You can solve this by using an iterator. When the iterator runs out of items it raises an error which I capture with the try clause. I adjusted the time for the after() function so the effect can be seen better.
import tkinter as tk
window = tk.Tk()
btn = tk.Button(window, text='Button')
btn.pack(padx=50, pady=30)
rainbow_colors = ['red','purple','yellow','orange','blue',
'lightblue','green','black']
color_iterator = iter(rainbow_colors)
def ButtonUpdate():
try:
color = next(color_iterator)
btn.config(bg=color)
except StopIteration:
return
window.after(500, ButtonUpdate)
ButtonUpdate()
window.mainloop()

Why isn't the fade in animation working but the fade out animation works?

I am new to python and I came up with this idea on how to make a simple fade animation in python using tkinter and the time module. I have defined two animations for the program: one to fade in and the other one to fade out. The fade out animation works perfectly and exactly how I want it to be, however, the fade in animation does not work at all. The program pretty much doesn't show up until the while loop is finished. Am I doing something wrong or is it just impossible to create a fade in effect in python tkinter?
Here is my code snippet:
from tkinter import *
import time
root = Tk()
transparency = 0
while transparency <= 1:
transparency += 0.1
root.wm_attributes("-alpha", transparency)
time.sleep(0.03)
def fade():
t = 1
while t > 0:
t -= 0.1
root.wm_attributes("-alpha", t)
time.sleep(0.03)
root.destroy()
btn = Button(root, text='fade exit', command=fade).pack()
root.mainloop()
Instead of using while loops and time, use after(millis, function) and recursion.
bonuses:
use the function arguments to customize the fade effect and behavior
this will not block the root from updating
everything is encapsulated
these functions can be used on any Toplevel window
applyFades manages everything in one call
If you absolutely need to attach these features to a Button, simply assign the command argument like this: command=lambda: fadeOut(root).
window.py
''' Fade In
#window ~ the window to affect
#millis ~ the amount of milliseconds to wait before next recursion
#inc ~ the amount to increment alpha on each recursion
'''
def fadeIn(window, millis:int=50, inc:float=0.1):
alpha = float(window.attributes('-alpha')) + inc
window.attributes('-alpha', alpha)
if alpha < 1:
window.after(millis, lambda: fadeIn(window, millis, inc))
else:
window.attributes('-alpha', 1.0)
''' Fade Out
#window, #millis ~ see: Fade In
#dec ~ the amount to decrement alpha on each recursion
#destroy ~ True|False destroy the window when effect is complete
'''
def fadeOut(window, millis:int=50, dec:float=0.1, destroy:bool=True):
alpha = float(window.attributes('-alpha')) - dec
window.attributes('-alpha', alpha)
if alpha > 0:
window.after(millis, lambda: fadeOut(window, millis, dec, destroy))
else:
window.attributes('-alpha', 0.0)
if destroy:
window.destroy()
''' Assign All Fades In One Call
#window, #millis, #inc ~ see: Fade In
#dec, #destroy ~ see: Fade Out
#close ~ True|False add fadeOut effect to window close button
'''
def applyFades(window, millis:int=50, inc:float=0.1, dec:float=0.1, destroy:bool=True, close:bool=True):
window.attributes('-alpha', 0.0)
window.after(millis, lambda: fadeIn(window, millis, inc))
if close:
window.protocol("WM_DELETE_WINDOW", lambda: fadeOut(window, millis, dec, destroy))
main.py
import tkinter as tk
import window as win
root = tk.Tk()
win.applyFades(root)
root.mainloop()
Let's see what you script does. At the beginning root = Tk() is assigned which starts a tcl/tk interpreter and creates a root window. Then it controls its opacity attribute to fade in. After that a Button widget is placed on the root window which has these properties:
a text 'fade exit' is written on the top on the widget
it waits for a mouse click and after it controls opacity attribute of a root window to fade out.
Finally, root.mainloop() is a substitute for
while True:
root.update_idletasks()
root.update()
You might pay attention that 'fade in' button is being created at the moment your root window is not updated. This is a reason why you're not able to see actual fade in.
Solution 1. Updating root window after each sample of fading in:
from tkinter import *
import time
def fade():
t = 1
while t > 0:
t -= 0.1
root.wm_attributes("-alpha", t)
time.sleep(0.03)
root.destroy()
root = Tk()
transparency = 0
btn = Button(root, text='fade in', command=fade)
btn.pack()
while transparency <= 1:
transparency += 0.1
root.wm_attributes("-alpha", transparency)
root.update_idletasks()
root.update()
time.sleep(0.03)
btn.configure(text='fade exit') #I guess no new button is needed and text should be replaced only
root.mainloop()
Solution 2. It's more typical not to use tkinter in combination with time and use after method. Check out Michael's answer.

Tkinter: How to update a label text after a certain amount of time has passed?

So, I'm trying to create a basic Tkinter program which, when I press a button, updates the text on a label field, waits X amount of seconds and then update the label again.
For example:
I click the button, the label clears immediately after pressing it, then the program waits 3 seconds and shows "Hello" on screen.
The code shown below does not do what I want it to do because when I press the button, it remains pressed for X amount of time and then the text is updated inmediately. I want to press the button, clear the label, wait for 3 seconds and then show "Hello" on screen.
from tkinter import *
class Origin:
def __init__(self):
self.root = Tk()
self.root.geometry('800x600')
self.root.config(bg="black")
self.v = StringVar()
self.v.set('O R I G I N')
self.main_label = Label(self.root, textvariable=self.v, font="Arial 40", fg="white", bg="black")
self.main_label.place(x=240, y=150)
self.clear = Button(self.root, text='Clear', command=self.clear)
self.clear.place(x=400, y=400)
self.root.mainloop()
def clear(self):
#just to clear the string
self.v.set('')
self.root.after(3000, self.v.set('Hello'))
def main():
App = Origin()
if __name__ == '__main__':
main()
after needs callback - it means function's name without () and arguments. If you have to use function with argument then use `lambda
after(3000, lambda:self.v.set('Hello'))
or create function which doesn't need arguments
def callback():
self.v.set('Hello')
self.root.after(3000, callback)
Your current code works like
result = self.v.set('Hello')
self.root.after(3000, result)
It executes function self.v.set('Hello') at once and uses its result as callback in after().
EDIT: as #acw1668 said in comment you can also run function with arguments this way
self.root.after(3000, self.v.set, 'Hello')

Categories

Resources