Search code examples
pythontkinterttkbootstrap

tkinter theme doesn't work/breaks when a new window is formed


I am working on a small tkinter(themes from ttkbootstrap) application which first opens a login window which then calls the main dashboard window. The problem is the theme used does not work or just breaks in the dashboard when called from login window but works completely fine if i directly open dashboard.

Code:

main.py

from test_login import Admin_Login
import ttkbootstrap as ttb

if __name__ == "__main__":
    root = Admin_Login()
    style = ttb.Style("darkly")
    root.mainloop()

test_login.py (trimmed enough to recreate the problem)

import tkinter as tk
import ttkbootstrap as ttb
from dashboard import Dashboard

class Admin_Login(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__()
        self.geometry('400x460')
        self.resizable(False, False)
        
        self.login_button = ttb.Button(self, text = "Login", width=8,
                                       command = self.call_dashboard)
        self.login_button.pack()
        
    def call_dashboard(self):
        self.withdraw()
        Dashboard()

dashboard.py (trimmed enough recreate the problem)

from tkinter import Menu, Tk, Canvas
from tkinter.ttk import Frame, Label
import tkinter.ttk as ttk
import ttkbootstrap as ttb
from utils.toggle_menu import Toggle_Menu

class Dashboard(Tk):
    def __init__(self, db=None):
        super().__init__()
        #configure Tk window
        self.title("My App")
        self.geometry('1920x1080')
        self.state('zoomed')
        
        #create masterframe that contains two columns
        style = ttb.Style('darkly')
        style.configure('custom1.TFrame', background='white')
        self.mainframe = Frame(self, style='custom1.TFrame')
        self.mainframe.rowconfigure(0, weight=1)
        self.mainframe.columnconfigure(0, minsize=200)
        self.mainframe.columnconfigure(1, weight=1)
        self.mainframe.pack(fill='both', expand=True)
        self.create_dashboard()    
        
    def create_dashboard(self):
        #menu for dashboard
        self.menubar = Menu(self)
        self.file = Menu(self.menubar, background='white', activebackground='white')
        self.file.add_command(label ='New File\t\t\t', command = None, background='white', foreground='black') 
        self.file.add_separator(background='white') 
        self.file.add_command(label ='Exit', background='white', foreground='black', command = self.destroy)
        self.menubar.add_cascade(label='File', menu=self.file)
        
        #create left frame
        self.dashboard_options = Frame(self.mainframe, width=300, borderwidth=5)
        self.dashboard_title = Label(self.dashboard_options, text='Dashboard')
        self.dashboard = Canvas(self.mainframe)
        toggle1 = Toggle_Menu(self.dashboard_options)
        toggle1.add_option("child1")
        toggle1.add_option("child2")
        
        #pack widgets
        self.dashboard_title.pack(anchor='nw', fill='x', expand=False, padx=4)
        toggle1.pack(anchor='nw', fill='x', expand=True)
        self.dashboard_options.grid(row = 0, column=0, sticky='nsew', padx=(0,3))
        self.dashboard.grid(row = 0, column=1, sticky='nsew')
        
        #set menu
        self.config(menu = self.menubar)
        
if __name__ == "__main__":
    root = Dashboard()
    style = ttb.Style("darkly")
    root.mainloop()

toggle_menu.py (trimmed enough to recreate the problem)

import tkinter as tk
import tkinter.ttk as ttk
import ttkbootstrap as ttb
from PIL import Image, ImageTk
    
class Toggle_Menu(ttk.Frame):
    def __init__(self, parent, *args, **kwargs):
        ttk.Frame.__init__(self, parent, *args, **kwargs)
        #frames
        self.parent_frame = ttk.Frame(self, relief='flat')
        self.child_frame = tk.Canvas(self, relief='flat')
        
        #arrow label and parent name
        self.arrow_label = ttk.Label(self.parent_frame, image=None)
        self.parent_name = ttk.Label(self.parent_frame, text='parent')
        
        #bind all of them with left mouse click button
        self.parent_frame.bind('<Button-1>', self.toggle)
        self.arrow_label.bind('<Button-1>', self.toggle)
        self.parent_name.bind('<Button-1>', self.toggle)
        
        #widget packs
        self.arrow_label.pack(side='left', anchor='w', padx=(3,0), pady=10)
        self.parent_name.pack(side='left', anchor='w', padx=10, pady=10)
        self.parent_frame.pack(fill='x', expand=True, anchor='ne')
        self.child_frame.pack(fill='x', expand=True, anchor='ne')
        

    def toggle(self, event):
        '''
        Toggles the parent frame on and off by unpacking/packing child frame
        '''
        if self.child_frame in self.pack_slaves():
            self.child_frame.pack_forget()
        else:
            self.child_frame.pack(fill='x', expand=True, anchor='ne')
            
    def add_option(self, button_name, command=None):
        '''
        Add new options to child frame
        '''
        style = ttb.Style('darkly')
        style.configure('info.Link.TButton', justify='left', foreground = 'white')
        button = ttk.Button(self.child_frame, text="hi", style="info.Link.TButton", command=None)
        button.pack(anchor='ne', fill='x', expand=True)

Screenshots:

Running main.py results in: broken

Running dashboard.py results in: expected

Directly running dashboard.py works but calling it from admin login seems to break the theme. If possible, please explain why this happens.


Solution

  • The problem:

    The problem is that you create two instances of tk.Tk, Admin_Login and Dashboard. These are two root windows. There should be only one root window. When you run the dashboard directly, it works because only one root window is created.

    Tkinter has a thing called default root. The first Tk instance created becomes the default root for all objects (widgets/toplevel windows/styles/control variables) without an explicit parent window.

    ttb.Style is a subclass of ttk.Style. It is allowed to create objects of this class without passing the parent window.

    import tkinter as tk
    import tkinter.ttk as ttk
    
    
    root = tk.Tk()
    root1 = tk.Tk()
    
    style = ttk.Style()
    # or
    style1 = ttk.Style(root)
    
    print(tk._default_root is root) # True
    print(style.master is root) # True
    print(style1.master is root) # True
    

    ttb.Style is also a singleton class, which means you don't create a new instance in the Dashboard class, but get the same object that was created in main.py.

    When you first create a ttb.Style object, it is associated with the first root window (Admin_Login). This is why your theme doesn't work for dashboard window children. And the theme works when you run the dashboard directly because the style is first created here and associated with the Dashboard(Tk).

    You usually only need one root window and one style object for that window. If you need several additional windows, you can use the Toplevel widget. The Toplevel window and all its children will have the same style as the root window. If you create the style as an attribute of the Admin_Login class (self.style), then it will be available to root's children (self.master.style). In the case of the Toggle_Menu class, this will be self.master.master.master.master.style.

    Proposed changes:

    test_login.py

    class Admin_Login(tk.Tk):
        def __init__(self, *args, **kwargs):
            super().__init__()
            self.geometry('400x460')
            self.resizable(False, False)
            self.style = ttb.Style('darkly') # create a style only once
            print("Admin_Login:", id(self), id(self.style))
            ...
    
        def call_dashboard(self):
            self.withdraw()
            Dashboard(self) # explicitly pass the parent root
    
    

    dashboard.py

    from tkinter import Toplevel
    
    
    class Dashboard(Toplevel):
        def __init__(self, master, db=None):
            super().__init__(master)
            print("Dashboard:", id(self.master), id(self.master.style))
            ...
    

    Output:

    Admin_Login: 2170364988816 2170367009552
    Dashboard: 2170364988816 2170367009552