i'm new on Tkinter and i'm trying to make an animated button.
I'm using the enter-leave events but the click on button it's not responding very well.
My code is:
imagePath = "Resources/"
imagelist = ["boton_1.gif","boton_2.gif","boton_3.gif","boton_4.gif","boton_5.gif","boton_6.gif",
"boton_7.gif","boton_8.gif","boton_9.gif","boton_10.gif","boton_11.gif","boton_12.gif",
"boton_13.gif","boton_14.gif","boton_15.gif","boton_16.gif"]
giflist = []
for imagefile in imagelist:
photo = PhotoImage(file=imagePath+imagefile)
giflist.append(photo)
self.photo=giflist[0]
button = Button(buttonFrame, image=self.photo,background='orange',activebackground='lightsalmon',
command=lambda: controller.show_frame(ListPlayerPage))
button.pack(pady=5)
def enter(event):
self.clickOnButton1 = True
for i in range(1,8):
button.config(image=giflist[i])
button.update()
time.sleep(0.1)
if self.clickOnButton1 == False:
break
while (self.clickOnButton1):
for i in range (9,15):
button.config(image=giflist[i])
button.update()
time.sleep(0.08)
if self.clickOnButton1 == False:
break
def leave(event):
self.clickOnButton1 = False
button.config(image=self.photo)
button.update()
button.bind("<Enter>",enter)
button.bind("<Leave>",leave)
Thanks!!
Part of the problem is definitely related to the fact you're calling sleep. As a good rule of thumb you should never call sleep in the main thread of a GUI. It prevents the GUI from processing all events, including screen refreshes.
Generally speaking, you should also avoid calling update. It can result in nested event loops, if during the processing of update you end up calling a method that again calls update.
Here's a really simple example of solution that creates a button that can be animated. It uses after to iterate over a list of text strings, one new string every half second. This example will animate forever, but you can easily have it show each item only once. This modifies the text to make the example shorter, but you can easily modify it to change images instead of text.
import Tkinter as tk # use tkinter for python 3.x
class AnimatedButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self._job = None
def cancel_animation(self):
if self._job is not None:
self.after_cancel(self._job)
self._job = None
def animate(self, textlist):
text = textlist.pop(0)
textlist.append(text)
self.configure(text=text)
self._job = self.after(500, self.animate, textlist)
You use it like any other Button, but you can call animate to start animation and cancel_animate to cancel it:
button = AnimatedButton(root, width=10)
data = ["one","two","three","four","five","six"]
button.bind("<Enter>", lambda event: button.animate(data))
button.bind("<Leave>", lambda event: button.cancel_animation())
I followed the Bryan Oakley example and found a nice solution!
First of all, this is an animated button with a bit complex animation. I have 16 images. The firts one is the base image. Then i have eight images that are the first part of the animation. The rest of the images are the loop part of the animation.
When you put the mouse over the button, the animation starts.
Here is the code!:
import Tkinter as tk # use tkinter for python 3.x
root = tk.Tk()
root.geometry("300x200")
class AnimatedButton(tk.Button):
def __init__(self, *args, **kwargs):
tk.Button.__init__(self, *args, **kwargs)
self._job = None
self.i = 1
def cancel_animation(self,image):
self.configure(image=image)
self.i = 1
if self._job is not None:
self.after_cancel(self._job)
self._job = None
def animate(self, imagelist):
image = imagelist[self.i]
self.i+=1
if self.i == (len(imagelist)-1):
self.i = 9
self.configure(image=image)
self._job = self.after(80, self.animate, imagelist)
imagePath = "Resources/"
imagelist = ["boton_1.gif","boton_2.gif","boton_3.gif","boton_4.gif","boton_5.gif","boton_6.gif",
"boton_7.gif","boton_8.gif","boton_9.gif","boton_10.gif","boton_11.gif","boton_12.gif",
"boton_13.gif","boton_14.gif","boton_15.gif","boton_16.gif"]
giflist = []
for imagefile in imagelist:
photo = tk.PhotoImage(file=imagePath+imagefile)
giflist.append(photo)
image = giflist[0]
button = AnimatedButton(root,image = image)
button.bind("<Enter>", lambda event: button.animate(giflist))
button.bind("<Leave>", lambda event: button.cancel_animation(image))
button.pack()
root.mainloop()
Thank's!!!
Related
I searched for ways of implementing tooltips for an application and I found in some comment or answer in this site some while ago a link to this page.
I've been using this class since then and I've been happy with the result.
But recently I noticed that the tooltips came up behind modal windows, when they refer to widgets on that modal window.
Below in the code downloaded from that GitHub link, where I just made the changes of replacing from tkinter import * with import tkinter as tk, and using the prefix tk throughout the code accordingly.
"""Tools for displaying tool-tips.
This includes:
* an abstract base-class for different kinds of tooltips
* a simple text-only Tooltip class
"""
import tkinter as tk
class TooltipBase:
"""abstract base class for tooltips"""
def __init__(self, anchor_widget):
"""Create a tooltip.
anchor_widget: the widget next to which the tooltip will be shown
Note that a widget will only be shown when showtip() is called.
"""
self.anchor_widget = anchor_widget
self.tipwindow = None
def __del__(self):
self.hidetip()
def showtip(self):
"""display the tooltip"""
if self.tipwindow:
return
self.tipwindow = tw = tk.Toplevel(self.anchor_widget)
# show no border on the top level window
tw.wm_overrideredirect(1)
try:
# This command is only needed and available on Tk >= 8.4.0 for OSX.
# Without it, call tips intrude on the typing process by grabbing
# the focus.
tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
"help", "noActivates")
except tk.TclError:
pass
self.position_window()
self.showcontents()
self.tipwindow.update_idletasks() # Needed on MacOS -- see #34275.
self.tipwindow.lift() # work around bug in Tk 8.5.18+ (issue #24570)
def position_window(self):
"""(re)-set the tooltip's screen position"""
x, y = self.get_position()
root_x = self.anchor_widget.winfo_rootx() + x
root_y = self.anchor_widget.winfo_rooty() + y
self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))
def get_position(self):
"""choose a screen position for the tooltip"""
# The tip window must be completely outside the anchor widget;
# otherwise when the mouse enters the tip window we get
# a leave event and it disappears, and then we get an enter
# event and it reappears, and so on forever :-(
#
# Note: This is a simplistic implementation; sub-classes will likely
# want to override this.
return 20, self.anchor_widget.winfo_height() + 1
def showcontents(self):
"""content display hook for sub-classes"""
# See ToolTip for an example
raise NotImplementedError
def hidetip(self):
"""hide the tooltip"""
# Note: This is called by __del__, so careful when overriding/extending
tw = self.tipwindow
self.tipwindow = None
if tw:
try:
tw.destroy()
except tk.TclError: # pragma: no cover
pass
class OnHoverTooltipBase(TooltipBase):
"""abstract base class for tooltips, with delayed on-hover display"""
def __init__(self, anchor_widget, hover_delay=1000):
"""Create a tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(OnHoverTooltipBase, self).__init__(anchor_widget)
self.hover_delay = hover_delay
self._after_id = None
self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)
def __del__(self):
try:
self.anchor_widget.unbind("<Enter>", self._id1)
self.anchor_widget.unbind("<Leave>", self._id2) # pragma: no cover
self.anchor_widget.unbind("<Button>", self._id3) # pragma: no cover
except tk.TclError: # pragma: no cover
pass
super(OnHoverTooltipBase, self).__del__()
def _show_event(self, event=None):
"""event handler to display the tooltip"""
if self.hover_delay:
self.schedule()
else:
self.showtip()
def _hide_event(self, event=None):
"""event handler to hide the tooltip"""
self.hidetip()
def schedule(self):
"""schedule the future display of the tooltip"""
self.unschedule()
self._after_id = self.anchor_widget.after(self.hover_delay,
self.showtip)
def unschedule(self):
"""cancel the future display of the tooltip"""
after_id = self._after_id
self._after_id = None
if after_id:
self.anchor_widget.after_cancel(after_id)
def hidetip(self):
"""hide the tooltip"""
try:
self.unschedule()
except tk.TclError: # pragma: no cover
pass
super(OnHoverTooltipBase, self).hidetip()
def showcontents(self):
"""content display hook for sub-classes"""
# See ToolTip for an example
raise NotImplementedError
class Hovertip(OnHoverTooltipBase):
"""A tooltip that pops up when a mouse hovers over an anchor widget."""
def __init__(self, anchor_widget, text, hover_delay=1000):
"""Create a text tooltip with a mouse hover delay.
anchor_widget: the widget next to which the tooltip will be shown
hover_delay: time to delay before showing the tooltip, in milliseconds
Note that a widget will only be shown when showtip() is called,
e.g. after hovering over the anchor widget with the mouse for enough
time.
"""
super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay)
self.text = text
def showcontents(self):
label = tk.Label(self.tipwindow, text=self.text, justify=tk.LEFT,
background="#ffffe0", relief=tk.SOLID, borderwidth=1)
label.pack()
Now some code illustrating the problem I'm having:
class PopupWindow:
def __init__(self, parent):
self.parent = parent
self.gui = tk.Toplevel(self.parent)
self.gui.geometry("100x30")
self.gui.wait_visibility()
self.ok_button = tk.Button(self.gui, text="OK", command=self.on_ok_button)
self.ok_button.pack()
Hovertip(self.ok_button, text="OK button", hover_delay=500)
def on_ok_button(self):
self.gui.destroy()
def show(self):
self.gui.grab_set()
# Hovertip(self.ok_button, text="OK button", hover_delay=500)
self.gui.wait_window()
return 0
class App(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
button = tk.Button(parent, text="Button -- no hover delay", command=self.button)
button.pack()
Hovertip(button, "This is tooltip\ntext for button.", hover_delay=0)
def button(self):
window = PopupWindow(self.parent)
window.show()
if __name__ == '__main__':
root = tk.Tk()
App(root)
root.mainloop()
You'll notice that the tooltip for the OK button in the modal window appears behind the window (I'm changing the geometry of the window for otherwise it would be so small that we wouldn't actually notice this).
Of course this becomes a problem in a real window with several widgets the tips for some of them will not be seen at all.
Apparently there are two ways around the problem: one is to delete the line self.gui.wait_visibility() in the __init__ of the PopupWindow class;
the other is to delete the self.gui.grab_set() in the show() method.
With any of these the window is no longer modal (if I get the meaning right: I mean I want the window to stay on top and prevent changes in the parent window while it exists).
The commented line in the show method was my tentative of working around it by defining the tooltip after the grab_set so that it might come on top, but it doesn't work either.
I suppose there must be an way of doing this properly using this class for tooltips.
How can I do it?
Thanks.
I found a solution.
Apparently, someone had the same problem, although with a different class for tooltips.
I refer to this question and answer.
The solution is to add the line tw.wm_attributes("-topmost", 1) somewhere in the showtip method of the TooltipBase class.
I did it as a last line, but I'm not sure it doesn't work some other place; I know it doesn't if immediately after tw.wm_overrideredirect(1).
I have a splash window in a tkinter application, which is a child class of Toplevel. My target is to show a GIF moving, thus I am trying with a recurrent .after() function to update a Label. My code for the Splash window is:
import tkinter as tk
class Splash(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.overrideredirect(True)
self.parent = parent
frames = [tk.PhotoImage(file="myfile.gif",format = 'gif -index %i' %(i)) for i in range(89)] #it has 89 frames
def animate(ind, label):
frame = frames[ind]
ind += 1
if ind>88: #With this condition it will play gif infinitely
ind = 0
label.configure(image=frame)
label.update()
self.after(110, animate, ind, label)
label = tk.ttk.Label(self)
label.pack(expand=1)
self.after(0, animate, 0, label)
self.parent.overrideredirect(True)
self.update()
self.lift()
Now: I tried to also put a print function before the next after call, and I could see that it reached that step. However, if I place a print(str(ind)) at the beginning of the animate function, I see only the first index appearing.
How can I actually loop in a Toplevel window in the correct way?
EDIT:
To add context to this, I have the main class App where I do the following:
from ttkthemes import ThemedStyle, ThemedTk
import GUI.splash as sp
class App:
def __init__(self, ...):
self.interface = ThemedTk()
self.interface.iconify()
splash = sp.Splash(self.interface)
...
self.interface.after(3000, splash.destroy) #Thanks #CoolCloud
self.interface.deiconify()
self.interface.mainloop()
EDIT 2:
I have tried with a minimum reproducible example:
import tkinter as tk
class Splash(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.overrideredirect(True)
self.parent = parent
frames = [tk.PhotoImage(file="C:/temp/splash.gif",format = 'gif -index %i' %(i)) for i in range(89)] #it has 89 frames
def animate(ind, label):
frame = frames[ind]
ind += 1
if ind>88: #With this condition it will play gif infinitely
ind = 0
label.configure(image=frame)
label.update()
self.after(110, animate, ind, label)
label = tk.Label(self)
label.pack(expand=1)
self.after(0, animate, 0, label)
self.update()
self.lift()
class App:
def __init__(self):
self.interface = tk.Tk()
self.interface.iconify()
splash = Splash(self.interface)
self.interface.after(3000, splash.destroy)
self.interface.deiconify()
self.interface.mainloop()
if __name__ == "__main__":
App()
It looks like the actual problem is the size of my underlying window, or at least that's what I would think, since in my case I load multiple frames (one on top of the other one) and switch between them depending on an OptionMenu. Maybe this is too much, however it works much better than destroying and rebuilding everything.
Btw, keeping my app heavy as it is, is there a better way to produce a splash screen to load my interface and show a gif moving as in my minimal example? The gif can be downloaded here: https://images.app.goo.gl/Bpi6EvpJgodiQs2e7
A common problem with Tkinter is that in order to use images in Labels and Buttons, you need a reference to the PhotoImage object... somehow.
I have written a wrapper class around Button to add my own functionalities, because I want to use GIFs instead of images, and I want to be able to switch between gifs when I press the button (or use a keyboard hotkey). The first GIF runs fine and loops perfectly. When I switch to the second GIF, I get the error message, saying _tkinter.TclError: image "pyimage48 ... pyimage55" doesn't exist. It looks like the following:
from tkinter import *
from PIL import ImageTk, Image
class AnimatedButton(Button)
def __init__(self, master, size, img_paths):
self.size = size
self.seq_count = len(img_paths) # Number of gif files
self.sequences = []
for path in img_paths:
gif, delay = loadGif(path)
# Create a tuple of all frames in a gif, with the delay between frames. Store this tuple in self.sequences
self.sequences.append(([ImageTk.PhotoImage(frame) for frame in gif], delay))
self.delay = self.sequences[0][1]
self.current_sequence = self.sequences[0][0]
self.image = self.current_sequence[0]
self.seq_id = 0 # Sequence counter
self.frame_id = 0 # Frame counter
Button.__init__(self, master, image=self.image, width=size, height=size)
self.cancel = self.after(self.delay, self.play)
def play(self):
self.image = self.current_sequence[self.frame_id]
self.config(image=self.image)
# More stuff below to loop through the frames etc.
What is strange is that I don't have any of this with my other Button class, MyButton, also a wrapper class.
class MyButton(Button):
def __init__(self, master, size, img_paths):
self.image_count = len(img_paths)
self.image_id = 0
self.size = size
self.images = []
for path in img_paths:
try:
im = Image.open(path)
except:
print("Could not open file {}".format(path))
photo_image = ImageTk.PhotoImage(im, image_mode)
self.images.append(photo_image)
self.image = self.images[0]
Button.__init__(self, master, image=self.image, width=size,
height=size)
Most Google searches came up with the fact that you shouldn't use two tkinter.Tk() calls, but I am only using one (Yes, I made sure).
Any ideas are very much appreciated!
Thanks to the hint by stovfl in the comments above, I was missing [0] in play():
Correct code should be:
def play(self):
self.image = self.current_sequence[self.frame_id][0]
self.config(image=self.image)
# More stuff below to loop through the frames etc.
Each OS has an activity indicator. OS X and iOS has a flower that lights up and fades each pedal in a circular pattern. Windows has a spinning blue disk thing. Android has a gray disk thing (I think it can be in a variety of other colors, too - IDK, I don't use Android much.)
What's the best way to use these icons in Tkinter? Is there some built in widget that provides this? Is there maybe a variable that I can point an Image widget at to get it to display this icon (and animate it?)
I know Tkinter provides a Progress Bar, which has an indeterminate mode. I don't want to use that - I need something that fits in a small square area, not a long rectangular area. The activity indicator as described in the first paragraph will be perfect.
Or is my best option going to be to just roll my own canvas animation?
This should be enough for what you trying to do. I would than create my own animation or download from the internet frame by frame which you want to display. Canvas are more for interactivity with the user thats why I am using a label for displaying the images...
import tkinter as tk
class Activity(tk.Label):
def __init__(self, master = None, delay = 1000, cnf = {}, **kw):
self._taskID = None
self.delay = delay
return super().__init__(master, cnf, **kw)
# starts the animation
def start(self):
self.after(0, self._loop)
# calls its self after a specific <delay>
def _loop(self):
currentText = self["text"] + "."
if currentText == "....":
currentText = ""
self["text"] = currentText
self._taskID = self.after(self.delay, self._loop)
# stopps the loop method calling its self
def stop(self):
self.after_cancel(self._taskID)
# Depends if you want to destroy the widget after the loop has stopped
self.destroy()
class AcitivityImage(Activity):
def __init__(self, imagename, frames, filetype, master = None, delay = 1000, cnf = {}, **kw):
self._frames = []
self._index = 0
for i in range(frames):
self._frames.append(tk.PhotoImage(file = imagename+str(i)+'.'+str(filetype)))
return super().__init__(master, delay, cnf, **kw)
def _loop(self):
self["image"] = self._frames[self._index]
# add one to index if the index is less then the amount of frames
self._index = (self._index + 1 )% len(self._frames)
self._taskID = self.after(self.delay, self._loop)
root = tk.Tk()
root.geometry("500x500")
# create a activity image widget
#root.b = AcitivityImage("activity", 3, "png", root)
#root.b.pack()
# create the activity widget
root.a = Activity(root, 500, bg = "yellow")
root.a.pack()
# start the loop
root.a.start()
#root.b.start()
# stop the activity loop after 7 seconds
root.after(7000, root.a.stop)
#root.after(8000, root.b.stop)
root.mainloop().
I have multiple Tkinter.labels in a row and i would like the user to be able to click and drag their mouse over each one, activating them in the process.
I know about bindings, but i need multiple events in a single binding. Ive been messing around with <Button-1> and <Enter>, however i need a callback to be called only when both are true.
I know l.bind('<Button-1>,<Enter>', ...) is not valid.
Anyone with more Tkinter experience know of a way to chain binding, or make a multi-bind??
The way you solve this particular problem is to have a binding on ButtonPress and ButtonRelease that sets a flag. Then, in your binding for <Enter> (or any other event) you check for that flag.
However, while the button is pressed you won't get any <Enter> events. This is because the widget you clicked over grabs the pointer and owns it until you release the button. The only <Enter> events you'll get while the button is pressed are when you enter the widget you originally clicked on.
What you want to do instead is bind to <B1-Motion>. You can then use the x/y coordinates of the event and winfo_containing to determine what widget you are over.
That being said, trying to simulate selection over a row of labels is a lot of work for very little benefit. Why not just use a text widget that already has selection built in? You can tweak it so that it looks like a bunch of labels (ie: make the background the same color as a frame) and you can turn editing off. That might be an easier way to go.
I encountered this same problem today and thanks to #Bryan Oakley's answer I was able to code a working solution. I will share my code in the hope that it will help someone someday.
This example builds 2 tkinter TreeViews, and enables dragging-and-dropping treeItems between the 2 trees. The key point is that by binding both trees to the B1-motion event, both trees are able to respond to the events.
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class TreeItem:
"""
Keeps a reference to a treeItem together with its parent tree.
"""
def __init__(self, tree, item):
self.tree = tree
self.item = item
self.itemTxt = tree.item(item,"text")
def __str__(self):
"""
Prints 'treename, itemname' upon calling str(TreeItem)
"""
return f'{self.tree}, {self.itemTxt}'
class Mouse(metaclass=Singleton):
"""
Handles treeitem clicking, dragging, dropping and shows feedback messages about them.
"""
def __init__(self, root):
self.root = root
self.clicked_item = None
self.is_dragging = False
self.drag_time = 0
self.current_hovering_widget = None
def OnMouseDown(self, event):
clicked_item = self.get_item_under_mouse(event)
print("You clicked on", str(clicked_item))
self.clicked_item = clicked_item
def OnDrag(self, event):
self.is_dragging = True
self.show_drag_init_msg()
self.show_hovering_item_change_msg(event)
self.drag_time += 1
def OnMouseUp(self, event):
if self.is_dragging:
self.finish_drag()
self.show_drop_msg()
self.clicked_item = None
def finish_drag(self):
self.is_dragging = False
self.drag_time = 0
def show_drag_init_msg(self):
if self.drag_time == 0:
print("You are now dragging item", self.clicked_item.tree, self.clicked_item.itemTxt)
def show_hovering_item_change_msg(self, event):
currently_hovering = self.get_item_under_mouse(event)
if str(self.current_hovering_widget) != str(currently_hovering):
print("Mouse is above", str(currently_hovering))
self.current_hovering_widget = currently_hovering
def show_drop_msg(self):
dragged_item:TreeItem = self.clicked_item
dragged_onto:TreeItem = self.current_hovering_widget
print(f'You dropped {str(dragged_item)} onto {str(dragged_onto)}')
def get_item_under_mouse(self, event):
current_tree = self.root.winfo_containing(event.x_root, event.y_root)
current_tree_item = current_tree.identify("item", event.x, event.y)
return TreeItem(tree=current_tree, item=current_tree_item)
class Tree:
def __init__(self, root, row, col):
self.root: tk.Tk = root
self.create_tree(root, row, col)
def OnDrag(self,event):
Mouse(self.root).OnDrag(event)
def OnMouseDown(self, event):
Mouse(self.root).OnMouseDown(event)
def OnMouseUp(self, event):
Mouse(self.root).OnMouseUp(event)
def create_tree(self, root, row, col):
self.tree = ttk.Treeview(root)
self.tree.heading('#0', text='Departments', anchor='w')
self.tree.grid(row=row, column=col, sticky='nsew')
self.add_dummy_data()
# add bindings
self.tree.bind("<ButtonPress-1>", self.OnMouseDown)
self.tree.bind("<ButtonRelease-1>", self.OnMouseUp)
self.tree.bind("<B1-Motion>", self.OnDrag)
def add_dummy_data(self):
# adding data
self.tree.insert('', tk.END, text='Administration', iid=0, open=False)
self.tree.insert('', tk.END, text='Logistics', iid=1, open=False)
self.tree.insert('', tk.END, text='Sales', iid=2, open=False)
self.tree.insert('', tk.END, text='Finance', iid=3, open=False)
self.tree.insert('', tk.END, text='IT', iid=4, open=False)
# adding children of first node
self.tree.insert('', tk.END, text='John Doe', iid=5, open=False)
self.tree.insert('', tk.END, text='Jane Doe', iid=6, open=False)
self.tree.move(5, 0, 0)
self.tree.move(6, 0, 1)
root = tk.Tk()
root.geometry('620x200')
# make two trees
tree1 = Tree(root,0,0)
tree2 = Tree(root,0,1)
# run the app
root.mainloop()