Menus
This chapter describes how to handle menubars and popup menus in Tk. For a polished application, these are areas you particularly want to pay attention to. Menus need special care if you want your application to fit in with other applications on your users' platform.
Speaking of which, the recommended way to figure out which platform you're running on is:
root.tk.call('tk', 'windowingsystem') # returns x11, win32 or aqua
tk windowingsystem; # returns x11, win32 or aqua
Tk.windowingsystem; # returns x11, win32 or aqua
Tkx::tk_windowingsystem(); # returns x11, win32 or aqua
Tkinter does not provide a direct equivalent to this call.
However, it is possible to directly execute an arbitrary Tcl-based Tk command
using the call()
method (available on any Tkinter widget). Here, we're invoking the
Tcl/Tk command tk windowingsystem
.
This is more useful than examining global variables like tcl_platform
or
sys.platform
; older checks that used these methods should be reviewed. While
there used to be a strong correlation between platform and windowing system,
that's less true today. For example, if your platform is identified as Unix, that might mean
Linux under X11, macOS under Aqua, or even macOS under X11.
Menubars
In this section, we'll look at menubars: how to create them, what goes in them, how they're used, etc.
Properly designing a menubar and its set of menus is beyond the scope of this tutorial. However, if you're creating an application for someone other than yourself, here is a bit of advice. First, if you find yourself with many menus, very long menus, or deeply nested menus, you may need to rethink how your user interface is organized. Second, many people use the menus to explore what the program can do, particularly when they're first learning it, so try to ensure major features are accessible by the menus. Finally, for each platform you're targeting, become familiar with how applications use menus. Consult the platform's human interface guidelines for full details about the design, terminology, shortcuts, and much more. This is an area you will likely have to customize for each platform.
Menubars.
You'll notice applications on some recent Linux distributions that show their menus at the top of the screen when active rather than in the window itself. Tk does not yet support this style of menus.
Menu Widgets and Hierarchy
Menus are implemented as widgets in Tk, just like buttons and entries. Each menu widget consists of a number of different items in the menu. Items have various attributes, such as the text to display for the item, a keyboard accelerator, and a command to invoke.
Menus are arranged in a hierarchy. The menubar is itself a menu widget. It has several items ("File," "Edit," etc.), each of which is a submenu containing more items. These items can include things like the "Open..." command in a "File" menu but also separators between other items. It can even have items that open up their own submenu (so-called cascading menus). As you'd expect from other things you've seen already in Tk, anytime you have a submenu, it must be created as a child of its parent menu.
Menus are part of the classic Tk widgets; there is no menu widget in the themed Tk widget set.
Before you Start
It's essential to put the following line in your application somewhere before you start creating menus.
root.option_add('*tearOff', FALSE)
option add *tearOff 0
TkOption.add '*tearOff', 0
Tkx::option_add("*tearOff", 0);
Without it, each of your menus (on Windows and X11) will start with what looks like a dashed line and allows you to "tear-off" the menu, so it appears in its own window. You should eliminate tear-off menus from your application as they're not a part of any modern user interface style.
This is a throw-back to the Motif-style X11 that Tk's original look and feel were based on. Get rid of them unless your application is designed to run only on that old box collecting dust in the basement. We'll all look forward to a future version of Tk where this misguided paean to backward compatibility is removed.
While on the topic of ancient history, the option_add
bit uses the option database.
This provided a standardized way to customize some aspects of X11 user interfaces through text-based
configuration files, but it's no longer used today. Older Tk programs may use the option
command internally to
separate style configuration options from widget creation code. That approach pre-dated themed Tk styles, which should be used
for that purpose today. However, it's somehow fitting to use the obsolete option database to automatically remove the obsolete tear-off menus.
Creating a Menubar
In Tk, menubars are associated with individual windows; each toplevel window can have at most one menubar. This is visually obvious on Windows and many X11 systems, where menus are part of each window, sitting just below the title bar.
On macOS, though, there is a single menubar along the top of the screen, shared by each window. As far as your Tk program is concerned, each window still has its own menubar. As you switch between windows, Tk ensures that the correct menubar is displayed. If you don't specify a menubar for a particular window, Tk uses the menubar associated with the root window; you'll have noticed by now that this is automatically created for you when your Tk application starts.
Because all windows have a menubar on macOS, it's important to define one, either for each window or a fallback menubar for the root window. Otherwise, you'll end up with the "built-in" menubar, which contains menus that are only intended for typing commands directly into the interpreter.
To create a menubar for a window, first, create a menu widget. Then, use the
window's menu
configuration option to attach the menu widget to the window.
win = Toplevel(root)
menubar = Menu(win)
win['menu'] = menubar
toplevel .win
menu .win.menubar
.win configure -menu .win.menubar
win = TkToplevel.new(root)
menubar = TkMenu.new(win)
win['menu'] = menubar
$w = $mw->new_toplevel;
$m = $w->new_menu;
$w->configure(-menu => $m);
You can use the same menubar for more than one window.
In other words, you can specify the same menubar as the menu
configuration option for several toplevel windows.
This is particularly useful on Windows and X11, where you may want a window
to include a menu but don't necessarily need to juggle different menus in
your application. However, if the contents or state of menu items
depends on what's going on in the active window, you'll have to manage this yourself.
This is truly ancient history, but menubars used to be implemented by creating a frame widget containing the menu items and packing it into the top of the window like any other widget. Hopefully, you don't have any code or documentation that still does this.
Adding Menus
We now have a menubar, but that's pretty useless without some menus to go in it. So again, we'll create a menu widget for each menu, each one a child of the menubar. We'll then add them all to the menubar.
menubar = Menu(parent)
menu_file = Menu(menubar)
menu_edit = Menu(menubar)
menubar.add_cascade(menu=menu_file, label='File')
menubar.add_cascade(menu=menu_edit, label='Edit')
set m .win.menubar
menu $m.file
menu $m.edit
$m add cascade -menu $m.file -label File
$m add cascade -menu $m.edit -label Edit
file = TkMenu.new(menubar)
edit = TkMenu.new(menubar)
menubar.add :cascade, :menu => file, :label => 'File'
menubar.add :cascade, :menu => edit, :label => 'Edit'
$m = $w->new_menu;
$file = $m->new_menu;
$edit = $m->new_menu;
$m->add_cascade(-menu => $file, -label => "File");
$m->add_cascade(-menu => $edit, -label => "Edit");
The add_cascade
method adds a menu item, which itself is a menu (a submenu).
Adding Menu Items
Now that we have a couple of menus in our menubar, we can add a few items to each menu.
Command Items
Regular menu items are called command
items in Tk. We'll see some other types of menu items shortly.
Notice that menu items are part of the menu itself; we don't have to create a separate menu widget for each one (submenus being the exception).
menu_file.add_command(label='New', command=newFile)
menu_file.add_command(label='Open...', command=openFile)
menu_file.add_command(label='Close', command=closeFile)
$m.file add command -label "New" -command "newFile"
$m.file add command -label "Open..." -command "openFile"
$m.file add command -label "Close" -command "closeFile"
file.add :command, :label => 'New', :command => proc{newFile}
file.add :command, :label => 'Open...', :command => proc{openFile}
file.add :command, :label => 'Close', :command => proc{closeFile}
$file->add_command(-label => "New", -command => sub {newFile()});
$file->add_command(-label => "Open...", -command => sub {openFile()});
$file->add_command(-label => "Close", -command => sub {closeFile()});
The ellipsis ("...") is a special character on macOS, more tightly spaced than three periods in a row. Tk takes care of substituting this character for you automatically.
Each menu item has associated with it several configuration options, analogous to widget configuration options.
Each type of menu item has a different set of available options. Cascade
menu items have a menu
option used to specify the submenu, command menu items
have a command
option to specify the command to invoke when the item is
chosen. Both have a label
option to specify the text to display for the item.
Submenus
We've already seen cascade
menu items used to add a menu to a menubar.
Not surprisingly, if you want to add a submenu to an existing menu, you also use a
cascade
menu item exactly the same way. You might use this to build
build a "recent files" submenu, for example.
menu_recent = Menu(menu_file)
menu_file.add_cascade(menu=menu_recent, label='Open Recent')
for f in recent_files:
menu_recent.add_command(label=os.path.basename(f), command=lambda f=f: openFile(f))
menu $m.file.recent
$m.file add cascade -menu $m.file.recent -label "Open Recent"
foreach f $recent_files {
$m.file.recent add command -label [file tail $f] -command "openFile $f"
}
recent = TkMenu.new(file)
file.add :cascade, :menu => recent, :label => 'Open Recent'
recent_files.each do |f|
recent.add :command, :label => File.basename(f), :command => proc{openFile(f)}
end
$recent = $file->new_menu;
$file->add_cascade(-menu => $recent, -label => "Open Recent");
foreach $f (@recent_files) {
$recent->add_command(-label => basename($f), -command => [\&openFile, $f]);
}
Separators
A third type of menu item is the separator
, which produces the dividing line
you often see between different menu items.
menu_file.add_separator()
$m.file add separator
file.add :separator
$file->add_separator();
Checkbutton and Radiobutton Items
Finally, there are also checkbutton
and radiobutton
menu items
that behave analogously to checkbutton and radiobutton widgets. These menu items
have a variable associated with them. Depending on its value,
an indicator (i.e., checkmark or selected radiobutton) may be shown next to its label.
check = StringVar()
menu_file.add_checkbutton(label='Check', variable=check, onvalue=1, offvalue=0)
radio = StringVar()
menu_file.add_radiobutton(label='One', variable=radio, value=1)
menu_file.add_radiobutton(label='Two', variable=radio, value=2)
$m.file add checkbutton -label Check -variable check -onvalue 1 -offvalue 0
$m.file add radiobutton -label One -variable radio -value 1
$m.file add radiobutton -label Two -variable radio -value 2
check = TkVariable.new
file.add :checkbutton, :label => 'Check', :variable => check, :onvalue => 1, :offvalue => 0
radio = TkVariable.new
file.add :radiobutton, :label => 'One', :variable => radio, :value => 1
file.add :radiobutton, :label => 'Two', :variable => radio, :value => 2
$file->add_checkbutton(-label => "Check", -variable => \$check, -onvalue => 1, -offvalue => 0);
$file->add_radiobutton(-label => "One", -variable => \$radio, -value => 1);
$file->add_radiobutton(-label => "Two", -variable => \$radio, -value => 2);
When a user selects a checkbutton item that is not already checked, it sets the
associated variable to the value in onvalue
. Selecting an item that
is already checked sets it to the value in offvalue
. Selecting a radiobutton
item sets the associated variable to the value in value
. Both types of items
also react to any changes you make to the associated variable.
Like command items, checkbutton and radiobutton menu items support a command
configuration option that is invoked when the menu item is chosen. The associated
variable and the menu item's state are updated before the callback is invoked.
Radiobutton menu items are not part of the Windows or macOS human interface guidelines. On those platforms, the item's indicator is a checkmark, as it would be for a checkbutton item. The semantics still work. It's a good way to select between multiple items since it will show one of them selected (checked).
Manipulating Menu Items
As well as adding items to the end of menus, you can also insert them in the middle of menus
via the insert index type ?option value...?
method; here
index
is the position (0..n-1) of the item you want to insert before.
You can also delete one or more menu items using the delete index ?endidx?
method.
menu_recent.delete(0, 'end')
$m.file.recent delete 0 end
recent.delete 0, 'end'
$recent->delete(0, 'end');
Like most everything in Tk, you can look at or change the value of an item's options at any time.
Items are referred to via an index. Usually, this is a number (0..n-1
) indicating the
item's position in the menu. You can also specify the label of the menu item (or, in fact, a
"glob-style" pattern to match against the item's label).
print( menu_file.entrycget(0, 'label')) # get label of top entry in menu
print( menu_file.entryconfigure(0)) # show all options for an item
puts [$m.file entrycget 0 -label]; # get label of top entry in menu
puts [$m.file entryconfigure 0]; # show all options for an item
puts( file.entrycget 0, :state ); # get label of top entry in menu
puts( file.entryconfiginfo 0 ); # show all options for an item
print $file->entrycget(0, -label); # get label of top entry in menu
print $file->entryconfigure(0); # show all options for an item
State
You can disable a menu item so that users cannot select it. This can be done
via the state
option, setting it to the value disabled
. Use a value
of normal
to re-enable the item.
Menus should always reflect the current state of your application. If a menu item is not presently relevant (e.g., the "Copy" item is only applicable if something in your application is selected), you should disable it. When your application state changes so that the item is applicable, make sure to enable it.
menu_file.entryconfigure('Close', state=DISABLED)
$m.file entryconfigure Close -state disabled
file.entryconfigure 'Close', :state => 'disabled'
$file->entryconfigure("Close", -state => "disabled");
Sometimes you may have menu items whose name changes in response to application state changes, rather than the menu item being disabled. For example, A web browser might have a menu item that changes between "Show Bookmarks" and "Hide Bookmarks" as a bookmarks pane is hidden or displayed.
menu_bookmarks.entryconfigure(3, label="Hide Bookmarks")
$m.bookmarks entryconfigure 3 -label "Hide Bookmarks"
bookmarks.entryconfigure 3, :label => 'Hide Bookmarks'
$bookmarks->entryconfigure(3, -label => "Hide Bookmarks");
As your program grows complex, it's easy to miss enabling or disabling some items. One strategy is to centralize all the menu state changes in one routine. Whenever there is a state change in your application, it should call this routine. It should examine the current state and update menus accordingly. The same code can also handle toolbars, status bars, or other user interface components.
Accelerator Keys
The accelerator
option is used to indicate a keyboard equivalent that corresponds to a menu item.
This does not actually create the accelerator but only displays it next to the menu item.
You still need to create an event binding for the accelerator yourself.
Remember that event bindings can be set on individual widgets, all widgets of a certain type, the toplevel window containing the widget you're interested in, or the application as a whole. As menu bars are associated with individual windows, event bindings for menu items will usually be on the toplevel window the menu is associated with.
Accelerators are very platform-specific, not only in terms of which keys are used for
what operation, but what modifier keys are used for menu accelerators (e.g., on macOS,
it is the "Command" key, on Windows and X11, it is usually the "Control" key).
Examples of valid accelerator options are Command-N
, Shift+Ctrl+X
, and
Command-Option-B
. Commonly used modifiers include Control
, Ctrl
, Option
,
Opt
, Alt
, Shift
, "Command
, Cmd
, and Meta
.
On macOS, modifier names are automatically mapped to the different modifier icons that appear in menus, i.e.,
Shift
⇒ ⇧, Command
⇒ ⌘, Control
⇒ ⌃, and Option
⇒ ⌥.
m_edit.entryconfigure('Paste', accelerator='Command+V')
$m.edit entryconfigure "Paste" -accelerator "Command+V"
edit.entryconfigure "Paste", :accelerator => "Command+V"
$edit->entryconfigure("Paste", -accelerator => "Command+V");
Underline
All platforms support keyboard traversal of the menubar via the arrow keys. On Windows
and X11, you can also use other keys to jump to particular menus or menu items. The keys
that trigger these jumps are indicated by an underlined letter in the menu item's label.
To add one of these to a menu item, use the underline
configuration option for the item. Its value should be the index of the
character you'd like underlined (from 0 to the length of the string - 1). Unlike
accelerator keys, the menu will watch for the keystroke, so no separate event binding is needed.
m.add_command(label='Path Browser', underline=5) # underline "B"
$m add command -label "Path Browser" -underline 5" # underline "B"
m.add :command, :label => 'Path Browser', :underine => 5 # underline "B"
$m->add_command(-label => "Path Browser", -underline => 5); # underline "B"
Images
It is also possible to use images in menu items, either beside the menu item's label or
replacing it altogether. To do this, use the image
and compound
options, which work just like in label widgets. The value for image
must be
a Tk image object, while compound
can have the values bottom
,
center
, left
, right
, top
, or none
.
Menu Virtual Events
Platform conventions for menus suggest standard menus and items that should be available in most applications.
For example, most applications have an "Edit" menu, with menu items for "Copy," "Paste," etc. Tk widgets like
entry or text will react appropriately when those menu items are chosen. But if you're building your own menus,
how do you make that work? What command
would you assign to a "Copy" menu item?
Tk handles this with virtual events. As you'll recall from the Tk Concepts chapter, these are high-level application events, not low-level operating system events. Tk's widgets will watch for specific events. When you build your menus, you can generate those events rather than directly invoking a callback function. Your application can create event bindings to watch for those events too.
Some developers create virtual events for every item in their menus, generating those events instead of directly calling routines in their code. It's one way of splitting off your user interface code from the rest of your application. Remember that even if you do this, you'll still need code that enables and disables menu items, adjusts their labels, etc., in response to application state changes.
Here's a minimal example showing how we'd add two items to an "Edit" menu, the standard "Paste" item, and an application-specific "Find..." item that will open a dialog to find or search for something. We'll include an entry widget so that we can check that "Paste" works.
from tkinter import *
from tkinter import ttk, messagebox
root = Tk()
ttk.Entry(root).grid()
m = Menu(root)
m_edit = Menu(m)
m.add_cascade(menu=m_edit, label="Edit")
m_edit.add_command(label="Paste", command=lambda: root.focus_get().event_generate("<<Paste>>"))
m_edit.add_command(label="Find...", command=lambda: root.event_generate("<<OpenFindDialog>>"))
root['menu'] = m
def launchFindDialog(*args):
messagebox.showinfo(message="I hope you find what you're looking for!")
root.bind("<<OpenFindDialog>>", launchFindDialog)
root.mainloop()
grid [ttk::entry .e]
menu .m
menu .m.edit
.m add cascade -menu .m.edit -label Edit
.m.edit add command -label "Paste" -command {event generate [focus] "<<Paste>>"}
.m.edit add command -label "Find..." -command {event generate . "<<OpenFindDialog>>"}
. configure -menu .m
proc launchFindDialog {} {tk_messageBox -message "I hope you find what you're looking for!"}
bind . <<OpenFindDialog>> launchFindDialog
require 'tk'
require 'tkextlib/tile'
root = TkRoot.new()
Tk::Tile::Entry.new(root).grid
m = TkMenu.new(root)
m_edit = TkMenu.new(m)
m.add :cascade, :menu => m_edit, :label => "Edit"
m_edit.add :command, :label => "Paste", :command => proc{Tk.event_generate(Tk.focus, "<Paste>")}
m_edit.add :command, :label => "Find...", :command => proc{Tk.event_generate(root, "<OpenFindDialog>")}
root['menu'] = m
def launchFindDialog() Tk::messageBox :message => "I hope you find what you're looking for!" end
root.bind("<OpenFindDialog>") {launchFindDialog}
Tk.mainloop()
use Tkx;
my $mw = Tkx::widget->new(".");
$mw->new_ttk__entry()->g_grid();
$m = $mw->new_menu;
$m_edit = $m->new_menu;
$m->add_cascade(-menu => $m_edit, -label => "Edit");
$m_edit->add_command(-label => "Paste", -command => sub{Tkx::event_generate(Tkx::focus(), "<<Paste>>");});
$m_edit->add_command(-label => "Find...", -command => sub{$mw->g_event_generate("<<OpenFindDialog>>");});
$mw->configure(-menu => $m);
sub launchFindDialog {Tkx::tk___messageBox(-message => "I hope you find what you're looking for!");}
$mw->g_bind("<<OpenFindDialog>>", sub{launchFindDialog();});
Tkx::MainLoop();
When you generate a virtual event, you need to specify the widget that the event should be sent to.
We want the "Paste" event to be sent to the widget with the keyboard focus (usually indicated by a focus ring).
You can determine which widget has the keyboard focus using the focus
command. Try it out, choosing
the Paste item when the window is first opened (when there's no focus) and after clicking on the entry (making it the focus).
Notice the entry handles the <<Paste>>
event itself. There's no need for us to create an event binding.
The <<OpenFindDialog>>
event is sent to the root window, which is where we create an event binding.
If we had multiple toplevel windows, we'd send it to a specific window.
Tk predefines the following virtual events: <<Clear>>
, <<Copy>>
,
<<Cut>>
, <<Paste>>
, <<PasteSelection>>
,
<<PrevWindow>>
, <<Redo>>
, and <<Undo>>
.
For additional information, see the event
command reference.
Platform Menus
Each platform has a few menus in every menubar that are handled specially by Tk.
macOS
You've probably noticed that Tk on macOS supplies its own default menubar. It includes a menu named after the program being run (in this case, your programming language's shell, e.g., "Wish", "Python", etc.), a File menu, and standard Edit, Windows, and Help menus, all stocked with various menu items.
You can override this menubar in your own program, but to get the results you want, you'll need to follow some particular steps (in some cases, in a particular order).
Starting at Tk 8.5.13, the handling of special menus on macOS changed due to the underlying Tk code migrating from the obsolete Carbon API to Cocoa. If you're seeing duplicate menu names, missing items, things you didn't put there, etc., review this section carefully.
The first thing to know is that if you don't specify a menubar for a window (or its parent window, e.g., the root window), you'll end up with the default menubar Tk supplies, which unless you're just mucking around on your own, is almost certainly not what you want.
The Application Menu
Every menubar starts with the system-wide apple icon menu. To the right of that is a menu for the frontmost application.
It is always named after the binary being run.
When you attach a menubar to the window, if it does not already contain
a specially named .apple
menu (see below), Tk will provide its default application menu.
It includes an "About Tcl & Tk" item, followed by the
standard menu items: preferences, the services submenu, hide/show items, and quit. Again, you don't want this.
If you supply your own .apple
menu, when the menubar is attached to the window, Tk will add the
standard items (preferences and onward) onto the end of any items you have added. Perfect! Items you add after the menubar is
attached to the window will appear after the quit item, which, again, you don't want.
The application menu, which we're dealing with here, is distinct from the apple menu (the one with the apple icon, just to the left of the application menu). Despite that, we really mean the application menu, even though Tk still refers to it as the "apple" menu. This is a holdover from pre-OS X days when these sorts of items did go in the actual apple menu, and there was no separate application menu.
So, in other words, in your program, make sure you:
- Create a menubar for each window or the root window. Do not attach the menubar to the window yet!
- Add a menu to the menubar named
.apple
. It will be used as the application menu. - The menu will automatically be named the same as the application binary; if you want to change this, rename (or make a copy of) the binary used to run your script.
- Add the items you want to appear at the top of the application menu, i.e., an "About yourapp" item, followed by a separator.
- After doing all this, you can then attach the menubar to your window via the window's
menu
configuration option.
win = Toplevel(root)
menubar = Menu(win)
appmenu = Menu(menubar, name='apple')
menubar.add_cascade(menu=appmenu)
appmenu.add_command(label='About My Application')
appmenu.add_separator()
win['menu'] = menubar
While usually, Tkinter chooses a widget pathname for us, we've had to explicitly provide one (apple
)
using the name
option when creating the application menu.
toplevel .win
menu .win.menubar
.win.menubar add cascade -menu [menu .win.menubar.apple]
.win.menubar.apple add command -label "About My Application"
.win.menubar.apple add separator
.win configure -menu .win.menubar
The pathname of the application menu must be .apple
.
win = TkToplevel.new(root)
menubar = TkMenu.new(win)
appmenu = TkSysMenu_Apple.new(menubar)
menubar.add :cascade, :menu => appmenu
appmenu.add :command, :label => 'About My Application'
appmenu.add :separator
win['menu'] = menubar
The TkSysMenu_Apple
call ensures the menu is named .apple
internally.
$w = $mw->new_toplevel;
$m = $w->new_menu;
$appmenu = Tkx::widget->new(Tkx::menu($m->_mpath . ".apple"));
$m->add_cascade(-menu => $appmenu);
$appmenu->add_command(-label => "About My Application")
$appmenu->add_separator;
$w->configure(-menu => $m);
While usually, Tkx chooses a widget pathname for us, we've had to explicitly provide one (.apple
)
using the _mpath
option when creating the application menu.
Handling the Preferences Menu Item
As you've noticed, the application menu always includes a "Preferences..." menu item. This menu item should open a preferences dialog if your application has one. If not, this menu item should be disabled, which it is by default.
To hook up your preferences dialog, you'll need to define a Tcl procedure named ::tk::mac::ShowPreferences
.
It will be called when the Preferences menu item is chosen; if the procedure is not defined, the menu item will be disabled.
def showMyPreferencesDialog():
....
root.createcommand('tk::mac::ShowPreferences', showMyPreferencesDialog)
To hook up your preferences dialog, you'll need to define a Tcl procedure named ::tk::mac::ShowPreferences
.
This will be called when the Preferences menu item is chosen; if the procedure is not defined, the menu item will be disabled.
proc tk::mac::ShowPreferences {} {showMyPreferencesDialog}
::tk::mac::ShowPreferences
.
This will be called when the Preferences menu item is chosen; if the procedure is not defined, the menu item will be disabled.
Tk.ip_eval("proc ::tk::mac::ShowPreferences {} {#{Tk.install_cmd(proc{showMyPreferencesDialog})}}")
To hook up your preferences dialog, you'll need to define a Tcl procedure named ::tk::mac::ShowPreferences
.
This will be called when the Preferences menu item is chosen; if the procedure is not defined, the menu item will be disabled.
Tkx::proc('::tk::mac::ShowPreferences', '{args}', sub {showMyPreferencesDialog()});
Providing a Help Menu
Like the application menu, any help menu you add to your own menubar is treated specially on macOS. As with the application menu
that needed a special name (.apple
), the help menu must be given the name .help
. The help menu
should also be added before the menubar is attached to the window.
The help menu will include the standard macOS search box to search help, as well as an item named "yourapp Help."
As with the name of the application menu, this comes from your program's executable and
cannot be changed. Similar to how preferences dialogs are handled, to respond to this help item, you need to define a Tcl
procedure named ::tk::mac::ShowHelp
. If this procedure is not defined, it will not disable the menu item.
Instead, it will generate an error when the help item is chosen.
If you don't want to include help, don't add a help menu to the menubar, and none will be shown.
Unlike on X11 and earlier versions of Tk on macOS, the Help menu will not automatically be put at the end of the menubar, so ensure it is the last menu added.
You can also add other items to the help menu. These will appear after the application help item.
helpmenu = Menu(menubar, name='help')
menubar.add_cascade(menu=helpmenu, label='Help')
root.createcommand('tk::mac::ShowHelp', ...)
.win.menubar add cascade -menu [menu .win.menubar.help] -label Help
::tk::mac::ShowHelp {} {...}
helpmenu = TkSysMenu_Help.new(menubar)
menubar.add :cascade, :menu => helpmenu, :label => 'Help'
Tk.ip_eval("proc ::tk::mac::ShowHelp {} {#{Tk.install_cmd(proc{...})}}")
$helpmenu = Tkx::widget->new(Tkx::menu($m->_mpath . ".help"));
$m->add_cascade(-menu => $helpmenu);
Tkx::proc('::tk::mac::ShowHelp', '{args}', sub {...});
Providing a Window Menu
On macOS, a "Window" menu contains items like minimize, zoom, bring all to front, etc. It also includes a list of currently open windows. Before that list, other application-specific items are sometimes provided.
By providing a menu named .window
, this standard window menu will be added. Tk automatically keeps it in sync with all
your toplevel windows, without any extra code on your part. You can also add any application-specific commands to this menu.
These appear before the list of your windows.
windowmenu = Menu(menubar, name='window')
menubar.add_cascade(menu=windowmenu, label='Window')
.win.menubar add cascade -menu [menu .win.menubar.window] -label Window
class Tk::TkSysMenu_Window<Tk::Menu
include Tk::SystemMenu
SYSMENU_NAME = 'window'
end
windowmenu = Tk::TkSysMenu_Window.new(menubar)
menubar.add :cascade, :menu => windowmenu, :label => 'Window'
As of this writing, Ruby/Tk hadn't yet added the TkSysMenu_Window helper.
$windowmenu = Tkx::widget->new(Tkx::menu($m->_mpath . ".window"));
$m->add_cascade(-menu => $windowmenu);
Other Menu Handlers
You've seen how handling certain standard menu items required you to define Tcl callback procedures,
e.g., tk::mac::ShowPreferences
and tk::mac::ShowHelp
.
There are several other callbacks that you can define. For example, you might intercept the Quit menu item, prompting users to save their changes before quitting. Here is the complete list:
tk::mac::ShowPreferences
:- Called when the "Preferences..." menu item is selected.
tk::mac::ShowHelp
:- Called to display main online help for the application.
tk::mac::Quit
:- Called when the Quit menu item is selected, when a user is trying to shut down the system etc.
tk::mac::OnHide
:- Called when your application has been hidden.
tk::mac::OnShow
:- Called when your application is shown after being hidden.
tk::mac::OpenApplication
:- Called when your application is first opened.
tk::mac::ReopenApplication
:- Called when a user "reopens" your already-running application (e.g., clicks on it in the Dock)
tk::mac::OpenDocument
:- Called when the Finder wants the application to open one or more documents (e.g., that were dropped on it). The procedure is passed a list of pathnames of files to be opened.
tk::mac::PrintDocument
:- As with OpenDocument, but the documents should be printed rather than opened.
For additional information, see the tk_mac
command reference.
Windows
On Windows, each window has a "System" menu at the top left of the window frame, with a small icon for your application. It contains items like "Close", "Minimize", etc. In Tk, if you create a system menu, you can add new items below the standard items.
sysmenu = Menu(menubar, name='system')
menubar.add_cascade(menu=sysmenu)
While Tkinter usually chooses a widget pathname for us, we've had to explicitly provide one
with the name system
; this is the cue that Tk needs to recognize it as the system menu.
$m add cascade -menu [menu $m.system]
The pathname of the menu widget must be .system
.
sysmenu = TkSysMenu_System.new(menubar)
menubar.add :cascade, :menu => sysmenu
$system = Tkx::widget->new(Tkx::menu($m->_mpath . ".system"));
$m->add_cascade(-menu => $system);
The pathname of the menu must be explicitly provided, in this case with the name ".system".
X11
On X11, if you create a help menu, Tk ensures that it is always the last menu in the menubar.
menu_help = Menu(menubar, name='help')
menubar.add_cascade(menu=menu_help, label='Help')
While Tkinter usually chooses a widget pathname for us, we've had to explicitly provide one
with the name help
; this is the cue that Tk needs to recognize it as the help menu.
$m add cascade -label Help -menu [menu $m.help]
The pathname of the menu widget must be .help
.
help = TkSysMenu_Help.new(menubar)
menubar.add :cascade, :menu => help, :label => 'Help'
$help = Tkx::widget->new(Tkx::menu($m->_mpath . ".help"));
$m->add_cascade(-menu => $help);
The pathname of the menu must be explicitly provided, in this case with the name ".help".
Contextual Menus
Contextual menus ("popup" menus) are typically invoked by a right mouse button click on an object in the application. A menu pops up at the location of the mouse cursor. Users can then select an item from the menu (or click outside it to dismiss it without choosing any item).
To create a contextual menu, we'll use exactly the same commands we used to create menus in the menubar. Typically, we'd create one menu with several command items and potentially some cascade menu items and their associated menus.
To activate the menu, users will perform a contextual menu click. We'll have to create an event binding to capture that click. That, however, can mean different things on different platforms. On Windows and X11, this can be clicking the right mouse button (the third mouse button). On macOS, it can be either clicking the left (or only) button with the control key held down or right-clicking on a multi-button mouse. Unlike Windows and X11, macOS refers to this as the second mouse button, not the third, so that's the event we'll see in our program.
Most earlier programs that have used popup menus assumed it was only "button 3" they needed to worry about.
Besides capturing the correct contextual menu event, we also need to capture the mouse's location.
It turns out we need to do this relative to the entire screen (global coordinates) and not local to the window or
widget you clicked on (local coordinates). The %X
and %Y
substitutions in Tk's event binding system will capture
those for us.
The last step is telling the menu to pop up at the particular location via the post
method.
Here's an example of the whole process, using a popup menu on the application's main window.
from tkinter import *
root = Tk()
menu = Menu(root)
for i in ('One', 'Two', 'Three'):
menu.add_command(label=i)
if (root.tk.call('tk', 'windowingsystem')=='aqua'):
root.bind('<2>', lambda e: menu.post(e.x_root, e.y_root))
root.bind('<Control-1>', lambda e: menu.post(e.x_root, e.y_root))
else:
root.bind('<3>', lambda e: menu.post(e.x_root, e.y_root))
root.mainloop()
menu .menu
foreach i [list One Two Three] {.menu add command -label $i}
if {[tk windowingsystem]=="aqua"} {
bind . <2> "tk_popup .menu %X %Y"
bind . <Control-1> "tk_popup .menu %X %Y"
} else {
bind . <3> "tk_popup .menu %X %Y"
}
require 'tk'
root = TkRoot.new
menu = TkMenu.new(root)
%w(One Two Three).each {|i| menu.add :command, :label => i}
if Tk.windowingsystem == 'aqua'
root.bind '2', proc{|x,y| menu.popup(x,y)}, "%X %Y"
root.bind 'Control-1', proc{|x,y| menu.popup(x,y)}, "%X %Y"
else
root.bind '3', proc{|x,y| menu.popup(x,y)}, "%X %Y"
end
Tk.mainloop
use Tkx;
my $mw = Tkx::widget->new(".");
my $menu = $mw->new_menu();
foreach ("One", "Two", "Three") {$menu->add_command(-label => $_);}
if (Tkx::tk_windowingsystem() eq "aqua") {
$mw->g_bind("<2>", [sub {my($x,$y) = @_; $menu->g_tk___popup($x,$y)}, Tkx::Ev("%X", "%Y")] );
$mw->g_bind("<Control-1>", [sub {my($x,$y) = @_; $menu->g_tk___popup($x,$y)}, Tkx::Ev("%X", "%Y")]);
} else {
$mw->g_bind("<3>", [sub {my($x,$y) = @_; $menu->g_tk___popup($x,$y)}, Tkx::Ev("%X", "%Y")]);
}
Tkx::MainLoop();
Spotted a mistake? Couldn't find what you were looking for? Suggestions? Let me know!
If you've found this tutorial useful, please check out Modern Tkinter.