0

Simplified example: I have a frame with several canvases and a label contained within it.

When anywhere inside the frame (even if it is also inside the contained canvases or label), I want the mouse scroll wheel or button to trigger a specific eventhandler. If I bind to the frame. It only works if the cursor is inside the frame but outside the child widgets.

(NB This seems different to what happens if I bind to the toplevel window - for that it works when inside any child widget however they are nested, but not if I start the binding below toplevel.)

Don't want to use bind_all or bind_class as in the real full application this eventhandler should only get called when inside this frame but not elsewhere. Could create the same binding on every child widget, but it would be a pain as there will be lots of them eventually.

Is there some setting to force the same type of behaviour for Frames etc as for toplevel window?

Dragon1
  • 1
  • 1

1 Answers1

0

Yes, you can set things up so that events that happen on any widget will trigger a binding on a higher level widget. Unfortunately, that means you have to configure every child widget in one way or another. There's no way to automatically get this behavior.

The mechanism for making this work is called a binding tag (or bindtag). When you bind to a widget you aren't actually binding to the widget. Instead, you are binding to a tag that is the name of a widget. When an event happens on a widget, tkinter will search through all of the bindtags for that widget to see if there is a binding for the event. When a match is found, the bound function is executed.

As an example, let's create a frame with a canvas, and with labels in the canvas.

frame = tk.Frame(root, bg="black")
canvas = tk.Canvas(frame, width=400, height=400, bg="bisque")
canvas.pack(padx=10, pady=10)

for i in range(10):
    label = tk.Label(canvas, text=f"Label #{i+1}")
    label.pack(side="top", padx=20, pady=4)

As mentioned earlier, each widget has a set of binding tags associated with it, and one of those tags is the name of the widget. Thus, the frame has a binding tag with the name of the frame. You can see these binding tags by printing the results of the bindtag method:

print(f"bindtags for {frame}: {frame.bindtags()}")

The above will print this:

bindtags for .!frame: ('.!frame', 'Frame', '.', 'all')

These values represent, in order:

  • the actual widget, so you can create bindings for the specific widget,
  • the widget class, so you can create bindings for all widgets of the same ty pe,
  • the toplevel (or root) window for the widget, so you can bind to all widgets in a window,
  • the special string "all" so you can bind to literally all widgets at once for application-wide hot keys.

If we add the binding tag of the frame to the other widgets, then any bindings on the frame will also be picked up by the other widgets widgets. Again, this is because bindings to widgets aren't actually on the widget but rather on the widget's binding tag (which is set to the name of the widget)

canvas.bindtags((canvas, "Canvas", frame, ".", "all"))
for label in canvas.winfo_children():
    label.bindtags((label, "Label", frame, ".", "all"))

If you set a binding on the frame, the binding will trigger even if you click on the canvas or one of the labels.

For more insights into binding tags, see the following answers which also mention binding tags:


Here is a complete example that has a working version of the above code:

import tkinter as tk

root = tk.Tk()

frame = tk.Frame(root, bg="black")
status_label = tk.Label(root, width=50)
print(f"bindtags for {frame}: {frame.bindtags()}")

status_label.pack(side="top", fill="x")
frame.pack(side="top", fill="both", expand=True)

canvas = tk.Canvas(frame, width=400, height=400, bg="bisque")
canvas.pack(padx=10, pady=10)

for i in range(10):
    label = tk.Label(canvas, text=f"Label #{i+1}")
    label.pack(side="top", padx=20, pady=4)

canvas.bindtags((canvas, "Canvas", frame, ".", "all"))
for label in canvas.winfo_children():
    label.bindtags((label, "Label", frame, ".", "all"))

def handle_click(event):
    status_label.configure(text=f"you clicked on a {event.widget.winfo_class()}")

frame.bind("<1>", handle_click)

root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685