Search code examples
pythonpython-3.xtkinterttkttkwidgets

ttk.Treeview's width increases after hitting ttk.Radiobutton


I have been making this downloader app in python using tkinter and tkinter.ttk and added a recent feature in the app for the users to see a log of previous actions done by the program. It works fine, however, I recently discovered a bug.

I have a settings button which creates another Toplevel and then you can manage the default directories and change the switch the mode of the app to light or night mode or a custom theme using three ttk.Radiobuttons but whenever I hit the first two radiobuttons (the first two are responsible for switching the theme from light mode to dark mode and vise versa) my ttk.treeview's width gets added by one for no reason. the strange part is that it does not happen to the third radiobutton, responsible for making a custom mode.

I tried setting a maxsize for the log's toplevel but it is affecting the ttk.treeview itself. Why is this happening? How can I prevent this?

A simulation of my app (this is just a demonstration so I didn't write the entire theme code):

from tkinter import *
from tkinter import colorchooser
from tkinter import ttk

columns = ("Operation", "URL", "File Path", "Status", "Start Date", "End Date")
log_data = [("Download (File)", "https://www.youtube.com", "C:/Users/Mypc/Downloads/youtube.html", "Finished", "2021-03-30 13:15:30", "2021-03-30 13:15:33"),
            ("Format Fetch", "https://www.youtube.com/watch?v=xxNxqveseyI", "------", "Finished", "2021-03-30 13:15:30", "2021-03-30 13:15:33")]
font_color, bg_color = "black", "white"
root = Tk()
root.configure(bg=bg_color)
root.resizable(False, False)
night_on = IntVar(value=1)
style = ttk.Style()
style.configure("Treeview", rowheight=25, font=('Arial', 10))
style.configure("Treeview.Heading", font=('Arial', 10))
style.configure("TLabel", foreground=font_color, background=bg_color)
style.configure('my.TButton', font=('Helvetica', 20, 'italic'))
style.configure("TRadiobutton", foreground=font_color, background=bg_color, font=('Arial', 10))
font_color_var = StringVar(value=f"Current font color:  \t  {font_color}")
bg_color_var = StringVar(value=f"Current background color:  \t  {bg_color}")
log_top = Toplevel(root)
log_top.withdraw()
log_top.resizable(False, False)
log_fr = Frame(log_top)
log_scroll = ttk.Scrollbar(log_fr, orient=VERTICAL)
log_tree = ttk.Treeview(log_fr, selectmode="browse", yscrollcommand=log_scroll.set, height=12, columns=columns)
log_scroll.config(command=log_tree.yview)


def clear_records():
    for child in log_tree.get_children():
        log_tree.delete(child)


clr_log_btn = ttk.Button(log_top, text="Clear Log", takefocus=False, style="my.TButton", command=clear_records)
log_tree.column("#0", width=0, stretch=NO)
log_tree.column("Operation", width=100, anchor=CENTER)
log_tree.column("URL", width=100, anchor=CENTER)
log_tree.column("File Path", width=100, anchor=CENTER)
log_tree.column("Status", width=80, anchor=CENTER)
log_tree.column("Start Date", width=126, anchor=CENTER)
log_tree.column("End Date", width=126, anchor=CENTER)

for head in columns:
    log_tree.heading(head, text=head, anchor=CENTER)

for item_indices, element in enumerate(log_data):
    log_tree.insert(parent='', index=0, iid=item_indices, values=element)


log_tree.pack(side=LEFT)
log_scroll.pack(side=RIGHT, fill=Y)
clr_log_btn.pack(side=BOTTOM, fill=X)
log_fr.pack()
log_top.protocol("WM_DELETE_WINDOW", log_top.withdraw)
log_lbl = Label(root, text="Show Log", fg="blue", bg=bg_color, cursor="hand2")


def show_log(event):
    if log_top.state() == "withdrawn":
        log_top.deiconify()
    elif log_top.state() == "normal":
        log_top.focus_set()


def settings_win():
    settings_top = Toplevel(root, bg=bg_color)
    settings_top.resizable(False, False)

    def set_night():
        global bg_color, font_color
        bg_color = "black"
        font_color = "white"
        settings_top.config(bg=bg_color)
        style.configure("TLabel", foreground=font_color, background=bg_color)
        style.configure("TRadiobutton", foreground=font_color, background=bg_color)

    def set_light():
        global bg_color, font_color
        bg_color = "white"
        font_color = "black"
        settings_top.config(bg=bg_color)
        style.configure("TLabel", foreground=font_color, background=bg_color)
        style.configure("TRadiobutton", foreground=font_color, background=bg_color)

    def set_custom():
        global font_color, bg_color
        color_fr = Toplevel(root, bg=bg_color)
        color_fr.resizable(False, False)
        current_font_color = ttk.Label(color_fr, textvariable=font_color_var)
        current_bg_color = ttk.Label(color_fr, textvariable=bg_color_var)

        def change_color(name):
            global font_color, bg_color
            new_color = colorchooser.askcolor()[1]
            if new_color is not None:
                if name == "font":
                    font_color = new_color
                    font_color_var.set(f"Current font color:  \t  {font_color}")
                    style.configure("TLabel", foreground=font_color)
                    style.configure("TRadiobutton", foreground=font_color)
                elif name == "bg":
                    bg_color = new_color
                    bg_color_var.set(f"Current background color:  \t  {bg_color}")
                    color_fr.config(bg=bg_color)
                    style.configure("TLabel", background=bg_color)
                    style.configure("TRadiobutton", background=bg_color)

        change_font = ttk.Button(color_fr, text="Change! ", command=lambda: change_color("font"),
                                 takefocus=False)
        change_bg = ttk.Button(color_fr, text="Change! ", command=lambda: change_color("bg"), takefocus=False)
        current_font_color.grid(row=0, column=0, pady=(10, 0), padx=5)
        change_font.grid(row=0, column=1, padx=(0, 7))
        current_bg_color.grid(row=1, column=0, pady=(10, 0), padx=5)
        change_bg.grid(row=1, column=1, padx=(0, 7))

    night_mode = ttk.Radiobutton(settings_top, text="Night Mode", variable=night_on, value=0,
                                 command=set_night, takefocus=False)
    light_mode = ttk.Radiobutton(settings_top, text="Light Mode", variable=night_on, value=1,
                                 command=set_light, takefocus=False)
    custom_mode = ttk.Radiobutton(settings_top, text="Custom Mode", variable=night_on, value=2,
                                  command=set_custom, takefocus=False)

    night_mode.grid(row=0, column=0)
    light_mode.grid(row=1, column=0)
    custom_mode.grid(row=2, column=0)


log_lbl.bind("<Button-1>", show_log)
log_lbl.pack(side=LEFT, pady=30, padx=30)
settings_btn = ttk.Button(root, text="Open Settings", takefocus=False, command=settings_win)
settings_btn.pack(side=RIGHT, padx=30, pady=30)
root.focus_set()
root.mainloop()

Solution

  • I think I worked out a way to solve your problem. First, I minified your code to make it easier to debug:

    import tkinter as tk
    from tkinter import ttk
    
    columns = ("1", "2", "3")
    font_color, bg_color = "black", "white"
    
    def change_theme():
        global bg_color, font_color
        # Switches the values of `bg_color` and `font_color`
        bg_color, font_color = font_color, bg_color
        root.config(bg=bg_color)
        style.configure("TLabel", fg=font_color, bg=bg_color)
    
    
    root = tk.Tk()
    
    style = ttk.Style()
    
    tree = ttk.Treeview(root, columns=columns)
    tree.pack()
    
    button = tk.Button(root, text="Click me", command=change_theme)
    button.pack()
    
    root.mainloop()
    

    After looking at your code again I noticed that you had stretch=NO which is the same as stretch=False for the #0 heading. I desided to apply it to all of the heading like this:

    tree.column("#0", stretch=False)
    for column_name in columns:
        tree.column(column_name, stretch=False)
    

    And it solved your problem. After looking at this documentation for ttk.TreeView I noticed what it said about the stretch parameter: "If this option is True, the column's width will be adjusted when the widget is resized. The default setting is 1." From that I concluded that for some reason style.configure("TLabel", ...) changes the width of the treeview triggering all of the columns to resize.

    I don't know enough about TreeView and Styles to tell you why this happens but if someone knows they can edit my answer.