Menus#
Menus are built from a single widget — Menu, imported as
ttk.Menu. The same widget plays three roles depending on how you attach it:
a menu bar across the top of a window, a cascade that drops down from a
bar entry, and a popup at the pointer for a right-click context menu. This
guide builds each in turn, then covers the part that trips people up — how a menu
bar differs across Windows, Linux, and macOS, where the application menu follows
its own rules.
Note
Menu is a classic tk widget, not a themed ttk one, so it takes no
bootstyle — but ttkbootstrap colors it to match the active theme
automatically.
The menu widget#
Every menu is a Menu you fill with items. The item kinds are the same
whatever role the menu plays:
Item |
Adds… |
|---|---|
|
a clickable entry that runs a callback. |
|
a submenu — another |
|
a toggle bound to a |
|
one of a mutually exclusive set sharing a variable. |
|
a divider line between groups. |
A menu bar is just a menu whose items are cascades; a context menu is a menu you pop up yourself. Build the items once — the structure is identical.
A menu bar#
Build the bar, build one submenu per top-level entry, then hand the bar to the
window with configure(menu=...):
import ttkbootstrap as ttk
app = ttk.App(title="Editor")
menubar = ttk.Menu(app)
file_menu = ttk.Menu(menubar, tearoff=False)
file_menu.add_command(label="New", command=lambda: print("new"))
file_menu.add_command(label="Open…", command=lambda: print("open"))
file_menu.add_separator()
file_menu.add_command(label="Exit", command=app.destroy)
menubar.add_cascade(label="File", menu=file_menu)
edit_menu = ttk.Menu(menubar, tearoff=False)
edit_menu.add_command(label="Undo", command=lambda: print("undo"))
menubar.add_cascade(label="Edit", menu=edit_menu)
app.configure(menu=menubar) # attach the bar to the window
app.mainloop()
Each submenu takes the bar as its
master(ttk.Menu(menubar, …)) and is attached withadd_cascade().tearoff=Falseremoves the dashed “tear this menu off” line at the top — a dated Tk default you almost always want off. (It is a no-op on macOS, which has no tearoffs.)configure(menu=menubar)(equivalentlyapp["menu"] = menubar) installs the bar on a top-level window —Appor aToplevel.
Note
On Windows, menu-bar menus are drawn by the operating system, so a
dropped menu-bar menu keeps the native (light) look and does not follow a
dark theme — visible above. Pop-up menus (tk_popup(), below) are drawn
by Tk and do theme.
Keyboard shortcuts (accelerators)#
An accelerator is the shortcut hint shown at the right of a menu entry. It is
only a label — Tk does not bind it for you. Set accelerator= for the
display and bind the matching event yourself:
file_menu.add_command(label="Save", accelerator="Ctrl+S", command=save)
app.bind_all("<Control-s>", lambda event: save())
Keep the two in step: the accelerator string is cosmetic, the bind is what
actually fires. Use bind_all so the shortcut works regardless of which widget
has focus.
Ctrl on Windows/Linux, Command on macOS
The conventional modifier is Ctrl on Windows/Linux and Command on macOS. Branch on the windowing system so the label and the binding match the platform:
import ttkbootstrap as ttk
app = ttk.App()
is_mac = ttk.windowing_system(app) == "aqua"
accel = "Cmd+S" if is_mac else "Ctrl+S"
seq = "<Command-s>" if is_mac else "<Control-s>"
file_menu.add_command(label="Save", accelerator=accel, command=save)
app.bind_all(seq, lambda event: save())
Stateful items#
Menus can hold items bound to a variable, exactly like the widgets of the same
name. add_checkbutton() toggles a BooleanVar; a group of
add_radiobutton() items sharing one variable is mutually exclusive:
wrap = ttk.BooleanVar(value=True)
view_menu = ttk.Menu(menubar, tearoff=False)
view_menu.add_checkbutton(label="Word wrap", variable=wrap)
theme = ttk.StringVar(value="light")
for name in ("light", "dark"):
view_menu.add_radiobutton(label=name.title(), value=name, variable=theme)
The menu keeps the variable in sync as the user toggles items — read
wrap.get() / theme.get() in your callbacks, or trace the variable to react
immediately (see Variables & reactivity).
Enabling, disabling, and rebuilding#
Refer to an existing item by its label (or index) and change it with
entryconfigure() — greying out Paste when there is nothing to paste,
say:
edit_menu.entryconfigure("Paste", state="disabled")
edit_menu.entryconfigure("Paste", state="normal") # re-enable
To decide an item’s state at the moment the menu opens, give the menu a
postcommand — it runs just before the menu is shown, the right hook for
“enable Paste only if the clipboard has text”:
from tkinter import TclError
def refresh_edit():
try:
can_paste = bool(app.clipboard_get())
except TclError: # clipboard empty or not text
can_paste = False
edit_menu.entryconfigure("Paste", state="normal" if can_paste else "disabled")
edit_menu.configure(postcommand=refresh_edit)
A right-click context menu#
A context menu is the same Menu widget, popped up at the pointer instead of
attached to a bar. Build it once, then bind the right-click event to a handler
that calls tk_popup() with the pointer’s screen coordinates:
import ttkbootstrap as ttk
app = ttk.App()
target = ttk.Label(app, text="Right-click me", padding=40, bootstyle="@secondary")
target.pack(fill="both", expand=True)
context = ttk.Menu(app, tearoff=False)
context.add_command(label="Cut", command=lambda: print("cut"))
context.add_command(label="Copy", command=lambda: print("copy"))
context.add_command(label="Paste", command=lambda: print("paste"))
def show_context(event):
try:
context.tk_popup(event.x_root, event.y_root)
finally:
context.grab_release()
target.bind("<Button-3>", show_context)
app.mainloop()
event.x_root/event.y_rootare the pointer’s position in screen coordinates, which is whattk_popup()expects.The
try/finallywithgrab_release()is the standard idiom — it releases the menu’s input grab even if the user dismisses the popup without choosing anything.
Right-click across platforms
The right mouse button is <Button-3> on Windows and Linux. On macOS a
right-click is also <Button-2> on some mice, and a Control-click arrives as
<Button-1>; bind the extras when you target macOS:
target.bind("<Button-3>", show_context) # Windows / Linux / most macOS
target.bind("<Button-2>", show_context) # some macOS mice
target.bind("<Control-Button-1>", show_context) # macOS Control-click
The menu bar across platforms#
A menu bar looks the same in code everywhere, but the OS decides where it lives and how the first menu behaves. This is the one area worth understanding before you ship cross-platform.
Platform |
Where the menu bar lives |
|---|---|
Windows / Linux |
Per window — the bar sits at the top of each window it is attached to. |
macOS |
Global — one bar at the top of the screen, shared by every window in the app. The first cascade becomes the bold application menu named after your app. |
macOS: the application menu#
On macOS the leftmost menu is the application menu (bold, your app’s name),
and users expect About, Preferences…, and Quit to live there — not under
File. Menu builds these native slots for you: add_application_menu(),
add_window_menu(), and add_help_menu() create the standard macOS
menus, and on_preferences() / on_quit() wire the standard commands.
add_application_menu and add_window_menu return None off macOS (there
is no application menu there), and on_preferences / on_quit are no-ops;
add_help_menu builds an ordinary Help cascade on every platform. So a single
code path builds a native-feeling bar everywhere:
import ttkbootstrap as ttk
app = ttk.App(title="Editor")
menubar = ttk.Menu(app)
app_menu = menubar.add_application_menu() # None off macOS
if app_menu:
app_menu.add_command(label="About Editor", command=show_about)
app_menu.on_preferences(open_preferences) # enables the Preferences… item (⌘,)
app_menu.on_quit(save_and_quit) # runs on Quit / ⌘Q
menubar.add_window_menu()
file_menu = ttk.Menu(menubar, tearoff=False)
file_menu.add_command(label="New", command=new_file)
menubar.add_cascade(label="File", menu=file_menu)
menubar.add_help_menu(command=open_help) # standard Help menu; ⌘? search on macOS
app.configure(menu=menubar)
app.mainloop()
The if app_menu: guard reads as “if this platform has an application
menu.” On macOS app_menu is the real menu and its items land in the native
About/Preferences/Quit block; on Windows and Linux it is None, so those items
are simply skipped — put your Exit and Preferences where those platforms expect
them (under File and Edit, say). add_help_menu returns an ordinary Help
cascade off macOS, so a Help menu is present everywhere.
See also
Windows — the cross-platform windowing model this builds on.
Events guide — the event object and binding conventions.
Menu reference — the full option set.