Search code examples
pythontkinterbuttontreeview

Treeview columns do not stretch initially if inserted on button command, but do on manual function call


I have a treeview in a frame. The tree is managed by grid geometry manager. On button press I want to:

  1. Change columnspan of tree
  2. Change data of tree (including its number of columns)

The tree should stick to each end of its grid and the columns should stretch to both sides of the tree. I attempt to achieve this by remapping tree with new columnspan, deleting children of tree, and adding the new columns. See toggle_span().

import tkinter as tk
from tkinter import ttk

def setup_tree():
    columns = ["col1"]
    tree['columns'] = columns

    tree.column('#0', width=20, minwidth=20, stretch=tk.YES)
    tree.heading('#0', text='id')
    for i, column in enumerate(columns):
        tree.column(i, width=20, minwidth=20, stretch=tk.YES)
        tree.heading(i, text=column)

    tree.insert('', 'end', iid='1', text='1', values=("item1",))
    tree.insert('', 'end', iid='2', text='2', values=("item2",))

# Toggle treeview
def toggle_span():
    global span

    # Extend tree
    if span == 1:
        span = 2
        columns = ["col1", "col2"]
        row1 = ("item1", "item2")
        row2 = ("item3", "item4")

    # Retract tree
    else:
        span = 1
        columns = ["col1"]
        row1 = ("item1",)
        row2 = ("item2",)

    # Refresh columnspan of old tree
    tree.grid_forget()
    tree.grid(column=1, row=0, sticky="nsew", columnspan=span)

    # Clear tree
    for item in tree.get_children():
        tree.delete(item)

    # Rebuild tree
    tree['columns'] = columns

    tree.column('#0', width=20, minwidth=20, stretch=tk.YES)
    tree.heading('#0', text='id')
    for i, column in enumerate(columns):
        tree.column(i, width=20, minwidth=20, stretch=tk.YES)
        tree.heading(i, text=column)

    tree.insert('', 'end', iid='1', text='1', values=row1)
    tree.insert('', 'end', iid='2', text='2', values=row2)

span = 1

# Root
root = tk.Tk()
root.state("zoomed")

# Frame
frame = tk.Frame(root, bg="cyan")
frame.pack(fill='both', expand=True)

frame.grid_rowconfigure(0, weight=1)
for i in range(3):
    frame.grid_columnconfigure(i, weight=1) 

# Button
button = tk.Button(frame, text="Toggle Span", command=toggle_span)
button.grid(column=0, row=0, sticky="nsew")

# Tree
tree = ttk.Treeview(frame)
setup_tree()
tree.grid(column=1, row=0, sticky="nsew", columnspan=span)

# Manual toggle works as expected!
toggle_span()
toggle_span()

root.mainloop()

When toggle_span() is called manually, columns are stretched as expected. Howewer when toggle_span() is called by button press, only the initial retracted state and the extended state immediately following that is shown correctly. After that, the columns do not stretch initially. They do stretch though, when the edge of a column is clicked. Column states

How do I make sure the columns are always stretched?


Solution

  • Specific solution:

    Call grid_forget after modifying the first treeview column (but before the rest).

    ...
    
    tree.column('#0', width=20, minwidth=20, stretch=tk.YES)
    tree.heading('#0', text='id')
    
    tree.grid_forget()
    tree.grid(column=1, row=0, sticky="nsew", columnspan=span)  
    
    for i, column in enumerate(columns):
        tree.column(i, width=20, minwidth=20, stretch=tk.YES)
        tree.heading(i, text=column)
    
    ...
    

    After this the columns will stretch as expected. I don't know why it works this way.

    Its hardly visible with this few columns, but the button and the treeview are not always the same width, they wobble a bit. This can be solved by configuring the frame columns to be uniform.

    frame.grid_rowconfigure(0, weight=1)
    for i in range(3):
        frame.grid_columnconfigure(i, weight=1, uniform="colgroup") 
    

    More general solution:

    Create a distinct frame for the treeview to fit in. The tree can be resized by resizing this frame. With this, the columns stretch as expected.

    The inner frame's columnspan can be configured directly with grid_configure(columnspan=...) instead of grid_forget() and grid().

    However, now the inner frame changes based on the treeview, causing even wilder "wobbling". To stop this, we can call grid_propagate(False) on the inner frame.

    The code for the question's example:

    import tkinter as tk
    from tkinter import ttk
    
    def setup_tree():
        columns = ["col1"]
        tree['columns'] = columns
    
        tree.column('#0', width=20, minwidth=20, stretch=tk.YES)
        tree.heading('#0', text='id')
        for i, column in enumerate(columns):
            tree.column(i, width=20, minwidth=20, stretch=tk.YES)
            tree.heading(i, text=column)
    
        tree.insert('', 'end', iid='1', text='1', values=("item1",))
        tree.insert('', 'end', iid='2', text='2', values=("item2",))
    
    # Toggle treeview
    def toggle_span():
        global span
    
        # Extend tree
        if span == 1:
            span = 2
            columns = ["col1", "col2"]
            row1 = ("item1", "item2")
            row2 = ("item3", "item4")
    
        # Retract tree
        else:
            span = 1
            columns = ["col1"]
            row1 = ("item1",)
            row2 = ("item2",)
        
        # Clear tree
        for item in tree.get_children():
            tree.delete(item)
    
        # Rebuild tree
        tree['columns'] = columns
    
        tree.column('#0', width=20, minwidth=20, stretch=tk.YES)
        tree.heading('#0', text='id')
        for i, column in enumerate(columns):
            tree.column(i, width=20, minwidth=20, stretch=tk.YES)
            tree.heading(i, text=column)
    
        tree.insert('', 'end', iid='1', text='1', values=row1)
        tree.insert('', 'end', iid='2', text='2', values=row2)
    
        tree_frame.grid_configure(columnspan=span)
    
    span = 1
    
    # Root
    root = tk.Tk()
    root.state("zoomed")
    
    # Frame
    frame = tk.Frame(root, bg="cyan")
    frame.pack(fill='both', expand=True)
    
    frame.grid_rowconfigure(0, weight=1)
    for i in range(3):
        frame.grid_columnconfigure(i, weight=1) 
    
    # Tree frame
    tree_frame = tk.Frame(frame)
    tree_frame.grid(row=0, column=1, sticky="nsew", columnspan=span)
    tree_frame.grid_propagate(False)
    
    tree_frame.grid_rowconfigure(0, weight=1)
    tree_frame.grid_columnconfigure(0, weight=1)
    
    # Button
    button = tk.Button(frame, text="Toggle Span", command=toggle_span)
    button.grid(column=0, row=0, sticky="nsew")
    
    # Tree
    tree = ttk.Treeview(tree_frame)
    setup_tree()
    tree.grid(row=0, column=0, sticky="nsew")
    
    root.mainloop()
    

    About "manual" function call:

    I don't quite understand what is happening, but here is what I see.

    The columns stretch nicely because the treeview is toggled before the mainloop. They still shrink during the mainloop, even if the toggle is not called by button press:

    # Produces unexpected state
    def toggle():
        print("toggling")
        toggle_span()
        root.after(2000, toggle)
    root.after(2000, toggle)
    
    root.mainloop()