Search code examples
pythontkintertkinter-entrytkinter-button

Tkinter: custom frame widget overrides columnconfigure


I am trying to create a tkinter app of 5 columns and 2 rows, containing a ttk.Treeview in the top row (spanning all 5 cols). The bottom row should contain a ttk.Frame in each column. The frames' widths should correspond to the width of the treeview's columns. I have achieved the behavior with a custom empty frame class MyFrameResizable. However, once I fill the custom frame (MyFrame) with other widgets, the resizing behavior breaks down.

import tkinter as tk
from tkinter import ttk


class MyFrame(ttk.Frame):
    def __init__(self, master = None, **kwargs):
        super().__init__(master, **kwargs)

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

        # adding other widgets will destroy resizing behavior
        self.button = tk.Button(self)
        self.button.grid(column=0, row=0, sticky="nesw")

        self.entry = tk.Entry(self)
        self.entry.grid(column=1, row=0, sticky="nesw")


class MyFrameResizable(ttk.Frame):
    def __init__(self, master = None, **kwargs):
        super().__init__(master, **kwargs)

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


class ResizableApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Resizable Grid Example")

        # Treeview with 5 columns
        self.treeview = ttk.Treeview(root, columns=("Col1", "Col2", "Col3", "Col4", "Col5"), show="headings")
        for i, col in enumerate(("Col1", "Col2", "Col3", "Col4", "Col5"), start=1):
            self.treeview.heading(f"# {i}", text=col)
            self.treeview.column(f"# {i}", width=100, anchor="center")  # Initial column widths

        self.treeview.grid(row=0, column=0, columnspan=5, sticky="nsew")

        # Create bottom row frames with different colors
        self.style = ttk.Style()
        self.frames = []
        colors = ["red", "green", "blue", "yellow", "cyan"]
        for i, color in enumerate(colors):
            self.style.configure(style_name:=f"Custom{i}.TFrame", background=color)

            # change to MyFrame breaks resizing behavior
            frame = MyFrameResizable(root, height=10, style=style_name)

            frame.grid(row=1, column=i, sticky="nsew")
            self.frames.append(frame)

        # Configure grid weights for resizing
        root.grid_rowconfigure(0, weight=1)  # Treeview row resizes with window
        root.grid_rowconfigure(1, weight=1)  # Frame row stays fixed
        for i in range(5):
            root.grid_columnconfigure(i, weight=1)

        # Bind resizing events to update frame widths
        self.treeview.bind("<Configure>", self.update_frame_widths)
        self.treeview.bind("<<TreeviewColumnMoved>>", self.update_frame_widths)
        self.treeview.bind("<ButtonRelease-1>", self.update_frame_widths)

    def update_frame_widths(self, event=None):
        """Update the widths of frames to match Treeview column widths."""
        print("update")
        for i, frame in enumerate(self.frames):
            col_width = self.treeview.column(f"# {i + 1}", option="width")  # Get current column width
            frame.config(width=col_width)

# Run the app
if __name__ == "__main__":
    root = tk.Tk()
    app = ResizableApp(root)
    root.mainloop()

I expected the grid layout manager to assign the width to the frames, no matter the content. Once the frame width is determined, I thought the child widgets of the frame will then be gridded according to the available space.

It seems however, that including child widgets overrides the columnar width set via columnconfigure. Why is that?


Solution

  • For both pack and grid, they are designed to grow or shrink to fit the size of their children. That is the behavior you're seeing. This feature is called geometry propagation. You can turn this off by calling self.grid_propagate(False) in your MyFrame class.

    class MyFrame(ttk.Frame):
        def __init__(self, master = None, **kwargs):
            super().__init__(master, **kwargs)
    
            self.grid_propagate(False)
            ...