Version 41 of menu

Updated 2020-11-20 15:03:55 by oehhar

The documentation for menu and tk_menuSetFocus can be found at http://www.tcl.tk/man/tcl/TkCmd/menu.htm .

Separating the Menu Bar from the Main Window

On Win XP Classic at least, RS gets a nice groove below the menu with

pack [frame .sep -relief sunken -borderwidth 1 -height 2] -side top -fill x

How to add to the System menu

Courtesy KBK (Kevin Kenny) on comp.lang.tcl:

Chris Nelson wrote: I'm trying to add an item to the system menu of an NT-based Tcl application and nothing is happening. What am I doing wrong?

% menu .mb
.mb
% . configure -menu .mb
% .mb add command -label "file"
% menu .mb.system
.mb.system
% .mb.system add  command -label "foo"

This confused me for a while, too. You also need to do:

.mb add cascade -label System -menu .mb.system

Also, tearoffs look awful in the system menu. You probably want to do:

.mb.system configure -tearoff 0

NEM (19Jan04) -- In order to add items to the application/system menu under Mac OS X (where a lot of apps put an "About..." and "Preferences.." menu items, you seem to have to create a menu with the label "Apple" (and possibly with the a path ending in .apple, I'm not really sure which bit gets it to work:

menu .mb
. configure -menu .mb
menu .mb.apple -tearoff 0
.mb.apple add command -label "About My App.."
.mb add cascade -label "Apple" -menu .mb.apple

How to add console control to the System menu on Windows

KBK: (14 November 2000) -- After I posted this, I went to put it on the Wiki, and found that someone already had! (I guess the Wiki runs on Internet time, too!) Well, I can improve on my own work with this little convenience:


I've found it useful to be able to do 'console hide' and 'console show' in applications that are already deployed, and found that the system menu is one place to put the function that doesn't clutter up the user interface. (A View menu, if the app has one, might also be a logical choice.)

As soon as she saw it, my system tester said that the separate Show and Hide functions ought to be a checkbutton. I found that required a little bit more hacking, and this was the result:

  Show the code
    # The ''consoleAux'' namespace holds variables and procedures
    # that help manage the system console on Windows

    namespace eval consoleAux {
        namespace export setup;         # Initialization
        namespace export toggle;        # Command to map/unmap the
                                        # console on demand
        variable mapped;                # Flag == 1 iff the console
                                        # is currently mapped
    }
    #------------------------------------------------------------------
    #
    # consoleAux::setup --
    #
    #       Set up the system console control on Windows.
    # 
    # Parameters:
    #       None.
    #
    # Results:
    #       None.
    #
    # Side effects:
    #       Bindings are established so that the variable,
    #       '::consoleAux::mapped' is set to reflect the state
    #       of the console.
    #
    # Notes:
    #       Depends on undocumented internal API's of Tk and
    #       therefore may not work on future releases.
    #
    #------------------------------------------------------------------

    proc consoleAux::setup {} {

        # Make the console have a sensible title
        console title "Console: [tk appname]"
        console eval {

            # Determine whether the console has started in the
            # mapped state.

            if { [winfo ismapped .console] } {
                consoleinterp eval {
                    set ::consoleAux::mapped 1
                }
            } else {
                consoleinterp eval {
                    set ::consoleAux::mapped 0
                }
            }
            # Establish bindings to reflect the state of the
            # console in the 'mapped' variable.

            bind .console <Map> {
                consoleinterp eval {
                    set ::consoleAux::mapped 1
                }
            }
            bind .console <Unmap> {
                consoleinterp eval {
                    set ::consoleAux::mapped 0
                }
            }
        }
        return
    }
    #------------------------------------------------------------------
    #
    # consoleAux::toggle --
    #
    #       Change the 'mapped' state of the console in response
    #       to a checkbutton.
    #
    # Parameters:
    #       None.
    #
    # Results:
    #       None.
    #
    # Side effects:
    #       If the console is marked 'mapped', shows and raises it.
    #       Otherwise, hides it.
    #
    #------------------------------------------------------------------
    
    proc consoleAux::toggle {} {
        variable mapped
        if {$mapped} {
            console show
            console eval { raise . }
        } else {
            console hide
        }
        return
    }
    # Sample application that shows the use of the console in the
    # system menu.

    # First, make sure there is a system menu.

    menu .menubar
    . configure -menu .menubar

    .menubar add cascade -label File -menu .menubar.file -underline 0

    menu .menubar.file
    .menubar.file add command -label Exit -underline 1 -command exit

    if { [string equal $tcl_platform(platform) windows] } {
        .menubar add cascade -label System -menu .menubar.system

        menu .menubar.system -tearoff 0

        # Add the 'Show Console' item to the system menu

        ::consoleAux::setup
        .menubar.system add checkbutton \
            -label {Show Console} \
            -variable ::consoleAux::mapped \
            -command ::consoleAux::toggle
    }

Compound Popups and Menu Animation

CT: (26 May 2003) -- I seem to be having problems with compound popup entries under windows. I am using both image and text labels for menu entries in popups and am experiencing problems with the images. Firstly the images only half appear in the menu until you hover over the menu item and it highlights - then the image displays properly. Secondly images with transparency in them are even worse and seem to cause a kind of black smudge obscurring half the image and only correctly display when you hover over them. I am loading regular gifs created in gimp using the image create photo ... command. Problem seems to exist in 98 and XP and its driving me potty...arg! :) Any help would be warmly appreciated, thanks.

MAK (Hm, still 25 May 2003 here :P) regarding the images that only half-appear until you hover: this is a common annoyance with checkbutton menus as well. It has something to do with a somewhat long-standing incompatibility between Tk and Windows' animated menus/windows. If you turn off animated menus/windows then the problem goes away. It may help with your transparent images, too. How you do this depends on the version of Windows, I believe. Would be nice if this were fixed some day, though.

CT: Thanks Mak, this one has been bugging me for years and I could only manage to ignore it for a few months at a time. :)

