israel-dryer / ttkbootstrap

A supercharged theme extension for tkinter that enables on-demand modern flat style themes inspired by Bootstrap.
MIT License
1.92k stars 386 forks source link

Major error in ttkboostrap #285

Closed sunny9495-dev closed 2 years ago

sunny9495-dev commented 2 years ago

Desktop (please complete the following information):

Hi,

I use the following code to spawn the next UI, but iam getting error _tkinter.TclError: NULL main window

from tkinter import *
from ttkbootstrap import Style
from tkinter import ttk
import multiprocessing

class GuiApp(object):
    def __init__(self):
        self.style = Style(theme='litera')
        self.style.configure('lefttab.TNotebook', tabposition='wn')
        self.style.configure('TNotebook.Tab', align=LEFT)
        self.root = self.style.master
        tabone = ttk.Button(self.root, text="Campaigns")
        tabone.place(x=20, y=20)
        tabtwo = ttk.Button(self.root, text="Settings")
        tabtwo.place(x=27, y=60)
        self.root.geometry("850x550")
        self.root.resizable(False, False)
def validate():
    second.destroy()
    multiprocessing.freeze_support()
    gui = GuiApp()
    gui.root.mainloop()
def validate_lic():
   global second
   global emailentry
   global passentry
   global keyentry
   global validate_btn
   style = Style(theme='litera')
   second = style.master
   validate_btn = ttk.Button(second, text="Sign-in", command=validate)
   validate_btn.place(x=155, y=150)
   second.geometry("350x300")
   second.resizable(False, False)
   # second.iconbitmap("Icon.ico")
   second.mainloop()

if __name__ == '__main__':
    multiprocessing.freeze_support()
    validate_lic()

If i tried with same code with ttkboostrap version 0.5.1 then no issue.

Describe the bug

Getting _tkinter.TclError: NULL main window bug when it was trying to spawn other UI

To Reproduce

Just run the code and click on sign in button, you can see the bug

Expected behavior

It has to spawn the other UI, it works fine V0.5.1, but not in the current.

Screenshots

https://i.imgur.com/8zaETCY.png

Additional context

No response

israel-dryer commented 2 years ago

@sunny9495-dev, what is your objective? Are you trying to create one window after destroying another, or to create a brand new window? Or to create two running instances of a window?

israel-dryer commented 2 years ago

ttkbootstrap 1.0+ works a bit differently under the hood than 0.5.

Try something like this:

import ttkbootstrap as ttk

class GuiApp:

    def __init__(self):
        self.root = ttk.Window(themename='litera', size=(850, 550), resizable=(False, False))
        tab_one = ttk.Button(self.root, text='Campaigns')
        tab_one.place(x=20, y=20)
        tab_two = ttk.Button(self.root, text='Settings')
        tab_two.place(x=27, y=60)

def validate(window):
    window.destroy()
    app = GuiApp()
    app.root.mainloop()

def validate_license():
    login = ttk.Window(themename='litera', size=(350, 300), resizable=(False,False))
    validate_btn = ttk.Button(login, text='Sign-in', command=lambda w=login: validate(w))
    validate_btn.place(x=155, y=150)
    login.mainloop()

if __name__ == '__main__':
    validate_license()
python_Ge5AsqJ6k0 python_BWSONobwVR
sunny9495-dev commented 2 years ago

Thanks alot for quick response @israel-dryer,

but, i still having issue mate

self.root = ttk.Window(themename='litera', size=(850, 550), resizable=(False, False))
self.root.layout('TNotebook.Tab', [])

iam using the code to hide the tab headers, but iam getting

AttributeError: '_tkinter.tkapp' object has no attribute 'layout'

Any solution for the issue

israel-dryer commented 2 years ago

Try, self.root.style.layout()

The window class has a style object attached to it.

Here's a reference to the new API https://ttkbootstrap.readthedocs.io/en/latest/gettingstarted/tutorial/

sunny9495-dev commented 2 years ago

One more issue @israel-dryer,

every style is giving issue. iam using style='light.TButton'

but iam getting error

