воскресенье, 2 декабря 2012 г.

Ttk. Меню

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:
tk windowingsystem;  # will return x11, win32 or aqua
Tk.windowingsystem;  # will return x11, win32 or aqua
Tkx::tk_windowingsystem();   # will return x11, win32 or aqua
root.tk.call('tk', 'windowingsystem')    ; # will return x11, win32 or aqua
To the best of my knowledge, Tkinter does not provide a direct equivalent to this call. However, as you can see from the example, it is possible to execute a Tcl-based Tk command directly, using the ".tk.call()" function available on any Tkinter widget.
This is probably more useful than examining global variables like tcl_platform orRUBY_PLATFORM, and older checks that used these methods should be examined. While in the olden days there was a pretty good correlation between platform and windowing system, it's less true today. For example, if your platform is identified on Unix, that might mean Linux under X11, Mac OS X under Aqua, or even Mac OS X under X11.

Menubars

In this section we'll look at menubars: how to create them, what goes in them, how they're used, and so on.
Properly designing a menubar and its set of menus is beyond the scope of this tutorial, but a few pieces of advice. First, if you find yourself with a large number of 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, and consult the platform's human interface guidelines for full details about design, terminology, shortcuts, and much more. This is an area you will likely have to customize for each platform.

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 are things like the "Open..." command in a File menu, but also separators between other items, and items which open up their own submenu (so-called cascading menus). Each of these menu items also has 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 children (submenus) consisting of items like "File", "Edit" and so on. Each of those in turn is a menu containing different items, some of which might themselves contain submenus. 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.

Before you Start

It's important to put the following line in your application somewhere before you start creating menus.
option add *tearOff 0
TkOption.add '*tearOff', 0
Tkx::option_add("*tearOff", 0);
root.option_add('*tearOff', FALSE)
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 really don't want that there.
This is a throw-back to the Motif-style X11 that Tk's original look and feel were based on. Unless your application is designed to run on that old box collecting dust in the basement, you really don't want to see this, as its not a part of any modern user interface style.
And we'll all be looking forward to a version of Tk where this backwards compatibility is not preserved, and the default is not to have these tear-off menus.

Creating a Menubar

In Tk, menubars are associated with individual windows; each toplevel window can have at most one menubar. On Windows and X11, this is visually obvious, as the menus are part of each window, sitting just below the title bar at the top.
On Mac OS X 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 does have its own menubar; as you switch between windows, Tk will automatically take care of making sure that the correct menubar is displayed at the top of the screen. If you don't specify a menubar for a particular window, Tk will use 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 on Mac OS X all windows have a menubar, it's important to make sure you do 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 use when typing commands directly into the interpreter.
To actually create a menubar for a window, we first create a menu widget, and then use the window's "menu"configuration option to attach the menu widget to the window. As noted, the menu widget must be a child of the toplevel window.
toplevel .win
menu .win.menubar
.win configure -menu .win.menubar
win = TkToplevel(root)
menubar = TkMenu.new(win)
win['menu'] = menubar
$w = $mw->new_toplevel;
$m = $w->new_menu;
$w->configure(-menu => $m);
win = Toplevel(root)
menubar = Menu(win)
win['menu'] = menubar
This is truly ancient history, but menubars used to be done by creating a frame widget containing the menu items, and packing it into the top of the window like you would 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 want to create a menu widget for each menu that will go in the menubar (each one a child of the menubar), and then add them all to the menubar.
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");
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')

Adding Menu Items

Now that we have a couple of menus in our menubar, it's probably a good time to add a few items to each menu. Remember that menu items are part of the menu itself, so we thankfully don't have to go and create another menu widget for each one.
$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()});
menu_file.add_command(label='New', command=newFile)
menu_file.add_command(label='Open...', command=openFile)
menu_file.add_command(label='Close', command=closeFile)
On Mac OS X, the ellipsis ("...") is actually a special character, which is more tightly spaced than three periods in a row. Tk takes care of substituting this character for you automatically.
So adding menu items to a menu is essentially the same as adding a submenu, but rather than adding a menu item of type "cascade", we're adding one of type "command".
Each menu item has associated with it a number of configuration options, in the same way widgets do, though each menu item type has a different set of relevant options. Cascade menu items have a "menu" option used to specify the submenu, command menu items have a "command" option used to specify the command to invoke when the item is selected, and both have a "label" option to specify the text to display for the item.
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 a menu using the "delete index" method.

Types of Menu Items

We've already seen "command" menu items, which are the common menu items that when selected will invoke a command.
We've also seen the use of "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, in exactly the same way.
A third type of menu item is the "separator", which produces the dividing line you often see between different sets of menu items.
$m.file add separator
file.add :separator
$file->add_separator
menu_file.add_separator()
Finally, there are also "checkbutton" and "radiobutton" menu items, which behave analogously to checkbutton and radiobutton widgets. These menu items have a variable associated with them, and depending on the value of that variable, will display an indicator (i.e. a checkmark or a selected radiobutton) next to the item's label.
$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);
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)
When the user selects a checkbutton item that is not already checked, it will set the associated variable to the value in"onvalue", while selecting a 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 changes in the associated variable from within other parts of your program.
As with command items, checkbutton and radiobutton menu items do accept a "command" configuration option, that will be invoked when the menu item is selected; the associated variable, and hence the menu item's state, is updated before the callback is invoked.
Radiobutton menu items are not part of the Windows or Mac OS X human interface guidelines, so on those platforms the indicator next to the item's label is a checkmark, as it would be for a checkbutton item. The semantics still work though; it's a good way to select between multiple items, since the display will show one of the items selected (checked).

