0

I was trying to design an application that gets the user input for connecting to the data base. After connecting to the database the application then gets the table that user has created based on some parameters. This has been implemented in an OO approach. I instantiated two object of this instance. Now here is where the problem arises. When the first window gets destroyed the second window opens but it doesn't have any of the style parameters used in rendering the window that I have passed for instantiating the class in the init part of the class. I have been trying to debug it for some time now. But havent found the root cause till now.

Here is the code :-

import ttkbootstrap as ttkb
from ttkbootstrap.constants import *
from ttkbootstrap.tableview import Tableview
import pyodbc
from typing import *
import pandas as pd
import inspect
import threading


def log_event(event):
    print(f"Event: {event.widget} {event.type}")


def print_args(*args, **kwargs):
    """
    Print the list of arguments and their types passed to a function.
    """
    sig = inspect.signature(print_args)  # Get the signature of the function.
    bound_args = sig.bind(*args, **kwargs)  # Bind the arguments to the signature.

    # Print the argument name, value, and type.
    for name, value in bound_args.arguments.items():
        print(f"{name}: {value} ({type(value).__name__})")


def create_form_entry(master, widget: str, label: str, variable, fun=None):
    """Create a single form entry using a frame for each label"""
    container = ttkb.Frame(master)
    container.pack(fill=X, expand=YES, pady=5)

    lbl = ttkb.Label(master=container, text=label.title(), width=10)
    lbl.pack(side=LEFT, padx=5, ipadx=10)

    if widget == "entry" or widget == "Entry":
        ent = ttkb.Entry(master=container, textvariable=variable)
        ent.pack(side=LEFT, padx=5, fill=X, expand=YES)
    elif widget == "combobox" or widget == "Combobox":
        ent = ttkb.Combobox(master=container, textvariable=variable, values=fun, width=20)
        ent.pack(side=LEFT, padx=5, ipadx=30, fill=X, expand=YES)
    elif widget == "Date" or widget == "date":
        ent = ttkb.DateEntry(master=container,dateformat=r"%Y%m%d")
        ent.pack(side=LEFT, padx=5, ipadx=30, fill=X, expand=YES)


def create_table(master: ttkb.Window, data: pd.DataFrame):
    colors = master.style.colors
    container = ttkb.Toplevel(title="Data Validation")
    table = Tableview(
        master=container,
        coldata=list(data.columns.values),
        rowdata=list(data.itertuples(index=False, name=None)),
        paginated=True,
        searchable=True,
        bootstyle=LIGHT,
        stripecolor=(colors.light, None),
        autofit=True,
        pagesize=30,
        height=30
    )
    table.pack(fill=BOTH, expand=YES, padx=10, pady=10)
    table.autofit_columns()

    btn_container = ttkb.Frame(master=container)
    btn_container.pack(fill=X, expand=YES, pady=(15, 10))

    def on_done():
        print("First window destroyed")
        master.destroy()
        print("After destruction of first window")
    done_btn = ttkb.Button(master=btn_container, text="Done", command=on_done)
    done_btn.pack(side=RIGHT, padx=5)

    cancel_btn = ttkb.Button(master=btn_container, text="Cancel", command=container.destroy)
    cancel_btn.pack(side=RIGHT, padx=5)


class DBLoginGUI(ttkb.LabelFrame):
    def __init__(self, master, connect_callback: Callable):
        super().__init__(master, padding=(15, 5), text="D/B Login")
        self.grid(row=0, column=0, padx=5, pady=5, sticky=NSEW)

        self.username = ttkb.StringVar()
        self.password = ttkb.StringVar()
        self.host = ttkb.StringVar()
        self.database = ttkb.StringVar()
        self.protocol = ttkb.StringVar()
        self.port = ttkb.IntVar()
        self.driver = ttkb.StringVar()
        self.connect_fun: Callable = connect_callback
        self.master = master
        self.cancel_btn = None
        self.connect_btn = ttkb.Button()

    def create_gui(self):
        """Creates Labels, Entries and Combo-boxes for the variables. Also created submit and cancel button"""
        create_form_entry(self, "Combobox", "Driver: ", self.driver, pyodbc.drivers())
        create_form_entry(self, "Entry", "Username: ", self.username)
        create_form_entry(self, "Entry", "Password: ", self.password)
        create_form_entry(self, "Entry", "Hostname: ", self.host)
        create_form_entry(self, "Entry", "Database: ", self.database)
        create_form_entry(self, "Entry", "Protocol: ", self.protocol)
        create_form_entry(self, "Entry", "Port: ", self.port)

        container = ttkb.Frame(self)
        container.pack(fill=X, expand=YES, pady=(15, 10))

        self.connect_btn = ttkb.Button(
            master=container,
            text="Connect",
            command=self.bind_on_connect,
            bootstyle=SUCCESS,
            width=6,
        )
        self.connect_btn.pack(side=RIGHT, padx=5, ipadx=5)
        self.connect_btn.focus_set()

        self.cancel_btn = ttkb.Button(
            master=container,
            text="Cancel",
            command=self.on_cancel,
            bootstyle=DANGER,
            width=6,
        )
        self.cancel_btn.pack(side=RIGHT, padx=5, ipadx=5)

    def bind_on_connect(self, callback):
        self.connect_btn.bind("<Button-1>", callback)

    def on_cancel(self):
        self.master.destroy()


