Is it possible in Tkinter to pass an event directly to the parent widget?
I have a canvas wich is covered by a grid of other canvases (is that the plural?), which I added using the parent_canvas.create_window() method. I want some of the events, e.g mouse release events, to be handled by the parent canvas.
If I only bind the event a parent method, the event.x and event.y coordinates come out relative to the child canvas which catches the event.
Tkinter does not pass events to parent widgets. However, you can simulate the effect through the use of bind tags (or "bindtags").
The shortest explanation I can give is this: when you add bindings to a widget, you aren't adding a binding to a widget, you are binding to a "bind tag". This tag has the same name as the widget, but it's not actually the widget.
Widgets have a list of bind tags, so when an event happens on a widget, the bindings for each tag are processed in order. Normally the order is:
bindings on the actual widget
bindings on the widget class
bindings on the toplevel widget that contains the widget
bindings on "all"
Notice that nowhere in that list is "bindings on the parent widget".
You can insert your own bindtags into that order. So, for example, you can add the main canvas to the bind tags of each sub-canvas. When you bind to either, the function will get called. Thus, it will appear that the event is passed to the parent.
Here's some example code written in python 2.7. If you click on a gray square you'll see two things printed out, showing that both the binding on the sub-canvas and the binding on the main canvas fire. If you click on a pink square you'll see that the sub-canvas binding fires, but it prevents the parent binding from firing.
With that, all button clicks are in effect "passed" to the parent. The sub-canvas can control whether the parent should handle the event or not, by returning "break" if it wants to "break the chain" of event processing.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.main = tk.Canvas(self, width=400, height=400,
borderwidth=0, highlightthickness=0,
background="bisque")
self.main.pack(side="top", fill="both", expand=True)
# add a callback for button events on the main canvas
self.main.bind("<1>", self.on_main_click)
for x in range(10):
for y in range(10):
canvas = tk.Canvas(self.main, width=48, height=48,
borderwidth=1, highlightthickness=0,
relief="raised")
if ((x+y)%2 == 0):
canvas.configure(bg="pink")
self.main.create_window(x*50, y*50, anchor="nw", window=canvas)
# adjust the bindtags of the sub-canvas to include
# the parent canvas
bindtags = list(canvas.bindtags())
bindtags.insert(1, self.main)
canvas.bindtags(tuple(bindtags))
# add a callback for button events on the inner canvas
canvas.bind("<1>", self.on_sub_click)
def on_sub_click(self, event):
print "sub-canvas binding"
if event.widget.cget("background") == "pink":
return "break"
def on_main_click(self, event):
print "main widget binding"
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack (fill="both", expand=True)
root.mainloop()
Related
In the following code, When the toplevel window is destroyed, the command in the
bind statement is executed multiple times. Probably once for each child widget within the Top Level. When I change the toplevel to a Frame, the bind command only executes once.
In the example, quit() or raise SystemExit are deferred until the command finishes its looping. Why is this happening?
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
class PlainFrame(tk.Frame):
def __init__(self,parent):
super().__init__(parent)
self.butts = []
for i in range(20) :
# button
self.butts.append(ttk.Button(self, text=f'Click Me {i}'))
self.butts[i]['command'] = self.button_clicked
self.butts[i].grid(column=0,row=i)
self.pack()
def button_clicked(self):
showinfo(title='Information',
message='Hello, Tkinter!')
class MainFrame(tk.Toplevel):
#class MainFrame(tk.Frame):
def __init__(self, container,*args,**kwargs):
super().__init__(container,kwargs)
options = {'padx': 5, 'pady': 5}
# label
self.label = ttk.Label(self, text='Hello, Tkinter!')
self.label.pack(**options)
self.quit_button = ttk.Button(self,text='Quit',
command = self._quit)
self.quit_button.pack()
self.frame = PlainFrame(self)
# add when frame
#self.pack()
def button_clicked(self):
showinfo(title='Information',
message='Hello, Tkinter!')
def _quit(self):
self.destroy()
class App(tk.Tk):
def __init__(self):
super().__init__()
# configure the root window
self.title('My Awesome App')
self.geometry('600x100')
def quitting(self,event):
print ('just passing through')
quit()
raise SystemExit
if __name__ == "__main__":
app = App()
frame = MainFrame(app)
# app.withdraw()
frame.bind('<Destroy>',app.quitting)
app.mainloop()
With toplevel binding for the bound command executes multiple times
Yes, this is how tkinter is designed to work.
When you bind to something, you don't bind to a widget. Rather, you bind to a binding tag. Every widget has a set of binding tags. When a widget receives an event, tkinter will check each of its binding tags to see if there's a function bound to it for the given event.
So, what are the widget binding tags? Every widget gets the binding tag "all". Each widget also gets a binding tag named after the widget itself. It gets a third binding tag that is the name of the widget class (eg: "Button", "Label", etc). The fourth tag -- the one causing you trouble -- is the name of the window that contains the widget. The order goes from most to least specific: widget, widget class, window, "all".
You can see this by printing out the binding tags for a widget. Consider the following code:
import tkinter as tk
root = tk.Tk()
toplevel = tk.Toplevel(root)
label = tk.Label(toplevel)
print(f"binding tags for label: {label.bindtags()}")
When run, the above code produces this output:
binding tags for label: ('.!toplevel.!label', 'Label', '.!toplevel', 'all')
The first string, .!toplevel.!label is the internal name of the label widget. Label is the widget class, .!toplevel is the name of the toplevel widget, and then there's the string all.
What happens when you click on the label? First, tkinter will check to see if there is a binding for the button click on the tag .!toplevel.!label. It will have one if you bound to the widget itself. Next, it will check to see if there is a binding on the widget class. Widgets like scrollbars and buttons will have a binding on the widget class, but label won't. Next, tkinter will see if there's a binding on the window itself for the event. And finally, it will see if there is a binding on the special tag all.
You can alter the bindtags for a widget by passing the list of binding tags to the bindtags command. For example, if you want every widget to have a bind tag for the frame, you could set the bindtags to include the frame, and then every widget will respond to an event that is bound to the frame.
You can use the same technique to remove bindings as well. For example, if you wanted to remove all default bindings from a Text widget, you could remove Text from its list of bind tags. Once you do that, the widget will not respond to any key presses or key releases.
The canonical description of how binding tags works is in the bindtags man page for tcl/tk.
The situation is simple. I have a main window with a Help - About menu.
When this menu item is clicked, a modal window is opened (let's say it's an About-window).
With self.grab_set() I disabled the main-window (although the modal window does flicker when you click the main title bar).
So far, so good.
Here is the question: I really like to sound a bell when the user clicks outside the modal window on the main window.
This is what I could find about grab_set(), really not that much:
[effbot] ...a method called grab_set, which makes sure that no mouse or keyboard
events are sent to the wrong window.
[effbot] Routes all events for this application to this widget.
[kite.com] A grab directs all events to this and descendant widgets in the application.
[google books] grab_set() ensures that all of the application's events are sent to w until a corresponding call is made to grab_release ([Me:] or till the window is destroyed?)
I'm not quite sure how to understand this: does it mean you can handle an event on the main window within the modal window (like sounding my bell)?
So I tried things like:
self.bind('<Button-1>', self.bell) Exception in Tkinter callback: _tkinter.TclError: bad window path name
parent.bind('<Button-1>', self.bell) Nothing happens
So, how to sound a bell like when clicked outside the modal window on the main window, like in so many other applications?
Derived questions:
Is it still possible to cature events from the main window after using
grab_set for the modal window?
Is there a way to prevent the flickering?
I really like to understand this mysterious grab_set() method.
Stripped code:
import tkinter as tk
class About(tk.Toplevel):
def __init__(self, parent):
tk.Toplevel.__init__(self, parent)
self.geometry('200x150')
#--- OK button
btn_ok = tk.Button(self, text='OK', command=self.destroy) # destroy with OK
btn_ok.pack(side=tk.TOP)
btn_ok.focus() # destroy with spacebar
#--- Make window modal
self.grab_set()
# self.wait_window() # is this necessary in this case?
# self.bind('<Button-1>', self.bell) ??? The question
class MenuBar(tk.Menu):
def __init__(self, parent):
tk.Menu.__init__(self)
helpmenu = tk.Menu(self, tearoff=0)
helpmenu.add_command(label='About', command=lambda: About(parent))
self.add_cascade(label='Help', menu=helpmenu)
class MainApp():
def __init__(self, parent):
parent.configure(background='#000000')
parent.geometry('800x600')
menubar = MenuBar(parent)
parent.configure(menu=menubar)
if __name__ == '__main__':
root = tk.Tk()
MainApp(root)
root.mainloop()
When you set a grab, all button clicks will go to the window with the grab. You capture them the way you capture any other event. In the case of a button click you do that by binding a function to <1>.
It's important to know that a binding on a root window or a Toplevel window will apply to all widgets in that window. For example, binding to self in your code will fire even when you click on the "Ok" button. Therefore, the callback should probably only do work when the widget associated with the event is the same as the toplevel.
Example:
class About(tk.Toplevel):
def __init__(self, parent):
...
self.bind("<1>", self.capture_click)
...
def capture_click(self, event):
if event.widget == self:
<your logic here>
In the case of wanting to know if the user clicked outside the window, you can use the coordinates of the event object to compare against the window to see if the click is inside or outside.
def on_click(self, event):
if event.widget == self:
if (event.x < 0 or event.x > self.winfo_width() or
event.y < 0 or event.y > self.winfo_height()):
self.bell()
I found a second solution. Though my question was explicitly about using grab_set(), this method does the same for me: making the window as modal as possible and sound a bell.
Instead of using self.grab(), you can also disable the parent window:
parent.attributes('-disabled', True)
Of course it needs to be enabled again when the OK button is clicked (and when the windows is closed with the [X] close control. However, my original About-window has no window decoration). The command for the OK-button becomes:
btn_ok = tk.Button(self, text='OK', command=lambda: self.closeme(parent))
...which calls the closeme function:
def closeme(self, parent):
parent.attributes('-disabled', False)
self.destroy()
The bell sounds automatically when clicking a disabled window.
Method 1: Keeps you in full control of the main window but does not 'freeze' the main window: you can still move it around.
Method 2: Completely freezes the main window, but if it happens to be (partially) covered by another window (not of this application), then you can only bring back to top using Alt+Tab (windows).
I'm sure I will use both techniques in the future depending on my needs.
I made simple script:
from tkinter import *
class MyFrame(Frame):
def __init__(self, parent = None):
Frame.__init__(self, parent, bg = 'red')
self.pack(fill=BOTH, expand=YES)
self.bind('<Key>', lambda e: print("pressed any key"))
root = Tk()
root.geometry("300x200")
f = MyFrame(root)
root.mainloop()
But binding for pressing any key do not work. Nothing happens whey I press any key. Do you know why?
You need to call the bind method of parent, which is a reference to the tkinter.Tk instance that represents the main window:
parent.bind('<Key>', lambda e: print("pressed any key"))
self.bind is calling the bind method of the tkinter.Frame instance created when you did:
Frame.__init__(self, parent, bg = 'red')
The reason the binding didn't seem to work is that the frame that you attached the binding to didn't have keyboard focus. Only the widget with keyboard focus will react to the binding. It's perfectly acceptable to do what you did and bind to a frame, you just need to make sure that the widget you bind to gets the keyboard focus.
There are at least two solutions: give the frame the keyboard focus (with the focus_set method), or put the binding on the main window which is what initially gets the keyboard focus.
Why doesn't clicking on a child element propagate to the parent?
from tkinter import *
root = Tk()
def handler(event):
print('clicked at', event.x, event.y)
frame = Frame(root, width=100, height=100)
label = Label(frame, text="Label")
frame.bind('<Button-1>', handler)
frame.pack()
label.pack(side=TOP)
root.mainloop()
When I run that, clicking on the label doesn't fire the handler. I've understood that events propagate to parents by default and if you didn't want that, you'd have to return "break"
You are incorrect in your original understanding that events propagate to their parent. They do not.
Admittedly, there's an edge case for widgets which are a direct descendant of a toplevel or root window. Even there, it's not that they are propagating to their parent, but rather they are being handled by other bindings as defined by the bind tags, and by default every widget has it's toplevel window as one of it's bind tags.
If you want to set a binding to work everywhere you can use the bind_all method, since each widget has an "all" bindtag by default. Another option is to give several widgets the same bindtag (using the bindtags method), then bind to that bindtag with bind_class. Which choice you make depends on what you are trying to accomplish.
bindtags are extremely powerful -- arguably more powerful than any binding mechanisms from any other toolkit. For example, if you need to have events propagate you can do that by adjusting the bindtags of every widget to include all of its ancestors. In my experience, however, such shenanigans is rarely ever needed.
You're mistaken. "break" causes that event to not propagate to other handlers for the widget that was clicked on.
In other words, if you bound your action to label and then you bound another action to the first button onto label, both callbacks will be called (unless you return "break" from the first one to be called.)
I'm not sure of a workaround though ... (We might need to wait for BryanOakley to show up ;)
In the following block, clicking on a_frame triggers the event handler on_frame_click, but clicking on a_label which is a child of a_frame does not. Is there a way to force a_frame to trap and handle events which originated on it's children (preferably with out having to add handlers to the children directly)? I am using Python 3.2.3.
import tkinter
def on_frame_click(e):
print("frame clicked")
tk = tkinter.Tk()
a_frame = tkinter.Frame(tk, bg="red", padx=20, pady=20)
a_label = tkinter.Label(a_frame, text="A Label")
a_frame.pack()
a_label.pack()
tk.protocol("WM_DELETE_WINDOW", tk.destroy)
a_frame.bind("<Button>", on_frame_click)
tk.mainloop()
Yes, you can do what you want, but it requires a bit of work. It's not that it's not supported, it's just that it's actually quite rare to need something like this so it's not the default behavior.
TL;DR - research "tkinter bind tags"
The Tkinter event model includes the notion of "bind tags". This is a list of tags associated with each widget. When an event is received on a widget, each bind tag is checked to see if it has a binding for the event. If so, the handler is called. If not, it continues on. If a handler returns "break", the chain is broken and no more tags are considered.
By default, the bind tags for a widget are the widget itself, the widget class, the tag for the toplevel window the widget is in, and finally the special tag "all". However, you can put any tags you want in there, and you can change the order.
The practical upshot of all this? You can add your own unique tag to every widget, then add a single binding to that tag that will be processed by all widgets. Here's an example, using your code as a starting point (I added a button widget, to show this isn't something special just for frames and labels):
import Tkinter as tkinter
def on_frame_click(e):
print("frame clicked")
def retag(tag, *args):
'''Add the given tag as the first bindtag for every widget passed in'''
for widget in args:
widget.bindtags((tag,) + widget.bindtags())
tk = tkinter.Tk()
a_frame = tkinter.Frame(tk, bg="red", padx=20, pady=20)
a_label = tkinter.Label(a_frame, text="A Label")
a_button = tkinter.Button(a_frame, text="click me!")
a_frame.pack()
a_label.pack()
a_button.pack()
tk.protocol("WM_DELETE_WINDOW", tk.destroy)
retag("special", a_frame, a_label, a_button)
tk.bind_class("special", "<Button>", on_frame_click)
tk.mainloop()
For more on bindtags, you might be interested in my answer to the question How to bind self events in Tkinter Text widget after it will binded by Text widget?. The answer addresses a different question than the one here, but it shows another example of using bind tags to solve real world problems.
I can't seem to find a direct method of automatically binding to child widgets (though there are methods of binding to an entire class of widgets and to all widgets in an application), but something like this would be easy enough.
def bind_tree(widget, event, callback, add=''):
"Binds an event to a widget and all its descendants."
widget.bind(event, callback, add)
for child in widget.children.values():
bind_tree(child, event, callback, replace_callback)
Just thought of this, but you could also put a transparent widget the size of a_frame on top of everything as a child of a_frame and bind the <Button> event to that, and then you could refer to a_frame as e.widget.master in the callback in order to make it reusable if necessary. That'd likely do what you want.
Based on what it says in the Levels of Binding section of this online Tkinter reference, it sounds like it's possible because you can bind a handler to three different levels.
To summarize:
Instance Level: Bind an event to a specific widget.
Class Level: Bind an event to all widgets of a specific class.
Application Level: Widget independent -- certain events always invoke a specific handler.
For the details please refer to the first link.
Hope this helps.
Depending on what you're trying to do, you could bind everything
print(a_label.bindtags()) # ('.!frame.!label', 'Label', '.', 'all')
tk.bind_class('.', "<Button>", on_frame_click)