pythontkinterpython-3.14

Force a widget to be a square


I am using Python 3.14 on Windows 11. I have 2 frames (left and right) inside a top frame and a bottom frame as status bar. I want the left frame to be a square, giving the rest of the horizontal space to the right frame if the window grows.

Bryan Oakley's solution is not suitable because I want to pack or grid more widgets in the right frame. Gary Kerr's solution I am unable to make it work.

Total window width is 700px and the height is 200px (181px the top container and 19px the bottom container). I want the left (dark gray) container to be 181x181, and the right (green) container 700 - 181 = 519 pixels wide and 181 pixels high. These sizes are just an example after resizing the main window. After forcing relative weights with columnconfigure and expanding the window to the right, the left container keeps growing and is not a square:

enter image description here

import tkinter as tk
from tkinter import ttk
from tkinter import Label

class App:
    def __init__(self, root):
        self.root = root
        self.root.geometry("400x200+900+400")

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

        # Main frames: top (for content) and bottom (status bar)
        self.top_container = ttk.Frame(self.root)
        self.bottom_container = ttk.Frame(self.root)
        self.top_container.pack(fill='both', expand=True)
        self.bottom_container.pack(fill='x', expand=False)

        # Left container for future Canvas map
        self.left_container = ttk.Frame(self.top_container, style='Dark.TFrame')
        # Right container for data display
        self.right_container = ttk.Frame(self.top_container, style='LightGreen.TFrame')

        self.top_container.rowconfigure(0, weight=1)
        self.top_container.columnconfigure(0, weight=1)
        self.top_container.columnconfigure(1, weight=2)

        self.left_container.grid(row=0, column=0, sticky='NSWE')
        self.right_container.grid(row=0, column=1, sticky='NSWE')

        # Labels just for displaying sizes
        self.left_label = ttk.Label(self.left_container, text="Left")
        self.right_label = ttk.Label(self.right_container, text="Right")
        self.bottom_label = ttk.Label(self.bottom_container, text="Bottom")
        self.left_label.pack()
        self.right_label.pack()
        self.bottom_label.pack()

        self.root.bind('<Configure>', self.resize)


    def resize(self, event):
        # Remove the binding. It will be bound again later.
        self.root.unbind('<Configure>')

        w, h = event.width, event.height
        w1, h1 = self.root.winfo_width(), self.root.winfo_height()

        self.root.update_idletasks()
        W = self.top_container.winfo_width()
        H = self.top_container.winfo_height()
        lw = self.left_container.winfo_width()
        lh = self.left_container.winfo_height()
        rw = self.right_container.winfo_width()
        rh = self.right_container.winfo_height()

        self.bottom_label.config(text=f'{W=} {H=}')
        self.left_label.config(text=f'{lw=} {lh=}')
        self.right_label.config(text=f'{rw=} {rh=}')

        print(f'SIZES: {W=}  {H=}  {w=}  {h=}  {lw=}  {rw=}  {lh=}  {rh=}')
        print('LEFT WEIGHT', self.top_container.columnconfigure(0)['weight'])
        print('RIGHT WEIGHT', self.top_container.columnconfigure(1)['weight'])

        if W > H:
            if lw != H:
                # self.left_container.grid_forget()
                # self.right_container.grid_forget()
                self.top_container.columnconfigure(0, weight=H)
                self.top_container.columnconfigure(1, weight=W-H)
                # self.left_container.grid(row=0, column=0, sticky='NSWE')
                # self.right_container.grid(row=0, column=1, sticky='NSWE')
            else:
                self.top_container.columnconfigure(0, weight=1)
                self.top_container.columnconfigure(1, weight=1)
        elif H > W:
            # TO DO
            pass

        # Enable binding again
        self.root.bind('<Configure>', self.resize)


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

Solution

  • This worked for me:

    # Source - https://stackoverflow.com/q/79815659
    # Posted by Mike Duke, modified by community. See post 'Timeline' for change history
    # Retrieved 2025-11-10, License - CC BY-SA 4.0
    
    import tkinter as tk
    from tkinter import ttk
    from tkinter import Label
    
    class App:
        def __init__(self, root):
            self.root = root
            self.root.geometry("400x200+900+400")
    
            # Styles
            self.style = ttk.Style(self.root)
            self.style.configure('Dark.TFrame', background='#343434')
            self.style.configure('LightGreen.TFrame', background='#a4edde')
    
            # Main frames: top (for content) and bottom (status bar)
            self.top_container = ttk.Frame(self.root)
            self.bottom_container = ttk.Frame(self.root)
            self.top_container.pack(fill='both', expand=True)
            self.bottom_container.pack(fill='x', expand=False)
    
            # Left container for future Canvas map
            self.left_container = ttk.Frame(self.top_container, style='Dark.TFrame')
            # Right container for data display
            self.right_container = ttk.Frame(self.top_container, style='LightGreen.TFrame')
    
            self.top_container.rowconfigure(0, weight=1)
            self.top_container.columnconfigure(0, weight=0)
            self.top_container.columnconfigure(1, weight=1)
    
            self.left_container.grid(row=0, column=0, sticky='NSWE')
            self.right_container.grid(row=0, column=1, sticky='NSWE')
            
            # Labels just for displaying sizes
            self.left_label = ttk.Label(self.left_container, text="Left")
            self.right_label = ttk.Label(self.right_container, text="Right")
            self.bottom_label = ttk.Label(self.bottom_container, text="Bottom")
            self.left_label.pack()
            self.right_label.pack()
            self.bottom_label.pack()
    
            self.left_container.bind('<Configure>', self.resize)
    
    
        def resize(self, event):
            w, h = event.width, event.height
            widget = event.widget
            grid_info = widget.grid_info()
            container = grid_info['in']
            column = grid_info['column']
            print(f'''resize event: {event!r} - {widget!r} - {container!r} - {column!r}''')
            container.columnconfigure(column, minsize=event.height)
    
    if __name__ == '__main__':
        root = tk.Tk()
        app = App(root)
        root.mainloop()
    

    The details:

    1. Set the column containing the widget you want to be square to not be resizable (weight=0); set the other column to get all the extra space (weight=1).
    2. Bind the <Configure> event to the square widget itself.
    3. In the event handler, identify the widget from the event, then the container and column number from the widget's grid information. Then use that to configure the container's column to have its minsize set to the widget's new height (from the resize event).