10

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()
Monkeyer
  • 488
  • 4
  • 10

4 Answers4

18

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.

Community
  • 1
  • 1
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • `bindtags` work like a champ! The only thing I did different was `a_label.bindtags((a_frame,) + a_label.bindtags())` which made `b_frame.bind(" – Monkeyer Jul 12 '12 at 19:57
6

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.

JAB
  • 20,783
  • 6
  • 71
  • 80
  • This would trigger the callback, for sure, but the _source_ of the event would still be the child element, instead of the parent. In my example callback, that wouldn't be an issue (since the source is not referenced). If however I wanted start a drag an drop operation on the parent, this would complicate the callback. – Monkeyer Jul 12 '12 at 18:08
  • @Monkeyer I see. In that case, I just thought of something else you could try. – JAB Jul 12 '12 at 18:19
  • Please note, the children must already be created for this to work. In the case of trying to put this in a constructor its a problem. To resolve this issue I have overridden the update method to do the normal update and do the binding. After creating all of the children I call update which fills in the bindings. There is probably a better way, but this works. – Ralph Ritoch Feb 16 '14 at 02:48
0

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:

  1. Instance Level: Bind an event to a specific widget.
  2. Class Level: Bind an event to all widgets of a specific class.
  3. Application Level: Widget independent -- certain events always invoke a specific handler.

For the details please refer to the first link.

Hope this helps.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • Thanks. Instance level binding is what I showed in the original question. Class level binding has same problem, clicking the label does not count as clicking the frame in terms of triggering the frame's event handler. Application Level binding is a quicker (but less fine grained) approach similar to JAB's solution. In this case, the callback will still report the label as the source of the event, and so the callback would have to be sophisticated enough to climb back up the containment hierarchy to find the parent in question, which is what I'll do if I can't find a better option. – Monkeyer Jul 12 '12 at 18:28
  • Going with application level binding means the callback would have to distinguish three cases 1) is the source widget the frame? 2) is the source a child of the frame? 3) is the source something else. This could get very complicated if I have several differing types of containment hierarchies which need to have different behaviors. – Monkeyer Jul 12 '12 at 18:53
  • The "levels of binding" page only tells part of the story. There's not just three levels, there's four by default, and there's potentially an infinite number of levels that can be added (though "level" is a bit of a misnomer -- it's more like a sequence). This is a very under-documented feature of Tkinter, but something that sets Tkinter head and shoulders above the event mechanisms of just about all other toolkits. For more information, google on "bind tags" (or read my answer to this question) – Bryan Oakley Jul 12 '12 at 19:20
0

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)
nda
  • 541
  • 7
  • 18