Home > Software engineering >  Tkinter Window Configure Event gives wrong values
Tkinter Window Configure Event gives wrong values

Time:01-04

I am trying to react to a tk window changing size. After some frustration with winfo_width and winfo_reqwidth I decided to only rely on the values of the <Configure> event to be informed about the actual size of the window.

The event gets called a few times during startup however as soon as an additional widget is involved the final event during startup does not reflect the actual window size.

from tkinter import*

tk = Tk()

tk.geometry("500x300")
tk.bind("<Configure>", lambda e: print(e))

f = Frame(tk)
f.place(width = 100, height = 50)

tk.mainloop()

The code above produces the following output

<Configure event x=260 y=260 width=500 height=300>
<Configure event x=0 y=0 width=100 height=50>

Then only after the window (not the widgets within) is interacted with again, is another event with the correct size fired.

<Configure event x=260 y=260 width=500 height=300>

This can be mitigated by querying winfo_height during the event callback but that seems unintended as the event is already providing width and height values. I was not able to find any exhaustive documentation on the winfo methods and the usages are littered with special cases.

Can anyone either provide documentation or insight in how the intended usage of these methods and events is?

CodePudding user response:

The reason for this is that when you bind an event to the root window, every widget in that window inherits that binding. The first output in your question represents the root window, and the second output represents the frame you placed in the window.

This is an intentional feature of the event handling system. It is what allows you to create bindings that apply to every widget within a window (for example, copy and paste shortcuts).

If you only want to process the event for the root window, add a conditional clause in a function instead of using lambda:

def handle_configure(event):
    if event.widget == tk:
        print(event)
...
tk.bind("<Configure>", handle_configure)

If you don't like hard-coding the root window in the conditional, you can also change it to this:

if event.widget == event.widget.winfo_toplevel():

winfo_toplevel() returns the window associated with the given widget.


The reason for this behavior lies in the fundamental design of the event handling mechanism. When you bind an event to a window, you aren't actually binding to the window. Instead, you are binding to a tag (a "binding tag" or "bindtag") that has the same name as the window.

When a widget receives an event, the event processing machinery will get a list of tags for a widget, and then compare the event to every binding tag in order. If a binding is found, the bound function is called.

By default every widget gets a list four binding tags in the following order:

  1. a tag for the widget itself,
  2. a tag for the class of the widget (eg: Button, Label, etc),
  3. a tag for the window containing the widget, either the root window, or an instance of Toplevel, and
  4. the special tag "all"

You can examine the bind tags for any widget by printing the results of the bindtags method. For example, if you add print(f.bindtags()) after defining your frame you'll see the following output:

('.!frame', 'Frame', '.', 'all')

This is why binding to the root window will cause every widget to inherit the binding. Like I showed in the example at the start of the answer, the simplest solution is to add a conditional so that you only execute code when the function is called for the root window.

Note: another solution is to add an additional binding tag to just the root window, and then bind to that instead. In that case the event will only be processed for the root window. You can use the bind_class method to bind to the class even though this custom tag isn't technically a class.

tk.bindtags((str(tk), "Tk", "all", "root_window"))
tk.bind_class("root_window", "<Configure>", lambda e: print(e))

CodePudding user response:

To work around this behavior on start up of your application you can use update_idletasks(). I believe (but am not 100% sure) that this is due to how the window is being initialized causing the print statement to post before the window is done forming.

Please also not that it is better to import tkinter as tk instead of importing *. This will help with debuging down the road and prevent overwriting issues.

EDIT: Updated to show what widget is being printed to console to better show what sizes you are seeing for what widget as Brian pointed out in the comments.

import tkinter as tk

root = tk.Tk()
root.geometry("500x300")


def print_window_size(event):
    root.update_idletasks()
    if event.widget == root:
        print(f' Widget: {event.widget} = root window - Event: {event}')
    elif isinstance(event.widget, tk.Frame):
        print(f' Widget: {event.widget} = frame - Event: {event}')
    else:
        print(f' Widget: {event.widget} - all other widgets - Event: {event}')


root.bind("<Configure>", lambda e: print_window_size(e))
f = tk.Frame(root)
f.place(width=100, height=50)

tk.mainloop()

Output:

 Widget: .!frame = frame - Event: <Configure event x=0 y=0 width=100 height=50>
 Widget: . = root window - Event: <Configure event x=26 y=26 width=500 height=300>
  • Related