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 theButton(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.