class FeedsInput(ttkb.LabelFrame):
    def __init__(self, master, opt):
        super().__init__(master, padding=(10, 5), text="Feeds Input")
        # self.pack(side=TOP, fill=BOTH)
        self.grid(row=0,column=1, padx=5, pady=5, sticky=N)

        self.param1 = ttkb.StringVar()
        self.param2 = ttkb.IntVar()
        self.param3 = ttkb.StringVar()
        self.param4 = ttkb.StringVar()
        self.param5 = ttkb.StringVar()
        self.table = ttkb.StringVar()
        self.table_opt = opt

        self.fetch_btn: ttkb.Button = None

    def create_gui(self):
        create_form_entry(self, "entry", "Parameter 1: ", self.kritype)
        create_form_entry(self, "Date", "Parameter 2: ", self.cob)
        create_form_entry(self, "Entry", "Parameter 3: ", self.risk_indicator)
        create_form_entry(self, "Entry", "PArameter 4: ", self.src_sys)
        create_form_entry(self, "Entry", "Parameter 5: ", self.feed_loc)
        create_form_entry(self, "Combobox", "Table: ",self.table, fun=self.table_opt)

        container = ttkb.Frame(self)
        container.pack(fill=X, expand=YES, pady=(15, 10))

        self.fetch_btn = ttkb.Button(
            master=container,
            text="Fetch",
            command=self.bind_on_fetch,
            bootstyle=SUCCESS,
            width=6,
        )
        self.fetch_btn.pack(side=RIGHT, padx=5, ipadx=5)
        self.fetch_btn.focus_set()

    def bind_on_fetch(self, func: Callable = None):

        import inspect

        args_passed = locals()
        print(args_passed)
        print("Called function print_args(self, func)")
        print_args(self, func)
        print(callable(func))
        # traceback.print_stack()
        caller_frame = inspect.currentframe().f_back.f_back
        print(caller_frame)
        print(caller_frame.f_back)
        print(caller_frame.f_back.f_trace)
        print(caller_frame.f_code.co_name)
        self.fetch_btn.bind("<Button-1>", func)


class TableView(ttkb.Toplevel):
    def __init__(self, master: ttkb.Window, data: pd.DataFrame):
        super().__init__(title="Fetched Data")
        self.master = master
        self.df = data
        self.btn_container: ttkb.Frame = None
        self.done_btn: ttkb.Button = None
        self.cancel_btn: ttkb.Button = None

    def create_table(self, done_fun: Callable, cancel_fun: Callable):
        colors = self.master.style.colors
        table = Tableview(
            master=self,
            coldata=list(self.df.columns.values),
            rowdata=list(self.df.itertuples(index=False, name=None)),
            paginated=True,
            searchable=True,
            bootstyle=LIGHT,
            stripecolor=(colors.dark, None),
            autofit=True,
            pagesize=38,
            height=38
        )
        table.pack(fill=BOTH, expand=YES, padx=10, pady=10)
        table.autofit_columns()

        self.btn_container = ttkb.Frame(master=self)
        self.btn_container.pack(fill=X, expand=YES, pady=(15, 10))

        self.done_btn = ttkb.Button(master=self.btn_container, text="Done", command=done_fun)
        self.done_btn.pack(side=RIGHT, padx=10)

        self.cancel_btn = ttkb.Button(master=self.btn_container, text="Cancel", command=cancel_fun)
        self.cancel_btn.pack(side=RIGHT, padx=10)