Entry Widgets in Menus

MG Jan 26 2006 - I've seen menus in Windows (notably in MS Word) which contain entry widgets - when you're configurable a toolbar, you can right-click a button, and in the menu that pops up there's an entry-style widget where you can type in a name for the button. Is there any way to do this in Tk? And if not, is there ever likely to be? I'm not sure if it's a native function of Windows menus which Tk simply doesn't make use of, or whether it's a bit of clever trickery specific to MS Word.

DKF 24 Aug 2006: I think it's an MSOffice thing. It's not the first (or last) time that MS's Office people have been off doing something very odd behind the backs of the general Windows UI people. But as such, it's all done with lots of code that we don't provide access to.

Scrollable Menus

LV I have had occasion to have to dynamically generate menus. I have at times found cases where it would be useful to have some sort of scrollable menu - either with a scrollbar or just allowing one to use arrow keys on the keyboard to move the display up and down. In at least one case, I tried to create columns in the menu, but there were still more items than would fit on the screen.

Has anyone ever tried to figure out a way to do menus with some sort of scrolling?

Thanks.

RFox 1/16/2012 One of our local folks has done this by dynamically managing the set of items in the pop up menu and having up/down arrows at the top and bottom of the menu. Menu items bind to mouse motion events and if you move in the arrows the menu scrolls. I have a need for this myself and am about to take this apart to see if I can put it back into something usable (the implementation is uh.... physicist code let's just say).

RFox 1/19/2012 Ok.. here is a partial implementation as a snit widget adaptor. This works only for fairly static menus. I still need to override quite a few menu methods in order to make a complete implementation, but enough use cases are covered that it's a start that points the way. Note there is an assumption that you have a pair of arrow images (uparrow.gif, downarrow.gif) that are used to indicate there are more elements off the end of the menu...these images should be in the same directory as the widget is installed...you may need to adjust the scaling of those arrows depending on the actual images you grab/use.

  The code
 package require Tk
 package require snit

 package provide scrollingMenu 1.0


 namespace eval scrollingMenu {
    set dirname [file dirname [info script]]; # have to define this while sourceing.
 }


 ##
 # Provides a snit widgetadaptor that adapts a menu so that
 # it scrolls if the number of entries gets larger than the 
 # verticalscreen measure.
 #
 #  For edges of the menu that are off screen,
 #  an arrow graphic is placed at that end (up arrow at the top, down arrow at the bottom).
 #  Scrolling is accomplished by entering the scroller arrow with the mouse.  Scrolling continues
 #  at a settable rate until the mouse either leaves the scroller arrow or B1 is released (in 
 #  which case no menu command fires.
 #  
 #
 snit::widgetadaptor scrollingMenu  {

    # Arrow image handles if not null:

    typevariable uparrow ""
    typevariable downarrow ""


    variable items -array {};   # Initially empty array of menu contents.
    variable itemCount 0;       # Number of menu items.
    variable topIndex  0;       # Item index at the top of the menu (not counting the ^ if present.
    variable bottomIndex 0;     # Item index at the bottom of the menu (not counting the V if present.
    variable scrolling   0;     #  nonzero if the menu requires scrolling.
    variable timerId     -1;    # Scrolling timer id.

    # Our extended options:

    option -scrolltimer 20;     # ms betwee successive scrolls.

    # Unless overridden, all the methods and options are passed through to the underlying
    # menu.

    delegate option * to hull
    delegate method * to hull


    constructor args {
        installhull using menu

        $self configurelist $args
    }

    #-----------------------------------------------------------------------------
    # Public methods:
    #

    ##
    # Intercept the 'add' method so that we can see if it's necessary to 
    # The elements added to the menu are stored in the items array indexed by
    # item position.  Each element consists of a dict with the following values:
    # type - the entry type (e.g. command).
    # options - The set of options that differ from default values.
    # note that some options (e.g. -columnbreak) are not allowed in a scrollingMenu
    # item configuration because they make no sense.
    #
    # @param itemType - the type of menu entry to add.
    # @param args - Option/value pairs that configure then entry:
    #
    method add {itemType args} {

        # If we don't need to scroll just add the entry.
        # otherwise set up scrolling if it's not already set up.
        #


        if {[$hull yposition last] < ([winfo vrootheight .] - 100)} {
            $hull add $itemType {*}$args
            set bottomIndex $itemCount; # last one visible.

        } else {

            $self StartScrolling

        }
        # regardless store the item in the array:
        #

        set items($itemCount) [dict create type $itemType options $args]
        incr itemCount;
    }
    #-----------------------------------------------------------------------------
    #
    # Private methods:

    ##
    # This is called when it's time to setup scrolling for the menu.
    #  The topIndex, bottomIndex items will be displayed but we're going to put in
    #  up/down scrolling arrows as well.  Events get bound to those items to facilitate
    #  the scrolling and the scrolling flag is set true.  
    #  When scrolling gets turned on, top and bottomIndex are already correct.
    #
    method StartScrolling {} {

        # Already scrolling:

        if {$scrolling} {
            return
        }

        set scrolling 1

        $self LoadImages;       # if necessary, load the up/down arrows.

        # add the image items... these have no commands so they don't do anything..
        # however they do respond to mouse entry to initiate scrolling..

        $hull add command -image $downarrow


        bind $win <Enter>           [mymethod ScrollMenu]
        bind $win <ButtonRelease-1> [mymethod CancelScrollingTimer]
        bind $win <Leave>           [mymethod CancelScrollingTimer]


    }

    ##
    #  If images have not yet been loaded, load them and save the image
    #  handles in the uparrow/downarrow typevariables:
    #
    method LoadImages {} {


        if {[string length $uparrow] != 0} {
            return
        }


        set img [image create photo \
                     -file [file join $scrollingMenu::dirname uparrow.gif]]
        set uparrow [image create photo]
        $uparrow copy $img -subsample 40 40
        image delete $img


        set img [image create photo \
                     -file [file join $scrollingMenu::dirname downarrow.gif]]
        set downarrow [image create photo]
        $downarrow copy $img -subsample 40 40
        image delete $img



    }
    ## 
    # Called in response to an enter event:
    #  - If the scroll timer is not yet set we reschedule ourselves to go again later.
    #  - If we are in the last item and the 
    method ScrollMenu {} {

        set timerId [after $options(-scrolltimer) [mymethod ScrollMenu]]


        set activeItem [$hull index active]


        # If the active item is 0 and topIndex != 0 we need to scroll down and insert
        # the prior item into the menu.

        if {($activeItem == 0) && ($topIndex != 0)} {
            $self ScrollDown
        }

        # If the active item is the last one and the bottomIndex is not last element of the
        # list of menu items, Scroll up:


        if {($activeItem == [$hull index last]) 
            && ($bottomIndex < ($itemCount - 1)) }  {
            $self ScrollUp
        }
    }
    
    ##
    # Cancel the scrolling timer if it's set:
    #
    method CancelScrollingTimer {} {
        if {$timerId != -1} {
            after cancel $timerId
            set timerId -1
        }
    }
    ##
    # Scroll the menu up
    # - if necessary, add a scroll arrow at the top.
    # - deleting item 1 from the menu.
    # - inserting a new last-1 element in the menu from our array.
    # - if necessary, removing the scroll up arrow
    # - Somewhen adjust the top/bottom indices.
    #
    method ScrollUp {} {

        # If the top index is 0, we need an up arrow at the top:
        # and must delete an entry to keep the menu size constant.
        #
        if {$topIndex == 0} {
            $hull insert 0 command -image $uparrow
            $hull delete 1 
        }
        $hull delete 1;         # Drop the top functional item.

        # Add the next unseen item:

        incr topIndex
        incr bottomIndex
        set  newBottomItem $items($bottomIndex)
        set  menuIndex [expr {[$hull index last]}]

        set itemType [dict get $newBottomItem type]
        set itemOptions  [dict get $newBottomItem options]


        $hull insert $menuIndex $itemType {*}$itemOptions

        # If the new bottom item is the last one, remove the
        # bottom scroll arrow:

        if {$bottomIndex == ($itemCount -1)} {
            $hull delete last
        }
        # Ensure the last item remains active:

        $hull activate last

    }
    ##
    # Scroll the  menu down.  This means:
    #   - If necessesaary add a scroll arrow to the bottom.
    #   - Remove the next to the last mene entry.
    #   - adjust the top/bottom indices.
    #   - Add a new menu item at position 1.
    #   - If necessary kill the top arrow.
    # 
    method ScrollDown {} {

        # If the bottom index is the last element
        # we  must add a scrolling arrow to the bottom of the menu.

        if {$bottomIndex == ($itemCount - 1)} {
            $hull add command -image $downarrow
        }

        # Remove the next to last elementL:

        set lastItem [expr {[$hull index last]} -1]
        $hull delete $lastItem

        # Adjust the indices and push in a new item at the top of the menu:

        incr bottomIndex -1
        incr topIndex    -1
        set newItem     $items($topIndex)
        set itemType    [dict get $newItem type]
        set itemOptions     [dict get $newItem options]

        $hull insert 1 $itemType {*}$itemOptions

        # If topIndex is 0, kill the arrow:
        # And add yet another entry... if there is one:

        if {$topIndex == 0} {
            $hull delete 0
            if {$bottomIndex < ($itemCount -1)} {
                incr bottomIndex
                set newItem $items($bottomIndex)
                set itemType [dict get $newItem type]
                set itemOptions [dict get $newItem options]
                $hull insert [expr [$hull index last] -1] $itemType {*}$itemOptions
            }
        }
 }

Styling the Menu Bar

AMG: How does one go about adjusting the style of the menu bar?

I have a customer complaining that grayed-out text for disabled menu items in the menu bar is too faint and hard to read. I know for a fact that this is a problem with his monitor, but unfortunately I can't change that. Instead I have to look into adjusting the color of grayed-out menu items. We're talking X11, by the way.

AMG, update: Turns out the menubar [menu] as a whole has a -disabledforeground option, even though individual entries do not. That should be okay. I'll probably set it to black even though there's no visual cue that the items are disabled until you try to click on them. Customer request, you know.

bll 2018-7-30: You could always use a deep blue or deep red color (assuming a light colored theme).

In my project, I try to make sure all the colors are configurable by the user.

The default for the light colored themes could probably be quite a bit darker.

See also: Changing Widget Colors

radiobutton menu entries with empty -values use the -label value

HaO 2020-11-20: A value with the empty string uses the label as value. This is the documented behaviour, but sometimes quite surprising:

The following three lines all have the effect to set the value to the string "empty".

.m.c add radiobutton -label empty -variable choice -value ""
.m.c add radiobutton -label empty -variable choice
.m.c add radiobutton -label empty -variable choice -value empty

  radiobutton fun code

To have fun, you may try the following snippet. All radiobutton widgets behave differently for a value of the empty string:

menu .m
. configure -menu .m
menu .m.c
.m add cascade -labe choice -menu .m.c
set choice ""
.m.c add radiobutton -label empty -variable choice -value ""
.m.c add radiobutton -label A -variable choice -value "A"

pack [radiobutton .r1 -text empty -variable choice -value ""]
pack [radiobutton .r2 -text A -variable choice -value "A"]

pack [ttk::radiobutton .r3 -text empty -variable choice -value ""]
pack [ttk::radiobutton .r4 -text A -variable choice -value "A"]

If you wonder about the behaviour of the radiobutton widgets, try this modification:

pack [radiobutton .r1 -text empty -variable choice -value "" -tristatevalue I]
pack [radiobutton .r2 -text A -variable choice -value "A" -tristatevalue I]

See Also