CustomTkinter
CustomTkinter copied to clipboard
Request: Messageboxes and Menu
Hi, I've got a suggestion for a new feature to CustomTkinter: adding messageboxes! There is no such feature available in CustomTkinter, so I tried using Tkinter messageboxes. They don't fit with the styling of CustomTkinter, so I was wondering if you could make that an available feature. Also, adding a menu option (like the Tkinter Menu option), would be nice. Thanks, Ben
Note: On Windows, regular tk menus work fine:

until you use Dark mode

And if you want to get fancy, I'm happy to contribute the following code for allowing you to define menus in yaml...
menu_defn_yaml = """
- File:
- !item [ Open New Workspace, on_new_workspace ]
- !sep
- !item [ Reload Config, on_reload_config ]
- !sep
- !item [ Exit, on_closing ]
- View:
- !check [ Show Debug Logging, show_debug_logging ]
- !sep
- !item [ Open Logs Folder, on_open_logs_folder ]
- !sep
- !radio [ Theme, var_theme,
[ "System", "Light", "Dark" ]
]
- Help:
- !item [ About, on_about, ]
- !item [ Support, on_support, ]
- !item [ Report a Bug, on_bug_report, ]
"""
class MenuItem:
""" Base type for something we can 'add' to the menu system. """
name: str # How the item appears in the menu
func: str # Name of the app method to call.
shortcut: typing.Optional[str] # not implemented
def __init__(self, name, func, shortcut) -> None:
self.name = name
self.func = func
self.shortcut = shortcut
def _missing_command(self, app):
print(f"** {app.__class__.__name__} missing command: {self.func}")
def command(self, app):
return getattr(app, self.func, lambda: self._missing_command(app) )
def add(self, menubar, app):
menubar.add_command(label=self.name, command=self.command(app))
class MenuToggle(MenuItem):
shortcut: typing.Optional[str]
check: bool
var_name: str
def __init__(self, name, func, shortcut) -> None:
var_name = func.replace("on_change_", "")
super().__init__(name, f"on_change_{var_name}", shortcut=shortcut)
self.var_name = var_name
def add(self, menubar, app):
var = tk.BooleanVar()
setattr(app, self.var_name, var)
menubar.add_checkbutton(label=self.name, variable=var, command=self.command(app))
class MenuRadio(MenuItem):
def __init__(self, name, var_name, items) -> None:
super().__init__(name, f"on_change_{var_name}", shortcut=None)
self.var_name = var_name
self.items = items
def add(self, menubar, app):
submenu = tk.Menu(menubar, tearoff=0)
var = tk.StringVar(value=self.items[0])
setattr(app, self.var_name, var)
for item in self.items:
submenu.add_radiobutton(label=item, variable=var, value=item, command=self.command(app))
menubar.add_cascade(label=self.name, menu=submenu)
class MenuSeparator:
def add(self, menubar, _):
menubar.add_separator()
def yaml_menu_sep(loader, node):
""" Helper: !sep: Inject a separator into the menu. """
return MenuSeparator()
def yaml_menu_item(loader, node):
""" Helper: !item: Inject a new item into the menu. """
seq = loader.construct_sequence(node)
shortcut = seq[2] if len(seq) > 2 and seq[2] else None
return MenuItem(name=seq[0], func=seq[1], shortcut=shortcut)
def yaml_menu_check(loader, node):
""" Helper: !check: Inject a toggle (checkbox) item into the menu. """
seq = loader.construct_sequence(node)
shortcut = seq[2] if len(seq) > 2 and seq[2] else None
return MenuToggle(name=seq[0], func=seq[1], shortcut=shortcut)
def yaml_menu_radio(loader, node):
""" Helper: !radio: Inject a radio menu. """
return MenuRadio(*loader.construct_sequence(node))
yaml.add_constructor("!sep", yaml_menu_sep)
yaml.add_constructor("!item", yaml_menu_item)
yaml.add_constructor("!check", yaml_menu_check)
yaml.add_constructor("!radio", yaml_menu_radio)
# Code in the menubar type that adds the constructed data:
# takes a 'menu' which can be a menu bar or a nested
# menu item.
class MenuHolder(tk.Menu):
# ... __init__ etc.
def add_menu_items(self, menu, menu_level):
if isinstance(menu_level, dict):
for menu_name, menu_items in menu_level.items():
submenu= tk.Menu(menubar, tearoff=False)
self.add_menu_items(submenu, menu_items)
menu.add_cascade(label=menu_name, menu=submenu, underline=0)
elif isinstance(menu_level, list):
for item in menu_level:
self.add_menu_items(menu, item)
else:
menu_level.add(menu, self)
def populate(self, yaml_data):
menu_defn = yaml.load(yaml_data, Loader=yaml.Loader)
self.menubar = tk.Menu()
self.config(menu=self.menubar)
for menu_level in MENUS:
self.add_menu_items(self.menubar, menu_level)
class App:
def __init__(self):
...
self.menubar = MenuHolder(...)
m.poupulate(menu_defn_yaml)

Nicely done!
Thanks for contributing the code. Any ideas about the messageboxes? Thanks!
@BenjaminTGa
Any ideas about the messageboxes?
Here is a messagebox for customtkinter: https://github.com/Akascape/CTkMessagebox
@Akascape Love it! Can it show modal messageboxes? Like, the ones that show above the main window and don't let the user interact with it until they've closed the dialog?
@Akascape I try everything to make the CTkMessageBox modal or dialog.. thinking topmost=True or topmost=False would work. But sadly neither work