Search code examples
pythontkintertreeview

How can I dynamically change TreeView columns without TreeView width growing beyond column widths?


I'm trying to update a ttk.TreeView's columns, but the TreeView always expands beyond the column widths. The issue is shown in the screenshots below.

Here is a minimum reproducible example:

import tkinter as tk
from tkinter import ttk


class DataView(ttk.Labelframe):
    """Widget that displays tabular data in a console-like view."""

    def __init__(self, master, max_lines=1000, **kwargs):
        super().__init__(master, **kwargs)
        self.rowconfigure(0, weight=1)

        self._column_headers = ()
        self._max_lines = max_lines

        self._tv = ttk.Treeview(self, show="headings", selectmode="none", columns=[])
        self._tv.grid(row=0, column=0, padx=2, pady=2, sticky=tk.NS)

    def set_column_headers(self, headers: tuple[str, ...]):
        self._column_headers = headers
        self._tv.config(columns=headers)
        for header in headers:
            self._tv.column(header, width=70, stretch=False)
            self._tv.heading(header, text=header)


def main():
    root = tk.Tk()
    root.title("Data View Test")
    root.geometry("800x600")
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)

    data_view = DataView(root, text="Data View", max_lines=100)
    data_view.grid(row=0, column=0, sticky=tk.NSEW)

    data_view.set_column_headers(("Time", "Voltage", "Current"))
    data_view.after(3000, data_view.set_column_headers, ("Time", "Voltage", "Current", "Power"))

    root.mainloop()


if __name__ == "__main__":
    main()

The first call to set_column_headers works as expected: first set_column_headers call

However, the second call (and any subsequent calls) makes the treeview expand beyong the width of the columns:

second set_column_headers call

Stepping through the code, this seems to happen on self._tv.config(columns=headers)

I would expect a result like the first screenshot, but with the additional "Power" column. Is there any way I can achieve this? This seems like a tkinter bug but I may be doing something wrong.

Python version: 3.11.4

OS: Windows

tkinter.TkVersion: 8.6

tkinter.Tcl().call('info', 'patchlevel')): 8.6.12


Solution

  • When analyzing the execution of the program, I noticed that when setting self._tv.column(header, width=70, stretch=False), the type of the show option changes. This is what causes the “strange” behavior of the program.

        def set_column_headers(self, headers: tuple[str, ...]):
    
            self._column_headers = headers
            self._tv.config(columns=headers)
            print(self._tv["show"], type(self._tv["show"][0]))
    
            for i, header in enumerate(headers):
                self._tv.column(header, width=70, stretch=False)
                print(self._tv["show"], type(self._tv["show"][0]))
                self._tv.heading(header, text=header + str(i))
    
    ----------------------
    
    (<string object: 'headings'>,) <class '_tkinter.Tcl_Obj'>
    ('headings',) <class 'str'>
    ...........................
    

    In the source code on GitHub, the column method is implemented through the _val_or_dict function. Several other methods use this function and they also changed the type of the show option. That is, this is a flaw, at least ttk tkinter. The workaround is to assign self._tv["show"] = "headings" again, after applying the heading and column methods.