File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 4929, in __init__
    func(self, *args, **kwargs)
  File "C:\Python\Lib\tkinter\ttk.py", line 702, in __init__
    Entry.__init__(self, master, "ttk::combobox", **kw)
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 4948, in __init__
    ttkstyle = Bootstyle.update_ttk_widget_style(
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 5034, in update_ttk_widget_style
    builder_method(builder, widget_color)
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 1203, in create_combobox_style
    arrowsize=self.scale_size(12),
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 1104, in scale_size
    winsys = self.style.master.tk.call("tk", "windowingsystem")
_tkinter.TclError: can't invoke "tk" command: application has been destroyed
bgerror failed to handle background error.
    Original error: can't invoke "event" command: application has been destroyed
    Error in bgerror: can't invoke "tk" command: application has been destroyed
israel-dryer commented 2 years ago

You are setting a button style?

Try using the argument bootstyle="light"

https://ttkbootstrap.readthedocs.io/en/latest/styleguide/

sunny9495-dev commented 2 years ago

Sorry to say @israel-dryer, that isnt worked too

File "D:\Python Projects\Activity New\Activity.py", line 128, in __init__
    self.browserbtn = ttk.Checkbutton(self.tab2, text="On", bootstyle="success-round-toggle",
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 4943, in __init__
    ttkstyle = Bootstyle.update_ttk_widget_style(
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 5034, in update_ttk_widget_style
    builder_method(builder, widget_color)
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 3115, in create_round_toggle_style
    images = self.create_round_toggle_assets(colorname)
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 3010, in create_round_toggle_assets
    size = self.scale_size([24, 15])
  File "C:\Python\lib\site-packages\ttkbootstrap\style.py", line 1104, in scale_size
    winsys = self.style.master.tk.call("tk", "windowingsystem")
_tkinter.TclError: can't invoke "tk" command: application has been destroyed
bgerror failed to handle background error.
    Original error: can't invoke "event" command: application has been destroyed
    Error in bgerror: can't invoke "tk" command: application has been destroyed
israel-dryer commented 2 years ago

When are you getting this error? When starting the program, or performing some action?

Can you please post an example of the code in question.

sunny9495-dev commented 2 years ago

Here is the code @israel-dryer

import ttkbootstrap as ttk
from tkinter import *

class GuiApp:
    def __init__(self):
        self.root = ttk.Window(themename='litera', size=(850, 550), resizable=(False, False))
        self.root.style.configure('lefttab.TNotebook', tabposition='wn')
        self.root.style.configure('TNotebook.Tab', align=LEFT)
        self.root.style.layout('TNotebook.Tab', [])
        self.notebook = ttk.Notebook(self.root, style='lefttab.TNotebook')
        self.tab1 = ttk.Frame(self.notebook, width=700, height=500)
        self.tab2 = ttk.Frame(self.notebook, width=700, height=500)
        self.tab3 = ttk.Frame(self.notebook, width=700, height=500)
        self.tab4 = ttk.Frame(self.notebook, width=700, height=500)
        self.tab5 = ttk.Frame(self.notebook, width=700, height=500)
        self.tab6 = ttk.Frame(self.notebook, width=700, height=500)
        self.notebook.add(self.tab1, text="Campaigns")
        self.notebook.add(self.tab2, text="Settings")
        self.notebook.add(self.tab3, text="Reports")
        self.notebook.add(self.tab4, text="Add New Campaign")
        self.notebook.add(self.tab5, text="Camp Reports")
        self.notebook.add(self.tab6, text="Log Report")
        # self.notebook.pack(fill=BOTH, expand=1)
        self.notebook.place(x=100, y=20)
        tabone = ttk.Button(self.root, text="Campaigns")
        tabone.place(x=20, y=20)
        tabtwo = ttk.Button(self.root, text="Settings")
        tabtwo.place(x=27, y=60)
        tabthree = ttk.Button(self.root, text="Reports")
        tabthree.place(x=27, y=100)
        # Tab1
        self.camplbl = ttk.Label(self.tab1, text="Campaigns", bootstyle='info', font='Helvetica 14 bold')
        self.camplbl.place(x=10, y=20)
        self.addcamp = ttk.Button(self.tab1, text="Add New Campaign", bootstyle='primary')
        self.addcamp.place(x=10, y=60)
        self.label = ttk.Label(self.tab1, text="Search", bootstyle='info', font=(('Arial'), 11))
        self.label.place(x=10, y=110)
        self.searchentry = ttk.Entry(self.tab1, width=30)
        self.searchentry.place(x=65, y=105)

    def run_camp(self):
        pass

def validate(window):
    window.destroy()
    app = GuiApp()
    app.root.mainloop()

def validate_license():
    login = ttk.Window(themename='litera', size=(350, 300), resizable=(False,False))
    validate_btn = ttk.Button(login, text='Sign-in', command=lambda w=login: validate(w))
    validate_btn.place(x=155, y=150)
    login.mainloop()

if __name__ == '__main__':
    validate_license()

One more thing is the tabs headers isnt hiding and the second window doesnt appear in bootstrap style

israel-dryer commented 2 years ago

I need to look into this further, but generally, it is not a recommended practice to use more than one tcl/tk interpreter in your application. This is what Toplevel widgets are created for. There are several ways to accomplish what you want without closing and reopening a new application window.

On solution, as I mentioned, is to use Toplevel widgets. Another solution (demonstrated below) is to use frames to control and contain your content.

import ttkbootstrap as ttk
from ttkbootstrap.constants import *

THEME = 'litera'
APP_SIZE = (850, 550)

class GuiApp(ttk.Window):

    def __init__(self):
        super().__init__(themename=THEME, size=APP_SIZE, resizable=(False, False))
        self.is_valid_license = ttk.BooleanVar(self, name='is_valid_license', value=False)
        self.is_valid_license.trace_add('write', self.on_license_validation)
        # setup frames
        self.setup_custom_styles()
        self.validation_frame = LicenseValidationFrame(self)
        self.main_frame = MainAppFrame(self)
        # show the initial validation frame
        self.show_validation_frame()

    def show_main_frame(self):
        self.main_frame.pack(fill=BOTH, expand=YES)

    def show_validation_frame(self):
        if not self.validation_frame.winfo_ismapped():
            self.validation_frame.pack(fill=BOTH, expand=YES)

    def setup_custom_styles(self):
        self.style.configure('lefttab.TNotebook', tabposition='wn')
        self.style.configure('TNotebook.Tab', align=LEFT, fill=X)
        self.style.layout('TNotebook.Tab', [])

    def on_license_validation(self, *args):
        if self.is_valid_license.get():
            self.validation_frame.pack_forget()
            self.show_main_frame()

class MainAppFrame(ttk.Frame):

    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        # setup buttons
        self.lframe = ttk.Frame(self, padding=(10, 10, 0, 10))
        self.lframe.pack(side=LEFT, fill=Y)
        self.tab1_btn = ttk.Button(self.lframe, text='Campaigns')
        self.tab2_btn = ttk.Button(self.lframe, text='Settings')
        self.tab3_btn = ttk.Button(self.lframe, text='Reports')
        self.tab1_btn.pack(side=TOP, fill=X)
        self.tab2_btn.pack(side=TOP, fill=X)
        self.tab3_btn.pack(side=TOP, fill=X)

        # setup notebook & tabs
        self.rframe = ttk.Frame(self)
        self.rframe.pack(side=RIGHT, fill=BOTH)
        self.notebook = ttk.Notebook(self.rframe, style='lefttab.TNotebook')
        self.notebook.pack(fill=BOTH, expand=YES, padx=10, pady=10)

        tab_cnf = {'width': 700, 'height': 500}
        # tab 1 setup
        self.tab1 = CampaignTab(self.notebook)
        self.tab2 = ttk.Frame(self.notebook, **tab_cnf)
        self.tab3 = ttk.Frame(self.notebook, **tab_cnf)
        self.tab4 = ttk.Frame(self.notebook, **tab_cnf)
        self.tab5 = ttk.Frame(self.notebook, **tab_cnf)
        self.tab6 = ttk.Frame(self.notebook, **tab_cnf)
        self.notebook.add(self.tab1, text='Campaigns')
        self.notebook.add(self.tab2, text='Settings')
        self.notebook.add(self.tab3, text='Reports')
        self.notebook.add(self.tab4, text='Add New Campaign')
        self.notebook.add(self.tab5, text='Camp Reports')
        self.notebook.add(self.tab6, text='Log Reports')

class CampaignTab(ttk.Frame):
    def __init__(self, master, **kwargs):
        super().__init__(master, width=700, height=500, **kwargs)
        # create tab elements
        self.camp_lbl = ttk.Label(self, text='Campaigns', bootstyle=INFO, font='Helvetica 14 bold')
        self.add_camp = ttk.Button(self, text='Add New Campaign', bootstyle=PRIMARY)
        self.search_lbl = ttk.Label(self, text='Search', bootstyle=INFO, font='Arial 11')
        self.search_entry = ttk.Entry(self)
        # set layout
        self.camp_lbl.pack(pady=10)
        self.add_camp.pack(pady=10)
        self.search_entry.pack(pady=10)

class LicenseValidationFrame(ttk.Frame):

    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        btn = ttk.Button(self, text='Sign-in', command=self.validate)
        btn.place(anchor=CENTER, relx=0.5, rely=0.5)

    def validate(self):
        """perform some function to validate the license then update
        the is_valid_license variable"""
        is_valid_license = ttk.BooleanVar(name='is_valid_license')
        is_valid_license.set(True)

if __name__ == '__main__':
    app = GuiApp()
    app.mainloop()

In fact, since you are creating your own notebook buttons, you really do not need to use the notebook widget at all, just use the pack_forget method to remove the frame from the window as I did for the license frame above, and then pack the tab you want to show. The result will be the same except you won't need to do a lot of customizations on the notebook widget.

One other tip, I would avoid using place with x & y coordinates. If someone has a high resolution screen, the placement of widgets will be very different than you expect. If you need to use the place geometry manager, prefer to use relx or rely to make relative position placements.

python_wu3zt5czNH python_5bbkg3bhhV

here's with just the tabs

python_n1WsOmIs86
sunny9495-dev commented 2 years ago

Ok, Thanks @israel-dryer

Thank you for your excellent support

EmberLightVFX commented 1 year ago

I just want to add a note on how to solve the application has been destroyed error. My script is a module and the end user might use it ether with tk or pyside or what ever so I needed the functionality to create new tk instances.

The solution was to set Style.instance = None before setting the theme. That way the Style command will create a new instance. This is how my code looks like:

window = tk.Tk()
Style.instance = None
Style(theme="superhero")

@israel-dryer Maybe there could be an argument for Style() to create a new instance even if an old one exist for these cases?