[Python] Python Tkinter: resizing with .geometry() vs .config(width=..., height=...)

Discussão em 'Python' iniciado por Stack, Junho 25, 2025.

  1. Stack

    Stack Membro Participativo

    I am using Python 3.11 on Windows.

    The code below creates this window:

    [​IMG]

    I want to be able to resize the main window with these contraints:

    1. The dark area must always be a square
    2. The light green area must always be 200 pixels wide
    3. The status bar must always remain at the bottom

    The code works (mostly) but I have several questions about it.

    import tkinter as tk
    from tkinter import ttk


    class App:
    def __init__(self, root):
    self.root = root
    self.DATAFRAME_WIDTH = 200
    self.CANVAS_MIN_DIMENSION = 300

    # Styles
    self.style = ttk.Style(self.root)
    self.style.theme_use("clam")
    self.dark = ttk.Style()
    self.dark.configure('Dark.TFrame', background='#343434')
    self.light_green = ttk.Style()
    self.light_green.configure('LightGreen.TFrame', background='#a4edde')

    # Main frames
    self.top_container = ttk.Frame(self.root)
    self.status_bar = ttk.Frame(self.root)
    self.top_container.pack(fill='both', expand=True)
    self.status_bar.pack(fill='x', expand=False)

    # Frames inside top_container
    self.frm_canvas = ttk.Frame(self.top_container, style='Dark.TFrame')
    self.frm_data_display = ttk.Frame(self.top_container, style='LightGreen.TFrame')
    self.frm_canvas.pack(side='left', fill='both', expand=True)
    self.frm_canvas.config(height=500, width=500)
    self.frm_canvas.pack_propagate(False)
    self.frm_data_display.pack(side='left', fill='both', expand=True)
    self.frm_data_display.config(height=500, width=self.DATAFRAME_WIDTH)
    self.frm_data_display.pack_propagate(False)

    # Bind mouse movement inside the frm_canvas frame to show the coordinates
    self.frm_canvas.bind("<Motion>", self.update_coordinates)

    # Place some labels
    # 1: in the status bar
    self.lbl_left = ttk.Label(self.status_bar, relief=tk.GROOVE, text="Canvas size: ")
    self.lbl_center = ttk.Label(self.status_bar, relief=tk.GROOVE, text="Data frame size: ")
    self.lbl_right = ttk.Label(self.status_bar, relief=tk.GROOVE, text="Window size: ")
    self.lbl_left.pack(side='left', fill='x', expand=True)
    self.lbl_center.pack(side='left', fill='x', expand=True)
    self.lbl_right.pack(side='left', fill='x', expand=True)
    # 2: in the canvas frame
    self.lbl_canvas = ttk.Label(self.frm_canvas, text="Canvas size:\nData frame size:\nStatus bar size:")
    self.lbl_canvas_coordinates = ttk.Label(self.frm_canvas, text="Pointer coordinates:\n")
    self.lbl_canvas.pack(side='top', expand=True)
    self.lbl_canvas_coordinates.pack(side='top', expand=True)
    # 3: in the data display frame
    self.lbl_data = ttk.Label(self.frm_data_display, text="Data frame size:\n")
    self.lbl_data.pack(side='left', expand=True)

    # Force update so that .winfo_*() methods return correct values
    self.top_container.update()
    self.status_bar.update()
    self.frm_canvas.update()
    self.frm_data_display.update()

    # Update the labels with the initial dimensions of the frames and the window
    data_frame_dimensions = f'{self.frm_data_display.winfo_width()}x{self.frm_data_display.winfo_height()}'
    canvas_dimensions = f'{self.frm_canvas.winfo_width()}x{self.frm_canvas.winfo_height()}'
    self.lbl_left.config(text=f"Canvas size: {canvas_dimensions}")
    self.lbl_center.config(text=f"Data frame size: {data_frame_dimensions}")
    self.lbl_right.config(text=f"Window size: {self.root.winfo_width()}x{self.root.winfo_height()}")
    self.lbl_canvas.config(text=f"Canvas size:\n{canvas_dimensions}")
    self.lbl_data.config(text=f"Data frame size:\n{data_frame_dimensions}")

    # Initialize resize counter, which will be printed in the console to track the number of times the window is resized
    self.resize_counter = 0

    # Enforce certain dimension constraints:
    # 1. The canvas frame should be a square (aspect ratio of 1:1)
    # 2. The canvas should not be smaller than 300x300
    # 3. The data frame should always be 200 pixels wide
    # 4. The window should not be smaller than the sum of the canvas size and the status bar height.
    self.enforce_fixed_aspect_ratio()



    def enforce_fixed_aspect_ratio(self):
    self.root.bind("<Configure>", self.adjust_frame_sizes)

    def adjust_frame_sizes(self, event):
    self.resize_counter += 1

    # Unbind the event to prevent recursive calls. Is this a good practice?
    self.root.unbind("<Configure>")

    # Get new window dimensions after resizing event
    # Beware: if I were to add padding to the widgets, I would need to account for it here
    window_width = self.root.winfo_width()
    window_height = self.root.winfo_height()
    desired_canvas_height = window_height - self.status_bar.winfo_height()
    desired_canvas_width = window_width - self.DATAFRAME_WIDTH
    # The canvas frame needs to be a square
    desired_size = min(desired_canvas_height, desired_canvas_width)
    # The canvas frame needs to be at least 200 pixels wide and tall
    desired_size = max(desired_size, self.CANVAS_MIN_DIMENSION)

    print(f"Resizing event #{self.resize_counter}, {window_width=}, {window_height=}, {desired_canvas_width=}, {desired_canvas_height=}, {desired_size=}")

    # Update the different frame sizes
    self.frm_canvas.config(width=desired_size, height=desired_size)
    self.frm_canvas.pack_propagate(False)
    self.frm_data_display.config(width=self.DATAFRAME_WIDTH, height=desired_size)
    self.frm_data_display.pack_propagate(False)

    # The .config(width=..., height=...) does not change the actual size of the window,
    # so we need to set the geometry of the root window
    # Why isn't .config() enough and it is necessary to use .geometry()?
    if self.root.winfo_height() != (desired_size + self.status_bar.winfo_height()) or self.root.winfo_width() != (desired_size + self.DATAFRAME_WIDTH):
    self.root.geometry(f'{desired_size + self.DATAFRAME_WIDTH}x{desired_size + self.status_bar.winfo_height()}')
    #self.root.bind("<Configure>", self.adjust_frame_sizes)

    # This return seen here
    # https://stackoverflow.com/questions/68612137/how-to-resize-root-window-size-with-aspect-ratio-in-tkinter/68613478#68613478
    # but it works without it
    #return "break"


    # Update the labels with the new dimensions
    self.frm_canvas.update()
    self.frm_data_display.update()
    data_frame_dimensions = f'{self.frm_data_display.winfo_width()}x{self.frm_data_display.winfo_height()}'
    canvas_dimensions = f'{self.frm_canvas.winfo_width()}x{self.frm_canvas.winfo_height()}'
    status_bar_dimensions = f'{self.status_bar.winfo_width()}x{self.status_bar.winfo_height()}'
    self.lbl_left.config(text=f"Canvas size: {canvas_dimensions}")
    self.lbl_center.config(text=f"Data frame size: {data_frame_dimensions}")
    self.lbl_right.config(text=f"Window size: {self.root.winfo_width()}x{self.root.winfo_height()}")
    self.lbl_canvas.config(text=f"Canvas size: {canvas_dimensions}\nData frame size: {data_frame_dimensions}\nStatus bar size: {status_bar_dimensions}")
    self.lbl_data.config(text=f"Data frame size:\n{data_frame_dimensions}")

    # Re-bind the event to allow further resizing
    self.root.bind("<Configure>", self.adjust_frame_sizes)


    def update_coordinates(self, event):
    # Get the mouse coordinates
    x, y = event.x, event.y
    # Update the label with the coordinates
    self.lbl_canvas_coordinates.config(text=f"Pointer coordinates:\n({x}, {y})")


    if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    root.mainloop()

    • Q1: When handling a <Configure> event, in this case for resizing the window, is it a good practice to unbind the event in the handler and re-bind it when everything has been done?
    • Q2: If I only set the widgets' dimensions with self.frm_canvas.config(width=desired_size, height=desired_size) or self.frm_data_display.config(width=self.DATAFRAME_WIDTH, height=desired_size), the window is not redrawn with those sizes. I have to force self.root.geometry(f'{desired_size + self.DATAFRAME_WIDTH}x{desired_size + self.status_bar.winfo_height()}') instead. What is the explanation for this? I would imagine .geometry() takes precedence/priority over .config(), but in this case I am trying to achieve the same thing with 2 different approaches.
    • Q3: The documentation discourages using w.update() from an event callback or a function that is called from an event callback. In my callback adjust_frame_sizes() I am using it in order to "collect" the real dimensions of the widgets. How would I do it without using update() inside the callback, as recommended?
    • Q4: If I select some text on the console from which I launched the program, go back to the application window and try to move/resize it, my application becomes unresponsive and I have to force terminate it. Unless I click on the console again and press Enter, which seems to "liberate" the window application and make it responsive again. Why is that? Is the console messing with the mainloop?

    Continue reading...

Compartilhe esta Página