Is there anyway to implement a Menu Bar inside the Title Bar? #1422
Replies: 6 comments 18 replies
-
Could try using on enter on a label that then places a frame below it with the menu items, then on leave for the frame to remove it. |
Beta Was this translation helpful? Give feedback.
-
@LucianoSaldivia Change the title bar color (guide is given here: #1299) Or, you can recreate a new title bar with customtkinter by disabling the native title bar. Here is a full ready-made template: #736 Other than that python can't change window attributes, you can only do that with C (or ctypes maybe incase of python), an example library is here: https://github.com/littlewhitecloud/CustomTkinterTitlebar (Don't know if it will work properly) Edit: This menubar will work: #1670 |
Beta Was this translation helpful? Give feedback.
-
Hi, first of all, thanks for such a complete answer and also being so responsive! I've tried the three approaches you mention, and the farthest i've gotten was with the second one, disabling the native title bar and creating my own. But the DropdownMenu is giving me lots of issues, that's the main reason i'm looking for another approach. import customtkinter as ctk
import tkinter as tk
from functools import partial
from typing import Callable
OPTION_BUTTON_PADX = 3
OPTION_BUTTON_PADY = 3
class _CDMOptionButton(ctk.CTkButton):
def setParentMenu(self, menu: "CustomDropdownMenu"):
self.parent_menu = menu
class _CDMSubmenuButton(_CDMOptionButton):
def setSubmenu(self, submenu: "CustomDropdownMenu"):
self.submenu = submenu
class CustomDropdownMenu(ctk.CTkFrame):
def __init__(self,
master,
menu_seed_object: ctk.CTkBaseClass | _CDMSubmenuButton,
border_width: int = 1,
fg_color = None,
border_color: str | tuple[str, str] = "grey50",
item_fg_color: str | tuple[str, str] = "transparent",
item_hover_color: str | tuple[str, str] = "grey20",
item_font: ctk.CTkFont = ("helvetica", 10),
):
super().__init__(
master=master,
border_width=border_width,
fg_color=fg_color,
border_color=border_color,
)
self.menu_seed_object = menu_seed_object
self.item_fg_color = item_fg_color
self.item_hover_color = item_hover_color
self.item_font = item_font
self._options_list: list[_CDMOptionButton | _CDMSubmenuButton] = []
# Se dispara al entrar en cualquier opción?
self.bind("<Leave>", self._checkIfMouseLeft)
# hidden by default
def selectOption(self, command) -> None:
self._hideAllMenus()
command()
def addOption(self, option: str, command: Callable) -> None:
optionButton = _CDMOptionButton(
self,
text=option,
anchor="w",
command=partial(self.selectOption, command)
)
optionButton.setParentMenu(self)
self._options_list.append(optionButton)
self._configureButton(optionButton)
optionButton.pack(
side="top",
fill="both",
expand=True,
padx=OPTION_BUTTON_PADX,
pady=OPTION_BUTTON_PADY,
)
def addSubmenuFromSeed(self, submenuSeedName: str) -> "CustomDropdownMenu":
submenuButtonSeed = _CDMSubmenuButton(self, text=submenuSeedName, anchor="w")
submenuButtonSeed.setParentMenu(self)
self._options_list.append(submenuButtonSeed)
self._configureButton(submenuButtonSeed)
submenu = CustomDropdownMenu(
master=self.master,
menu_seed_object=submenuButtonSeed,
item_fg_color=self.item_fg_color,
item_hover_color=self.item_hover_color,
item_font=self.item_font,
)
submenuButtonSeed.setSubmenu(submenu=submenu)
submenuButtonSeed.configure(command=submenu.toggleShow)
submenuButtonSeed.bind("<Enter>", submenu._show)
submenuButtonSeed.pack(
side="top",
fill="both",
expand=True,
padx=3,
pady=3,
)
return submenu
def addSeparator(self) -> None:
# separator = ttk.Separator(self, orient="horizontal", bg=self._border_color) # No puedo cambiar el color
separator = ctk.CTkFrame(
master=self,
height=2, # No se puede hacer de 1px, es de mínimo 2px
fg_color='grey20',
# fg_color=self._border_color,
border_width=0
)
separator.pack(
side="top",
fill="x",
expand=True,
)
def _show(self, *args, **kwargs) -> None:
if isinstance(self.menu_seed_object, _CDMSubmenuButton):
# Submenu
self.place(
in_=self.menu_seed_object.parent_menu,
x=self.menu_seed_object.winfo_x() + self.menu_seed_object.winfo_width() + OPTION_BUTTON_PADX - 1,
y=self.menu_seed_object.winfo_y() - OPTION_BUTTON_PADY,
)
else:
# First Menu
self.place(
x=self.menu_seed_object.winfo_x(),
y=self.menu_seed_object.winfo_y() + self.menu_seed_object.winfo_height(),
)
self.focus()
def _hide(self, *args, **kwargs) -> None:
self.place_forget()
def _hideParentMenus(self, *args, **kwargs) -> None:
if isinstance(self.menu_seed_object, _CDMSubmenuButton):
self.menu_seed_object.parent_menu._hideParentMenus()
self.menu_seed_object.parent_menu._hide()
def _hideChildrenMenus(self, *args, **kwargs) -> None:
# Si self contiene submenú , lo cierro
if any(isinstance(option, _CDMSubmenuButton) for option in self._options_list):
for option in self._options_list:
if isinstance(option, _CDMSubmenuButton):
option.submenu._hide()
def _hideAllMenus(self, *args, **kwargs) -> None:
self._hideChildrenMenus()
self._hide()
self._hideParentMenus()
def _collapseSiblingSubmenus(self, button: _CDMOptionButton | _CDMSubmenuButton, *args, **kwargs) -> None:
for option in self._options_list:
if option != button and isinstance(option, _CDMSubmenuButton):
option.submenu._hideChildrenMenus()
option.submenu._hide()
def toggleShow(self, *args, **kwargs) -> None:
if not self.winfo_manager():
self._show()
else:
self._hideChildrenMenus()
self._hide()
def _configureButton(self, boton: ctk.CTkButton) -> None:
boton.configure(fg_color="transparent")
if self.item_fg_color:
boton.configure(fg_color=self.item_fg_color)
if self.item_hover_color:
boton.configure(hover_color=self.item_hover_color)
if self.item_font:
boton.configure(font=self.item_font)
boton.bind("<Enter>", partial(self._collapseSiblingSubmenus, boton))
def _getSubMenus(self) -> list["CustomDropdownMenu"]:
if any(isinstance(option, _CDMSubmenuButton) for option in self._options_list):
subMenusList = list()
for option in self._options_list:
if isinstance(option, _CDMSubmenuButton):
subMenusList.append(option.submenu)
return subMenusList
else:
return []
def _contieneCoordenada(self, x_root, y_root) -> bool:
return self.winfo_rootx() < x_root < self.winfo_rootx()+self.winfo_width() and \
self.winfo_rooty() < y_root < self.winfo_rooty()+self.winfo_height()
def _checkIfMouseLeft(self, event: tk.Event=None) -> None:
# print(event.x_root, event.y_root, self._contieneCoordenada(event.x_root, event.y_root))
# No está en el menu del evento
if not self._contieneCoordenada(event.x_root, event.y_root):
# Es Submenu y no está en el parent
if isinstance(self.menu_seed_object, _CDMSubmenuButton) and not self.menu_seed_object.parent_menu._contieneCoordenada(event.x_root, event.y_root):
# No tiene Submenus, o tiene y no está en ninguno de ellos
subMenus = self._getSubMenus()
if subMenus == [] or all((not submenu._contieneCoordenada(event.x_root, event.y_root)) for submenu in subMenus):
self._hideAllMenus()
# No es submenú
elif not isinstance(self.menu_seed_object, _CDMSubmenuButton):
# No tiene Submenus, o tiene y no está en ninguno de ellos
subMenus = self._getSubMenus()
if subMenus == [] or all((not submenu._contieneCoordenada(event.x_root, event.y_root)) for submenu in subMenus):
self._hideAllMenus()
def dummy(*kwargs):
print("dummy func called")
def main():
root = ctk.CTk()
root.geometry("800x500+200+200")
boton = ctk.CTkButton(root, anchor="w")
dropdownMenu = CustomDropdownMenu(master=root, menu_seed_object=boton)
boton.configure(
text="Archivo",
corner_radius=7,
fg_color="transparent",
hover_color="darkblue",
command=dropdownMenu.toggleShow,
)
dropdownMenu.addOption("Abrir...", dummy)
dropdownMenu.addOption("Nuevo", dummy)
dropdownMenu.addSeparator()
exportarSubmenu = dropdownMenu.addSubmenuFromSeed(submenuSeedName="Exportar...")
exportarSubmenu.addOption("Como PNG...", dummy)
exportarSubmenu.addOption("Como PDF...", dummy)
exportarSubmenu.addSeparator()
otraCosaSubmenu = exportarSubmenu.addSubmenuFromSeed(submenuSeedName="Como otra cosa...")
otraCosaSubmenu.addOption("Como querés exportarlo campeón?...", dummy)
# boton.pack()
boton.place(
x=0,
y=0,
)
root.mainloop()
if __name__ == "__main__":
main() As you can see, the whole menu closes either when you choose an option, or when you |
Beta Was this translation helpful? Give feedback.
-
@LucianoSaldivia I just found a hacky way to make this possible in python: import customtkinter
from ctypes import windll, byref, sizeof, c_int
class CTkTitleMenu(customtkinter.CTkToplevel):
def change_dimension(self):
self.geometry(f"{root.winfo_width()-125-self.x_offset}x{root.winfo_height()}+{root.winfo_x()+self.x_offset}+{root.winfo_y()+self.y_offset}")
self.deiconify()
def change_header_color(self, caption_color):
try:
# optional feature to change the header color in windows
HWND = windll.user32.GetParent(self.master.winfo_id())
DWMWA_CAPTION_COLOR = 35
windll.dwmapi.DwmSetWindowAttribute(HWND, DWMWA_CAPTION_COLOR, byref(c_int(caption_color)), sizeof(c_int))
except:
pass
def __init__(
self,
master,
title_bar_color = "default",
x_offset: int = 40,
y_offset: int = 6):
super().__init__()
self.after(10)
self.master = master
self.master.minsize(200,100)
self.master.title("")
self.overrideredirect(True)
if title_bar_color=="default":
if customtkinter.get_appearance_mode()=="Light":
title_bar_color = 0x999999 # RGB order: 0xrrggbb
else:
title_bar_color = 0x303030 # RGB order: 0xrrggbb
self.transparent_color = self._apply_appearance_mode(self._fg_color)
self.attributes("-transparentcolor", self.transparent_color)
self.resizable(True, True)
self.config(background=self.transparent_color)
self.caption_color = title_bar_color
self.change_header_color(self.caption_color)
self.x_offset = x_offset
self.y_offset = y_offset
self.master.bind("<Configure>", lambda _: self.change_dimension())
self.master.bind("<FocusIn>", lambda _: self.deiconify())
self.master.bind("<FocusOut>", lambda _: self.withdraw() if self.master.state()=="iconic" else None)
self.bind("<FocusIn>", lambda _: root.focus())
# Example Usage:
customtkinter.set_appearance_mode("Dark")
root = customtkinter.CTk()
header = CTkTitleMenu(root)
for i in range(0, 5):
# add widgets in header frame
customtkinter.CTkButton(header, text="Tab"+str(i), fg_color="grey15",
width=50, height=10).grid(row=0, column=i, padx=(0,10))
root.mainloop() |
Beta Was this translation helpful? Give feedback.
-
I finally made this project: #1670 |
Beta Was this translation helpful? Give feedback.
-
i HAVENT tried this, but you can use overridedirect. |
Beta Was this translation helpful? Give feedback.
-
In a professional looking app like Visual Studio Code, you'd have this menu bar:

implemented inside the title bar:

I've been searching everywhere, and while you can start with
overrideredirect(True)
, there are many issues when trying to implement something like this with basic widgets.No other GUI package has this ability.
If someone knows how to implement something as shown in the images, please let me know how.
Beta Was this translation helpful? Give feedback.
All reactions