Accelerator Keys

The "accelerator" option is used to indicate the menu accelerator that should be associated with this menu. This does not actually create the accelerator, but only displays what it is next to the menu item. You still need to create a 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, normally the event bindings you create will 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 Mac OS X it is the "Command" key, on Windows and X11 it is usually the "Control" key). Example 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 Mac OS X, those modifiers will be automatically mapped to the different modifier icons that appear in menus.

More on Item Options

There are a few more common options for menu items.

Underline

While 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. If you want to add one of these to a menu item, you can use the "underline"configuration option for the item. The value of this option should be the index of the character you'd like underlined (from 0 to the length of the string - 1).

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, you can 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".

State

It is also possible to disable a menu, so that the user cannot select it. This can be done via the "state" option, setting it to a value of "disabled", or a value of "normal" to reenable the item.

Querying and Changing Item Options

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. Normally, this is a number (0..n-1) indicating the item's position in the menu, but you can also specify the label of the menu item (or in fact, a "glob-style" pattern to match against the item's label).
puts [$m.file entrycget 0 -label]; # get label of top entry in menu
$m.file entryconfigure Close -state disabled; # change an entry
puts [$m.file entryconfigure 0]; # print info on all options for an item
puts( file.entrycget 0, :state ); # get label of top entry in menu
file.entryconfigure 'Close', :state => 'disabled'; # change an entry
puts( file.entryconfiginfo 0 ); # print info on all optinos for an item
print $file->entrycget(0, -label); # get label of top entry in menu
$file->entryconfigure("Close", -state => "disabled"); # change an entry
print $file->entryconfigure(0); # print info on all options for an item
print( menu_file.entrycget(0, 'label') )
menu_file.entryconfigure('Close', state=DISABLED)
print( menu_file.entryconfigure(0) )

Platform Menus

Each platform has a few menus that are handled specially by Tk.

Mac OS X

On Mac OS X, there are two menus that are treated specially. The application menu is the second menu in the menubar, titled with your application name. It will have any menu items you add to it, then following that, the standard application menu items (preferences, services, hide/show, and quit) are automatically added by Tk. The second special menu is the help menu, which is always placed as the last menu on the menubar. Again, along with the items you add, Tk will add others expected by the system (nothing on Tiger, a search box on Leopard).
$m add cascade -menu [menu $m.apple]
$m add cascade -menu [menu $m.help]
The pathnames of these menu widgets must be ".apple" and ".help", as this is what causes the menus to be treated specially by Tk.
apple = TkSysMenu_Apple.new(menubar)
menubar.add :cascade, :menu => apple
help = TkSysMenu_Help.new(menubar)
menubar.add :cascade, :menu => help
$apple = Tkx::widget->new(Tkx::menu($m->_mpath . ".apple"));
$help = Tkx::widget->new(Tkx::menu($m->_mpath . ".help"));
$m->add_cascade(-menu => $apple);
$m->add_cascade(-menu => $help);
Tk's menu code recognizes menu widgets with a pathname of ".apple" or ".help" and treats them specially. Because of this, we need to explicitly provide the pathname when creating these widgets, rather than letting Tkx choose a name for us.
apple = Menu(menubar, name='apple')
help = Menu(menubar, name='help')
menubar.add_cascade(menu=apple)
menubar.add_cascade(menu=help)
While normally Tkinter chooses a widget path name for us, here we've had to explicitly provide one using the 'name' option. Naming the menus 'apple' and 'help' are the cues that Tk needs to recognize them as special.
The application menu, which is the one 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 do really mean the application menu, in Tk it is still referred to 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.
It's normal practice to add an "About appname" command item to the Application menu, followed by a separator item. When invoked this should display your application's about box.
Each application should respond to the "Preferences" item in the Application menu. To do so, 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.
I don't believe that Ruby currently has a way to deal with the "Preferences" item in the Application menu; the underlying Tk API.
Each application should respond to the "Preferences" item in the Application menu. To do so, 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. To do this, use code like:
Tkx::proc('::tk::mac::ShowPreferences', '{args}', sub {showPrefs()});
PYTHONTODO

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 that will appear below the standard items.
$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".
sysmenu = Menu(menubar, name='system')
menubar.add_cascade(menu=sysmenu)
While normally Tkinter will choose a widget path name for us, here 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.

X11

On X11, if you create a help menu, Tk will ensure that it is always the last menu in the menubar.
$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".
menu_help = Menu(menubar, name='help')
menubar.add_cascade(menu=menu_help, label='Help')
The widget pathname of the menu must be explicitly provided, in this case with the name"help". This can be specified for any Tkinter widget using the 'name' option when creating the widget.

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, and the user can select from one of the items in the menu (or click outside the menu to dismiss it without choosing any item).
To create a contextual menu, you'll use exactly the same commands you did to create menus in the menubar. Typically, you'll create one menu with several command items in it, and potentially some cascade menu items and their associated menus.
To activate the menu, the user will use a contextual menu click, which you will have to bind to. That however, can mean different things on different platforms. On Windows and X11, this is the right mouse button being clicked (the third mouse button). On Mac OS X, this is either a click of the left (or only) button with the control key held down, or a right click on a multi-button mouse. Unlike Windows and X11, Mac OS X refers to this as the second mouse button, not the third, so that's the event you'll see in your 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, you'll also need to capture the location the mouse was clicked. It turns out you 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 you.
The last step is simply to tell the menu to popup at the particular location. Here's an example of the whole process, using a popup menu on the application's main window.
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()=="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();
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))