class Model:
    def __init__(self) -> None:
        self.master = ttkb.Window("Fetching Data")
        self.master.style.theme_use(themename="solar")
        self.db_login = DBLoginGUI(self.master, connect_callback=self.on_connect)
        self.db_login.create_gui()
        self.db_login.bind_on_connect(self.on_connect)
        self.conn: pyodbc.Connection = None
        self.cursor : pyodbc.Cursor = None
        self.table_opt: List[str] = list()
        self.feeds_input: FeedsInput = None
        self.conn_string: str = str()
        self.table_view: TableView = None

        #Login Credentials
        self.username: str = str()
        self.password: str = str()
        self.protocol: str = str()
        self.driver: str = str()
        self.host: str = str()
        self.port: int = int()
        self.database: str = str()

    def on_connect(self, event=None):
        self.username = self.db_login.username.get()
        self.password = self.db_login.password.get()
        self.driver = self.db_login.driver.get()
        self.host = self.db_login.host.get()
        self.port = self.db_login.port.get()
        self.protocol = self.db_login.protocol.get()
        self.database = self.db_login.database.get()

        self.conn_string = f'DRIVER={self.driver};' \
                           f'DATABASE={self.database};' \
                           f'HOSTNAME={self.host};' \
                           f'PORT={self.port};' \
                           f'PROTOCOL={self.protocol};' \
                           f'UID={self.username};' \
                           f'PWD={self.password};'

        try:
            self.conn = pyodbc.connect(self.conn_string)
            ttkb.dialogs.Messagebox.show_info("Connected Successfully!", position=(1000, 500))
            self.cursor = self.conn.cursor()
            self.feeds_input = FeedsInput(self.master, self.get_table())
            self.feeds_input.create_gui()
            self.feeds_input.bind_on_fetch(self.on_fetch)
        except pyodbc.Error as e:
            ttkb.dialogs.Messagebox.show_error(f"Error connecting to D/B: {e}", "Connection Error", position=(500, 500))

    def get_table(self) -> List[str]:
        self.table_opt = [row.table_name for row in self.cursor.tables(catalog=self.database, tableType="TABLE", schema=self.username.upper())]
        return self.table_opt

    def on_fetch(self, event=None):
        print(event.widget)
        print(f"Number of active threads in on_fetch:{threading.active_count()}")
        query = f"SELECT * FROM {self.feeds_input.table.get()}"
        rows = self.cursor.execute(query).fetchall()
        df = pd.DataFrame.from_records(rows, columns=[col[0] for col in self.cursor.description])
        # print(df.to_string())
        self.table_view = TableView(master=self.master, data=df)
        self.table_view.create_table(done_fun=self.on_done, cancel_fun=self.on_cancel)
        # create_table(master=self.master, data=df)

    def on_done(self, event=None):
        self.master.unbind_all("")
        print(f"Number of active threads in on_done before master_update:{threading.active_count()}")
        self.master.update()
        self.table_view.update()
        print(f"Number of active threads in on_done before destroy():{threading.active_count()}")
        self.table_view.destroy()
        self.master.destroy()

    def on_cancel(self, event=None): ...

    def run(self):
        self.master.bind("<<all>>", log_event)
        self.master.mainloop(0)


if __name__ == '__main__':
    data1 = Model()
    data1.run()

    data2 = Model()
    data2.run()

Issues faced: -

  • I have added functionality to the buttons using widget.bind() rather than the Button(text="Something", command=funciton_name). For what debugging I have done, it seems that when I use the .bind() function, then if a top level window is opened then the binding happens again. But since I have only bound the function explicitly once, the other binding is kind of arbitrary and the function that invokes the bind_on_fetch() function is a called from __call__ definition of mainloop. I am not sure what is happening here.
  • Now when the top level window opens and then when I click on "Done" button then the toplevel window as well as the master window is also destroyed. But one of the widgets initiates an event that leads to a background error that is not handled by the tkinter library.

Output for your reference: -

{'self': <__main__.FeedsInput object .!feedsinput>, 'func': <bound method Model.on_fetch of <__main__.Model object at 0x000001BB476EF510>>, 'inspect': <module 'inspect' from 'C:\\Users\\<Username>\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\inspect.py'>}
Called function print_args(self, func)
args: (<__main__.FeedsInput object .!feedsinput>, <bound method Model.on_fetch of <__main__.Model object at 0x000001BB476EF510>>) (tuple)
True
<frame at 0x000001BB583D19C0, file 'C:\\Users\\<Username>\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\tkinter\\__init__.py', line 1948, code __call__>
<frame at 0x000001BB58391FE0, file 'C:\\Users\\<Username>\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\tkinter\\__init__.py', line 1485, code mainloop>
None
__call__
.!feedsinput.!frame7.!button
Number of active threads in on_fetch:1
{'self': <__main__.FeedsInput object .!feedsinput>, 'func': None, 'inspect': <module 'inspect' from 'C:\\Users\\<Username>\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\inspect.py'>}
Called function print_args(self, func)
args: (<__main__.FeedsInput object .!feedsinput>, None) (tuple)
False
<frame at 0x000001BB58391FE0, file 'C:\\Users\\<Username>\\AppData\\Local\\Programs\\Python\\Python311\\Lib\\tkinter\\__init__.py', line 1485, code mainloop>
<frame at 0x000001BB58392820, file 'C:\\Users\\<Username>\\CompareDB\\model.py', line 311, code run>
None
mainloop
Number of active threads in on_done before master_update:1
Number of active threads in on_done before destroy():1
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

I tried adding some logging functions to get the traceback as to which function is triggering these event calls, but I have been unable to pinpoint which line in tkinter python files were leading to these event triggers.

JRiggles
  • 4,847
  • 1
  • 12
  • 27
user350331
  • 111
  • 3
  • 1
    https://stackoverflow.com/help/minimal-reproducible-example – Сергей Кох May 04 '23 at 15:55
  • 1
    Please trim your code to make it easier to find your problem. Follow these guidelines to create a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – Community May 05 '23 at 16:25

1 Answers1

0

You should not be calling mainloop twice. mainloop won't return until you've called the quit method or the window has been destroyed. The second time you call it, the root window will no longer exist.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • I haven't called the mainloop twice. I created root window through a class and then destroyed it when a button is pressed, afterwards I created the new window only when the old window has been destroyed. – user350331 May 05 '23 at 00:40
  • I think the code snippet that I have provided is a little too big I need to trim it according to MRE guidelines. – user350331 May 05 '23 at 00:41