More Widgets

This chapter introduces several more widgets: listbox, scrollbar, text, scale, spinbox, and progressbar. Some of these are starting to be a bit more powerful than the basic ones we looked at before. Here we'll also see a few instances of using the classic Tk widgets in cases where there isn't (or there isn't a need for) a themed counterpart.

Listbox


screen shot
Listbox widgets.

A listbox widget displays a list of single-line text items, potentially lengthy, and allows users to browse through the list, selecting one or more items.
 
Listboxes are part of the classic Tk widgets; there is not presently a listbox in the themed Tk widget set.


Tk's treeview widget (which is themed) can also be used as a listbox (a one-level deep tree), allowing you to use icons and styles with the list. It's also likely that a multi-column (table) list widget will make it into Tk at some point, whether based on treeview or one of the available extensions.

Listboxes are created using the Listbox class. A height configuration option can specify the number of lines the listbox will display at a time without scrolling:

l = Listbox(parent, height=10)

Listboxes are created using the tk::listbox command. A height configuration option can specify the number of lines the listbox will display at a time without scrolling:

tk::listbox .l -height 10

Listboxes are created using the TkListbox class. A height configuration option can specify the number of lines the listbox will display at a time without scrolling:

l = TkListbox.new(parent) {height 10}

Listboxes are created using the new_tk__listbox method, a.k.a. Tkx::tk__listbox. A height configuration option can specify the number of lines the listbox will display at a time without scrolling

$parent->new_tk__listbox(-height => 10);

Populating the listbox items

There's an easy way and a hard way to populate and manage all the items in the listbox.

Here's the easy way. Each listbox has a listvariable configuration option, which allows you to link a variable (which must hold a list) to the listbox. Each element of this list is a string representing one item in the listbox. To add, remove, or rearrange items in the listbox, you can simply modify this variable as you would any other list. Similarly, to find out, e.g., which item is on the third line of the listbox, just look at the third element of the list variable.

It's actually not quite that easy. Tkinter doesn't allow you to link regular Python lists to a listbox. As we saw with widgets like entry, we need to use a StringVar as an intermediary. It provides a mapping between Python's lists and a string representation that the underlying Tk widgets can use. It also means that anytime we change the list, we need to update the StringVar.
choices = ["apple", "orange", "banana"]
choicesvar = StringVar(value=choices)
l = Listbox(parent, listvariable=choicesvar)
...
choices.append("peach")
choicesvar.set(choices)

Unfortunately, you can't pass a reference to a Perl list for this listvariable parameter. It actually requires a Tcl formatted list, which is a string with elements separated by spaces, and braces around all but the simplest elements. So, you'll need to do the conversion yourself. There's a simple one-liner in the upcoming example that handles converting fairly simple lists, but for unknown data you'll need something more robust.

The older, harder way is to use a set of methods that are part of the listbox widget itself. They operate on the (internal) list of items maintained by the widget:

  • The insert idx item ?item...? method is used to add one or more items to the list; idx is a 0-based index indicating the position of the item before which the item(s) should be added; specify end to put the new items at the end of the list.
  • Use the delete first ?last? method to delete one or more items from the list; first and last are indices as per the insert method.
  • Use the get first ?last? method to deselect either a single item or any within the range of indices specified. To select an item or all items in a range, use the selection_set first ?last? method. Both of these will not touch the selection of any items outside the range specified.

    If you change the selection, you should also ensure that the newly selected item is visible (i.e., it is not scrolled out of view). To do this, use the see method.

    lbox.selection_set(idx)
    lbox.see(idx)
    $lbox selection set $idx
    $lbox see $idx
    $lbox.selection_set(idx)
    $lbox.see(idx)
    $lbox->selection_set($idx);
    $lbox->see($idx);

    When a user changes the selection, a <<ListboxSelect>> virtual event is generated. You can bind to this to take any action you need. Depending on your application, you may also want to bind to a double-click <Double-1> event and use it to invoke an action with the currently selected item.

    lbox.bind("<<ListboxSelect>>", lambda e: updateDetails(lbox.curselection()))
    lbox.bind("<Double-1>", lambda e: invokeAction(lbox.curselection()))
    bind $lbox <<ListboxSelect>> {updateDetails [$lbox curselection]}
    bind $lbox <Double-1> {invokeAction [$lbox curselection]}
    $lbox.bind('<ListboxSelect>', proc{updateDetails($lbox.curselection)})
    $lbox.bind('Double-1', proc{invokeAction($lbox.curselection)})
    $lbox->g_bind(<<ListboxSelect>>", sub {updateDetails($lbox->curselection)})'
    $lbox->g_bind(<Double-1>", sub {invokeAction($lbox->curselection)});

    Styling the list

    Like most of the "classic" Tk widgets, you have immense flexibility in modifying the appearance of a listbox. As described in the reference manual, you can modify the font the listbox items are displayed in, the foreground (text) and background colors for items in their normal state, when selected, when the widget is disabled, etc. There is also an itemconfigure method that allows you to change the foreground and background colors of individual items.

    As is often the case, restraint is useful. Generally, the default values will be entirely suitable and a good match for platform conventions. In the example we'll get to momentarily, we'll show how restrained use of these options can be put to good effect, in this case displaying alternate lines of the listbox in slightly different colors.

    Keeping extra item data

    The listvariable (or the internal list, if you're managing things the old way) holds the strings that will be displayed in the listbox. It's often the case, though, that each string you're displaying is associated with some other data item. This might be an internal object meaningful to your program but not meant to be displayed to users. In other words, what you're really interested in is not so much the string displayed in the listbox but the associated data item. For example, a listbox may display a list of names to users, but your program is really interested in the underlying user object (or id number) for each one, not the particular name.

    How can we associate this underlying value with the name that is displayed? Unfortunately, the listbox widget itself doesn't offer any facilities, so it's something we'll have to manage separately. There are a couple of obvious approaches. First, if the displayed strings are guaranteed unique, you could use a dictionary to map each name to its associated underlying object. This wouldn't work well for peoples' names, where duplicates are possible, but could work for countries, which are unique.

    Another approach is to keep a second list parallel to the list of strings displayed in the listbox. This second list will hold the underlying object associated with each item that is displayed. So the first item in the displayed strings list corresponds to the first item in the underlying objects list, the second to the second, etc. Any changes that you make in one list (insert, delete, reorder), you must make in the other. You can then easily map from the displayed list item to the underlying object based on their position in the list.

    Example

    Here is a silly example showing several of these listbox techniques. We'll display a list of countries and allow selecting only a single country at a time. When we do, a status bar displays the population of the country. You can press a button to send one of several gifts to the selected country's head of state (well, not really, but use your imagination). Sending a gift can also be triggered by double-clicking the list or hitting the Return key.

    Behind the scenes, we maintain two lists in parallel. The first is a list of two-letter country codes. The other is the corresponding name for each country that we will display in the listbox. We also have a dictionary that contains the population of each country, indexed by its two-letter country code.

    screen shot
    Country selector listbox example.

    from tkinter import *
    from tkinter import ttk
    root = Tk()
    
    # Initialize our country "databases":
    #  - the list of country codes (a subset anyway)
    #  - parallel list of country names, same order as the country codes
    #  - a hash table mapping country code to population
    countrycodes = ('ar', 'au', 'be', 'br', 'ca', 'cn', 'dk', 'fi', 'fr', 
            'gr', 'in', 'it', 'jp', 'mx', 'nl', 'no', 'es', 'se', 'ch')
    countrynames = ('Argentina', 'Australia', 'Belgium', 'Brazil', 
            'Canada', 'China', 'Denmark', 'Finland', 'France', 'Greece', 
            'India', 'Italy', 'Japan', 'Mexico', 'Netherlands', 'Norway', 
            'Spain', 'Sweden', 'Switzerland')
    cnames = StringVar(value=countrynames)
    populations = {'ar':41000000, 'au':21179211, 'be':10584534,
            'br':185971537, 'ca':33148682, 'cn':1323128240, 'dk':5457415,
            'fi':5302000, 'fr':64102140, 'gr':11147000, 'in':1131043000, 
            'it':59206382, 'jp':127718000, 'mx':106535000, 'nl':16402414,
            'no':4738085, 'es':45116894, 'se':9174082, 'ch':7508700}
    
    # Names of the gifts we can send
    gifts = { 'card':'Greeting card', 'flowers':'Flowers', 'nastygram':'Nastygram'}
    
    # State variables
    gift = StringVar()
    sentmsg = StringVar()
    statusmsg = StringVar()
    
    # Called when the selection in the listbox changes; figure out
    # which country is currently selected, and then lookup its country
    # code, and from that, its population.  Update the status message
    # with the new population.  As well, clear the message about the
    # gift being sent, so it doesn't stick around after we start doing
    # other things.
    def showPopulation(*args):
        idxs = lbox.curselection()
        if len(idxs)==1:
            idx = int(idxs[0])
            code = countrycodes[idx]
            name = countrynames[idx]
            popn = populations[code]
            statusmsg.set(f"The population of {name} ({code}) is {popn}")
        sentmsg.set('')
    
    # Called when the user double clicks an item in the listbox, presses
    # the "Send Gift" button, or presses the Return key.  In case the selected
    # item is scrolled out of view, make sure it is visible.
    #
    # Figure out which country is selected, which gift is selected with the 
    # radiobuttons, "send the gift", and provide feedback that it was sent.
    def sendGift(*args):
        idxs = lbox.curselection()
        if len(idxs)==1:
            idx = int(idxs[0])
            lbox.see(idx)
            name = countrynames[idx]
            # Gift sending left as an exercise to the reader
            sentmsg.set(f"Sent {gifts[gift.get()]} to leader of {name}")
    
    # Create and grid the outer content frame
    c = ttk.Frame(root, padding=(5, 5, 12, 0))
    c.grid(column=0, row=0, sticky=(N,W,E,S))
    root.grid_columnconfigure(0, weight=1)
    root.grid_rowconfigure(0,weight=1)
    
    # Create the different widgets; note the variables that many
    # of them are bound to, as well as the button callback.
    # We're using the StringVar() 'cnames', constructed from 'countrynames'
    lbox = Listbox(c, listvariable=cnames, height=5)
    lbl = ttk.Label(c, text="Send to country's leader:")
    g1 = ttk.Radiobutton(c, text=gifts['card'], variable=gift, value='card')
    g2 = ttk.Radiobutton(c, text=gifts['flowers'], variable=gift, value='flowers')
    g3 = ttk.Radiobutton(c, text=gifts['nastygram'], variable=gift, value='nastygram')
    send = ttk.Button(c, text='Send Gift', command=sendGift, default='active')
    sentlbl = ttk.Label(c, textvariable=sentmsg, anchor='center')
    status = ttk.Label(c, textvariable=statusmsg, anchor=W)
    
    # Grid all the widgets
    lbox.grid(column=0, row=0, rowspan=6, sticky=(N,S,E,W))
    lbl.grid(column=1, row=0, padx=10, pady=5)
    g1.grid(column=1, row=1, sticky=W, padx=20)
    g2.grid(column=1, row=2, sticky=W, padx=20)
    g3.grid(column=1, row=3, sticky=W, padx=20)
    send.grid(column=2, row=4, sticky=E)
    sentlbl.grid(column=1, row=5, columnspan=2, sticky=N, pady=5, padx=5)
    status.grid(column=0, row=6, columnspan=2, sticky=(W,E))
    c.grid_columnconfigure(0, weight=1)
    c.grid_rowconfigure(5, weight=1)
    
    # Set event bindings for when the selection in the listbox changes,
    # when the user double clicks the list, and when they hit the Return key
    lbox.bind('<<ListboxSelect>>', showPopulation)
    lbox.bind('<Double-1>', sendGift)
    root.bind('<Return>', sendGift)
    
    # Colorize alternating lines of the listbox
    for i in range(0,len(countrynames),2):
        lbox.itemconfigure(i, background='#f0f0ff')
    
    # Set the starting state of the interface, including selecting the
    # default gift to send, and clearing the messages.  Select the first
    # country in the list; because the <<ListboxSelect>> event is only
    # fired when users makes a change, we explicitly call showPopulation.
    gift.set('card')
    sentmsg.set('')
    statusmsg.set('')
    lbox.selection_set(0)
    showPopulation()
    
    root.mainloop()
    # Initialize our country "databases":
    #  - the list of country codes (a subset anyway)
    #  - parallel list of country names, same order as the country codes
    #  - a hash table mapping country code to population
    set countrycodes [list ar au be br ca cn dk fi fr gr in it jp mx nl no es se ch]
    set countrynames [list Argentina Australia Belgium Brazil Canada China Denmark \
            Finland France Greece India Italy Japan Mexico Netherlands Norway Spain \
            Sweden Switzerland]
    array set populations [list ar 41000000 au 21179211 be 10584534 br 185971537 \
            ca 33148682 cn 1323128240 dk 5457415 fi 5302000 fr 64102140 gr 11147000 \
            in 1131043000 it 59206382 jp 127718000 mx 106535000 nl 16402414 \
            no 4738085 es 45116894 se 9174082 ch 7508700]
    
    # Names of the gifts we can send
    array set gifts [list card "Greeting card" flowers "Flowers" nastygram "Nastygram"]
    
    # Create and grid the outer content frame
    grid [ttk::frame .c -padding "5 5 12 0"] -column 0 -row 0 -sticky nwes
    grid columnconfigure . 0 -weight 1; grid rowconfigure . 0 -weight 1
    
    # Create the different widgets; note the variables that many
    # of them are bound to, as well as the button callback.
    # The listbox is the only widget we'll need to refer to directly
    # later in our program, so for convenience we'll assign it to a variable.
    set ::lbox [tk::listbox .c.countries -listvariable countrynames -height 5]
    ttk::label .c.lbl -text "Send to country's leader:"
    ttk::radiobutton .c.g1 -text $gifts(card) -variable gift -value card
    ttk::radiobutton .c.g2 -text $gifts(flowers) -variable gift -value flowers
    ttk::radiobutton .c.g3 -text $gifts(nastygram) -variable gift -value nastygram
    ttk::button .c.send -text "Send Gift" -command {sendGift} -default active
    ttk::label .c.sentlbl -textvariable sentmsg -anchor center
    ttk::label .c.status -textvariable statusmsg -anchor w
    
    # Grid all the widgets
    grid .c.countries -column 0 -row 0 -rowspan 6 -sticky nsew
    grid .c.lbl       -column 1 -row 0 -padx 10 -pady 5
    grid .c.g1        -column 1 -row 1 -sticky w -padx 20
    grid .c.g2        -column 1 -row 2 -sticky w -padx 20
    grid .c.g3        -column 1 -row 3 -sticky w -padx 20
    grid .c.send      -column 2 -row 4 -sticky e
    grid .c.sentlbl   -column 1 -row 5 -columnspan 2 -sticky n -pady 5 -padx 5
    grid .c.status    -column 0 -row 6 -columnspan 2 -sticky we
    grid columnconfigure .c  0 -weight 1; grid rowconfigure .c 5 -weight 1
    
    # Set event bindings for when the selection in the listbox changes,
    # when the user double clicks the list, and when they hit the Return key
    bind $::lbox <<ListboxSelect>> "showPopulation"
    bind $::lbox <Double-1> "sendGift"
    bind . <Return> "sendGift"
    
    # Called when the selection in the listbox changes; figure out
    # which country is currently selected, and then lookup its country
    # code, and from that, its population.  Update the status message
    # with the new population.  As well, clear the message about the
    # gift being sent, so it doesn't stick around after we start doing
    # other things.
    proc showPopulation {} {
        set idx [$::lbox curselection]
        if {[llength $idx]==1} {
            set code [lindex $::countrycodes $idx]
            set name [lindex $::countrynames $idx]
            set popn $::populations($code)
            set ::statusmsg "The population of $name ($code) is $popn"
        }
        set ::sentmsg ""
    }
    
    # Called when the user double clicks an item in the listbox, presses
    # the "Send Gift" button, or presses the Return key.  In case the selected
    # item is scrolled out of view, make sure it is visible.
    #
    # Figure out which country is selected, which gift is selected with the 
    # radiobuttons, "send the gift", and provide feedback that it was sent.
    proc sendGift {} {
        set idx [$::lbox curselection]
        if {[llength $idx]==1} {
            $::lbox see $idx
            set name [lindex $::countrynames $idx]
            # Gift sending left as an exercise to the reader
            set ::sentmsg "Sent $::gifts($::gift) to leader of $name"
        }     
    }
    
    # Colorize alternating lines of the listbox
    for {set i 0} {$i<[llength $countrynames]} {incr i 2} {
        $::lbox itemconfigure $i -background #f0f0ff
    }
    
    # Set the starting state of the interface, including selecting the
    # default gift to send, and clearing the messages.  Select the first
    # country in the list; because the <<ListboxSelect>> event is only
    # fired when users makes a change, we explicitly call showPopulation.
    set gift card
    set sentmsg ""
    set statusmsg ""
    $::lbox selection set 0
    showPopulation
    require 'tkextlib/tile'
    
    # Initialize our country "databases":
    #  - the list of country codes (a subset anyway)
    #  - parallel list of country names, same order as the country codes
    #  - a hash table mapping country code to population
    $countrycodes = %w{ ar au be br ca cn dk fi fr gr in it jp mx nl no es se ch }
    $countrynames = %w{ Argentina Australia Belgium Brazil Canada China Denmark 
            Finland France Greece India Italy Japan Mexico Netherlands Norway Spain  
            Sweden Switzerland}
    $populations = { 'ar' => 41000000, 'au' => 21179211, 'be' => 10584534, 'br' => 185971537,  
            'ca' => 33148682, 'cn' => 1323128240, 'dk' => 5457415, 'fi' => 5302000, 
            'fr' => 64102140, 'gr' => 11147000, 'in' => 1131043000, 'it' => 59206382,
            'jp' => 127718000, 'mx' => 106535000, 'nl' => 16402414, 'no' => 4738085, 
            'es' => 45116894, 'se' => 9174082, 'ch' => 7508700}
    
    # Names of the gifts we can send
    $gifts = { 'card' => "Greeting card", 'flowers' => "Flowers", 'nastygram' => "Nastygram" }
    
    # Create and initialize the linked variables we'll need in the interface
    $gift = TkVariable.new( "card" )
    $names = TkVariable.new ( $countrynames )
    $sent = TkVariable.new ( "" )
    $status = TkVariable.new ( "" )
    
    # Create and grid the outer content frame
    root = TkRoot.new
    content = Tk::Tile::Frame.new(root) {padding "5 5 12 0"}.grid :column => 0, :row => 0, :sticky => "nwes"
    TkGrid.columnconfigure root, 0, :weight => 1
    TkGrid.rowconfigure root, 0, :weight => 1
    
    # Create the different widgets; note the variables that many
    # of them are bound to, as well as the button callback.
    $countries = TkListbox.new(content) {listvariable $names; height 5}
    sendlbl = Tk::Tile::Label.new(content) {text "Send to country's leader:"}
    gift1 = Tk::Tile::Radiobutton.new(content) {text $gifts['card']; variable $gift; value 'card'}
    gift2 = Tk::Tile::Radiobutton.new(content) {text $gifts['flowers']; variable $gift; value 'floweres'}
    gift3 = Tk::Tile::Radiobutton.new(content) {text $gifts['nastygram']; variable $gift; value 'nastygram'}
    send = Tk::Tile::Button.new(content) {text "Send Gift"; command "sendGift"; default "active"}
    sentlbl = Tk::Tile::Label.new(content) {textvariable $sent; anchor "center"}
    statuslbl = Tk::Tile::Label.new(content) {textvariable $status; anchor "w"}
    
    # Grid all the widgets
    $countries.grid :column => 0, :row => 0, :rowspan => 6, :sticky => 'nsew'
    sendlbl.grid    :column => 1, :row => 0, :padx => 10, :pady => 5
    gift1.grid      :column => 1, :row => 1, :sticky => 'w', :padx => 20
    gift2.grid      :column => 1, :row => 2, :sticky => 'w', :padx => 20
    gift3.grid      :column => 1, :row => 3, :sticky => 'w', :padx => 20
    send.grid       :column => 2, :row => 4, :sticky => 'e'
    sentlbl.grid    :column => 1, :row => 5, :columnspan => 2, :sticky => 'n', :pady => 5, :padx => 5
    statuslbl.grid  :column => 0, :row => 6, :columnspan => 2, :sticky => 'we'
    
    TkGrid.columnconfigure content, 0, :weight => 1
    TkGrid.rowconfigure content, 5, :weight => 1
    
    # Set event bindings for when the selection in the listbox changes,
    # when the user double clicks the list, and when they hit the Return key
    $countries.bind '<ListboxSelect>', proc{showPopulation}
    $countries.bind 'Double-1', proc{sendGift}
    root.bind 'Return', proc{sendGift}
    
    # Called when the selection in the listbox changes; figure out
    # which country is currently selected, and then lookup its country
    # code, and from that, its population.  Update the status message
    # with the new population.  As well, clear the message about the
    # gift being sent, so it doesn't stick around after we start doing
    # other things.
    def showPopulation
        idx = $countries.curselection
        if idx.length==1
            idx = idx[0]
            code = $countrycodes[idx]
            name = $countrynames[idx]
            popn = $populations[code]
            $status.value = "The population of #{name} (#{code}) is #{popn}"
        end
        $sent.value = ""
    end
    
    # Called when the user double clicks an item in the listbox, presses
    # the "Send Gift" button, or presses the Return key.  In case the selected
    # item is scrolled out of view, make sure it is visible.
    #
    # Figure out which country is selected, which gift is selected with the 
    # radiobuttons, "send the gift", and provide feedback that it was sent.
    def sendGift 
        idx = $countries.curselection
        if idx.length==1
            idx = idx[0]    
            $countries.see idx
            name = $countrynames[idx]
            # Gift sending left as an exercise to the reader
            $sent.value = "Sent #{$gifts[$gift.value]} to leader of #{name}"
        end
    end
    
    # Colorize alternating lines of the listbox
    0.step($countrycodes.length-1, 2) {|i| $countries.itemconfigure i, :background, "#f0f0ff"}
    
    # Select the first country in the list; because the <<ListboxSelect>> event is only
    # fired when users makes a change, we explicitly call showPopulation.
    $countries.selection_set 0
    showPopulation
    
    Tk.mainloop
    use Tkx;
    
    # Initialize our country "databases":
    #  - the list of country codes (a subset anyway)
    #  - parallel list of country names, same order as the country codes
    #  - a hash table mapping country code to population
    @countrycodes = ("ar", "au", "be", "br", "ca", "cn", "dk", "fi", "fr", "gr", "in", "it", "jp", "mx", 
                    "nl", "no", "es", "se", "ch");
    @countrynames = ("Argentina", "Australia", "Belgium", "Brazil", "Canada", "China", "Denmark", 
            "Finland", "France", "Greece", "India", "Italy", "Japan", "Mexico", "Netherlands", "Norway", "Spain", 
            "Sweden", "Switzerland");
    %populations = ("ar" => 41000000, "au" => 21179211, "be" => 10584534, "br" => 185971537, 
            "ca" => 33148682, "cn" => 1323128240, "dk" => 5457415, "fi" => 5302000, "fr" => 64102140, "gr" => 11147000, 
            "in" => 1131043000, "it" => 59206382, "jp" => 127718000, "mx" => 106535000, "nl" => 16402414, 
            "no" => 4738085, "es" => 45116894, "se" => 9174082, "ch" => 7508700);
    
    # Names of the gifts we can send
    %gifts =("card" => "Greeting card", "flowers" => "Flowers", "nastygram" => "Nastygram");
    
    # Create and grid the outer content frame
    $mw = Tkx::widget->new(".");
    $content = $mw->new_ttk__frame(-padding => "5 5 12 0");
    $content->g_grid(-column => 0, -row => 0, -sticky => "nwes");
    $mw->g_grid_columnconfigure(0, -weight => 1);
    $mw->g_grid_rowconfigure(0, -weight => 1);
    
    # Create the different widgets; note the variables that many
    # of them are bound to, as well as the button callback.
    # The listbox is the only widget we'll need to refer to directly
    # later in our program, so for convenience we'll assign it to a variable.
    # Remember that we must use a Tcl formatted list for listvariable.
    $cnames = ''; foreach $i (@countrynames) {$cnames = $cnames . ' {' . $i . '}';};
    $lbox = $content->new_tk__listbox(-listvariable => \$cnames, -height => 5);
    $lbl = $content->new_ttk__label(-text => "Send to country's leader:");
    $g1 = $content->new_ttk__radiobutton(-text => $gifts{'card'}, -variable => \$gift, -value => 'card');
    $g2 = $content->new_ttk__radiobutton(-text => $gifts{'flowers'}, -variable => \$gift, -value => 'flowers');
    $g3 = $content->new_ttk__radiobutton(-text => $gifts{'nastygram'}, -variable => \$gift, -value => 'nastygram');
    $send = $content->new_ttk__button(-text => "Send Gift", -command => sub {sendGift()}, -default => 'active');
    $l1 = $content->new_ttk__label(-textvariable => \$sentmsg, -anchor => "center");
    $l2 = $content->new_ttk__label(-textvariable => \$statusmsg, -anchor => "w");
    
    # Grid all the widgets
    $lbox->g_grid(-column => 0, -row => 0, -rowspan => 6, -sticky => "nsew");
    $lbl->g_grid(-column => 1, -row => 0, -padx => 10, -pady => 5);
    $g1->g_grid(-column => 1, -row => 1, -sticky => "w", -padx => 20);
    $g2->g_grid(-column => 1, -row => 2, -sticky => "w", -padx => 20);
    $g3->g_grid(-column => 1, -row => 3, -sticky => "w", -padx => 20);
    $send->g_grid(-column => 2, -row => 4, -sticky => "e");
    $l1->g_grid(-column => 1, -row => 5, -columnspan => 2, -sticky => "n", -pady => 5, -padx => 5);
    $l2->g_grid(-column => 0, -row => 6, -columnspan => 2, -sticky => "we");
    $content->g_grid_columnconfigure(0, -weight => 1);
    $content->g_grid_rowconfigure(0, -weight => 1);
    
    
    # Set event bindings for when the selection in the listbox changes,
    # when the user double clicks the list, and when they hit the Return key
    $lbox->g_bind("<<ListboxSelect>>", sub {showPopulation()});
    $lbox->g_bind("<Double-1>", sub {sendGift()});
    $mw->g_bind("<Return>", sub {sendGift()});
    
    # Called when the selection in the listbox changes; figure out
    # which country is currently selected, and then lookup its country
    # code, and from that, its population.  Update the status message
    # with the new population.  As well, clear the message about the
    # gift being sent, so it doesn't stick around after we start doing
    # other things.
    sub showPopulation {
        my @idx = $lbox->curselection;
        if ($#idx==0) {
            my $code = $countrycodes[$idx[0]];
            my $name = $countrynames[$idx[0]];
            my $popn = $populations{$code};
            $statusmsg = "The population of " . $name . "(" . $code . ") is $popn";
        }
        $sentmsg = "";
    }
    
    # Called when the user double clicks an item in the listbox, presses
    # the "Send Gift" button, or presses the Return key.  In case the selected
    # item is scrolled out of view, make sure it is visible.
    #
    # Figure out which country is selected, which gift is selected with the 
    # radiobuttons, "send the gift", and provide feedback that it was sent.
    sub sendGift {
        my @idx = $lbox->curselection;
        if ($#idx==0) {
            $lbox->see($idx[0]);
            my $name =$countrynames[$idx[0]];
            # Gift sending left as an exercise to the reader
            $sentmsg = "Sent " . $gifts{$gift} . " to leader of " . $name
        }     
    }
    
    # Colorize alternating lines of the listbox
    for ($i=0; $i<=$#countrynames; $i+=2) {
        $lbox->itemconfigure($i, -background => "#f0f0ff");
    }
    
    # Set the starting state of the interface, including selecting the
    # default gift to send, and clearing the messages.  Select the first
    # country in the list; because the <<ListboxSelect>> event is only
    # fired when users makes a change, we explicitly call showPopulation.
    $gift = 'card';
    $sentmsg = "";
    $statusmsg = "";
    $lbox->selection_set(0);
    showPopulation;
    
    Tkx::MainLoop();

    One obvious thing missing from this example was that while the list of countries could be quite long, only part of it fits on the screen at once. To show countries further down in the list, you had to either drag with your mouse or use the down arrow key. A scrollbar would have been nice. Let's fix that.

    Scrollbar


    screen shot
    Scrollbar widgets.

    A scrollbar widget helps users see all parts of another widget, whose content is typically much larger than what can be shown in the available screen space.


    Scrollbars are created using the ttk.Scrollbar class:

    s = ttk.Scrollbar( parent, orient=VERTICAL, command=listbox.yview)
    listbox.configure(yscrollcommand=s.set)

    Scrollbars are created using the ttk::scrollbar command:

    ttk::scrollbar .s -orient vertical -command ".l yview"
    .l configure -yscrollcommand ".s set"

    Scrollbars are created using the Tk::Tile::Scrollbar class:

    s = Tk::Tile::Scrollbar.new(parent) {orient "vertical"; 
            command proc{|*args| l.yview(*args);} }
    l['yscrollcommand'] = proc{|*args| s.set(*args);}
    Scrollbars are created using the new_ttk__scrollbar method, a.k.a. Tkx::ttk__scrollbar:
    $s = $parent->new_ttk__scrollbar(-orient => 'vertical', -command => [$listbox, 'yview']);
    $listbox->configure(-scrollcommand => [$s, 'set']);

    Unlike in some user interface toolkits, Tk scrollbars are not a part of another widget (e.g., a listbox) but are a separate widget altogether. Instead, scrollbars communicate with the scrolled widget by calling methods on the scrolled widget; as it turns out, the scrolled widget also needs to call methods on the scrollbar.

    If you're using a recent Linux distribution, you've probably noticed that the scrollbars you see in many applications have changed to look more like what you'd see on macOS. This newer look isn't supported on Linux by any of the default themes included with Tk. However, some third-party themes do support it.

    The orient configuration option determines whether the scrollbar will scroll the scrolled widget in the horizontal or vertical dimension. You then need to use the command configuration option to specify how to communicate with the scrolled widget. This is the method to call on the scrolled widget when the scrollbar moves.

    Every widget that can be scrolled vertically includes a method named yview, while those that can be scrolled horizontally have a method named xview). As long as this method is present, the scrollbar doesn't need to know anything else about the scrolled widget. When the scrollbar is manipulated, it passes several parameters to the method call, indicating how it was scrolled, to what position, etc.

    The scrolled widget also needs to communicate back to the scrollbar, telling it what percentage of the entire content area is now visible. Besides the yview and/or xview methods, every scrollable widget also has a yscrollcommand and/or xscrollcommand configuration option. This is used to specify a method call, which must be the scrollbar's set method. Again, additional parameters will be automatically tacked onto the method call.

    Most scrollable widgets also have xscrollbar and yscrollbar methods that will save you the trouble of writing your own command, xscrollcommand, and yscrollcommand callbacks needed to wire a scrollable widget to a scrollbar. Instead you just do something like:

    s = Tk::Tile::Scrollbar.new(parent) {orient "vertical"}
    l.yscrollbar = s

    If you want to move the scrollbar to a particular position from within your program, you can call the set first last method yourself. Pass it two floating-point values (between 0 and 1) indicating the start and end percentage of the content area that is visible.

    Example

    Listboxes are one of several types of widgets that are scrollable. Here, we'll build a very simple user interface consisting of a vertically scrollable listbox that takes up the entire window, with just a status line at the bottom.

    screen shot
    Scrolling a listbox.

    from tkinter import *
    from tkinter import ttk
    
    root = Tk()
    l = Listbox(root, height=5)
    l.grid(column=0, row=0, sticky=(N,W,E,S))
    s = ttk.Scrollbar(root, orient=VERTICAL, command=l.yview)
    s.grid(column=1, row=0, sticky=(N,S))
    l['yscrollcommand'] = s.set
    ttk.Label(root, text="Status message here", anchor=(W)).grid(column=0, columnspan=2, row=1, sticky=(W,E))
    root.grid_columnconfigure(0, weight=1)
    root.grid_rowconfigure(0, weight=1)
    for i in range(1,101):
        l.insert('end', f"Line {i} of 100")
    root.mainloop()
    grid [tk::listbox .l -yscrollcommand ".s set" -height 5] -column 0 -row 0 -sticky nwes
    grid [ttk::scrollbar .s -command ".l yview" -orient vertical] -column 1 -row 0 -sticky ns
    grid [ttk::label .stat -text "Status message here" -anchor w] -column 0 -columnspan 2 -row 1 -sticky we
    grid columnconfigure . 0 -weight 1; grid rowconfigure . 0 -weight 1
    for {set i 0} {$i<100} {incr i} {
       .l insert end "Line $i of 100"
    }
    require 'tk'
    require 'tkextlib/tile'
    
    root = TkRoot.new
    $l = TkListbox.new(root) {height 5; 
            yscrollcommand proc{|*args| $s.set(*args)} }.grid :column => 0, :row => 0, :sticky => 'nwes'
    $s = Tk::Tile::Scrollbar.new(root) {orient 'vertical'; 
            command proc{|*args| $l.yview(*args)}}.grid :column => 1, :row => 0, :sticky => 'ns'
    stat = Tk::Tile::Label.new(root) {text "Status message here"; 
            anchor 'w'}.grid :column => 0, :columnspan => 2, :row => 1, :sticky => 'we'
    TkGrid.columnconfigure root, 0, :weight => 1
    TkGrid.rowconfigure root, 0, :weight => 1
    
    (0..99).each {|i| $l.insert 'end', "Line #{i} of 100"}
    
    Tk.mainloop
    use Tkx;
    my $mw = Tkx::widget->new(".");
    
    ($lb = $mw->new_tk__listbox(-height => 5))->g_grid(-column => 0, -row => 0, -sticky => "nwes");
    ($s = $mw->new_ttk__scrollbar(-command => [$lb, "yview"], 
            -orient => "vertical"))->g_grid(-column =>1, -row => 0, -sticky => "ns");
    $lb->configure(-yscrollcommand => [$s, "set"]);
    ($mw->new_ttk__label(-text => "Status message here", 
            -anchor => "w"))->g_grid(-column => 0, -columnspan => 2, -row => 1, -sticky => "we");
    $mw->g_grid_columnconfigure(0, -weight => 1); $mw->g_grid_rowconfigure(0, -weight => 1);
    for ($i=0; $i<100; $i++) {
       $lb->insert("end", "Line " . $i . " of 100");
    }
    
    Tkx::MainLoop();

    If you've seen an earlier version of this tutorial, you might recall that at this point, we introduced a sizegrip widget. It placed a small handle at the bottom right of the window, allowing users to resize the window by dragging the handle. This was commonly seen on some platforms, including older versions of macOS. Some older versions of Tk even automatically added this handle to the window for you.

    Platform conventions tend to evolve faster than long-lived open source GUI toolkits. Mac OS X 10.7 did away with the size grip in the corner in favor of allowing resizing from any window edge, finally catching up with the rest of the world. Unless there's a pressing need to be visually compatible with 15+ year old operating systems, if you have a sizegrip in your application, it's probably best to remove it.

    Text


    screen shot
    Text widgets.

    A text widget allows users to enter multiple lines of text.
     
    Text widgets are part of the classic Tk widgets, not the themed Tk widgets.


    Tk's text widget is, along with the canvas widget, one of two uber-powerful widgets that provide amazingly deep but easily programmed features. Text widgets have formed the basis for full word processors, outliners, web browsers, and more. We'll get into some of the advanced stuff in a later chapter. Here, we'll show you how to use the text widget to capture fairly simple, multi-line text input.

    Text widgets are created using the Text class:

    txt = Text(parent, width=40, height=10)

    Text widgets are created using the tk::text command:

    tk::text .txt -width 40 -height 10

    Text widgets are created using the TkText class:

    txt = TkText.new(parent) {width 40; height 10}

    Text widgets are created using the new_tk__text method, a.k.a. Tkx::tk__text:

    $parent->new_tk__text(-width => 40, -height => 10);

    The width and height options specify the requested screen size of the text widget, in characters and rows, respectively. The contents of the text can be arbitrarily large. You can use the wrap configuration option to control how line wrapping is handled: values are none (no wrapping, text may horizontally scroll), char (wrap at any character), and word (wrapping will only occur at word boundaries).

    A text widget can be disabled so that no editing can occur. Because text is not a themed widget, the usual state and instate methods are not available. Instead, use the configuration option state, setting it to either disabled or normal.

    txt['state'] = 'disabled'
    .txt configure -state disabled
    txt['state'] = 'disabled'
    $txt->configure(-state => "disabled");

    Scrolling works the same way as in listboxes. The xscrollcommand and yscrollcommand configuration options attach the text widget to horizontal and/or vertical scrollbars, and the xview and yview methods are called from scrollbars. To ensure that a given bit of text is visible (i.e., not scrolled out of view), you can use the see method, passing it an index indicating the position of a character in the text.

    txt.see(5.0)
    .txt see 5.0
    txt.see('5.0')
    $txt->see('5.0');

    Contents and indices

    Text widgets do not have a linked variable associated with them like, for example, entry widgets do. To retrieve the contents of the entire text widget, call the get method, passing it the index of the first character (1.0) as well as the end of the text (end).

    contents = txt.get('1.0', 'end')
    set contents [.txt get 1.0 end]
    contents = txt.get("1.0", 'end')
    $contents = $txt->get("1.0", "end");

    In its simplest form, indices are of the form row.column, where row indicates the row number (1-based, so the first line is 1) and column indicates the character in the line (0-based, so 0 is the first character in the line). The shortcut end refers to just past the last character of the last line.

    The row.column indices can be provided as strings or floating point numbers, which are converted to strings. You can't provide an integer, e.g., 5, but would need to convert it to a floating point number, e.g., 5.0.

    Text can be added to the widget using the insert method, passing it the index where to insert the text as well as the text to insert. The delete method deletes text between two indices you provide.

    txt.insert('1.0', 'here is my\ntext to insert')
    txt.delete('1.5', '1.8')
    .txt insert 1.0 "here is my\ntext to insert"
    .txt delete 1.5 1.8
    txt.insert(1.0, "here is my\ntext to insert")
    txt.delete(1.5, 1.8)
    $txt->insert("1.0", "here is my\ntext to insert");
    $txt->delete("1.5", "1.8");

    We'll get into the text widget's many additional advanced features in a later chapter.

    Scale


    screen shot
    Scale widgets.

    A scale widget allows users to choose a numeric value through direct manipulation.


    Scale widgets are created using the ttk.Scale class:

    s = ttk.Scale(parent, orient=HORIZONTAL, length=200, from_=1.0, to=100.0)

    Because 'from' is a reserved keyword in Python, we need to add a trailing underscore when using it as a configuration option.

    Scale widgets are created using the ttk::scale command:

    ttk::scale .s -orient horizontal -length 200 -from 1.0 -to 100.0

    Scale widgets are created using the Tk::Tile::Scale class:

    s = Tk::Tile::Scale.new(parent) {orient 'horizontal'; length 200; from 1.0; to 100.0}

    Scale widgets are created using the new_ttk__scale method, a.k.a. Tkx::ttk__scale:

    $parent->new_ttk__scale(-orient => 'horizontal', -length => 200, -from => 1.0, -to => 100.0);

    As with scrollbars, the orient option may be either horizontal or vertical. The length option, which represents the longer axis of either horizontal or vertical scales, is specified in screen units (e.g., pixels). You should also define the range of the number that the scale allows users to choose; to do this, set a floating-point number for each of the from and to configuration options.

    There are several different ways you can set the current value of the scale (which must be a floating-point value between the from and to values). You can set (or read, to get the current value) the scale's value configuration option. You can link the scale to a variable using the variable option. Or, you can call the scale's set method to change the value or the get method to read the current value.

    A command configuration option lets you specify a callback which is invoked whenever the scale is changed. Tk passes the current value of the scale as a parameter each time it calls this script (we saw a similar thing with parameters being passed to scrollbar callbacks).

    # label tied to the same variable as the scale, so auto-updates
    num = StringVar()
    ttk.Label(root, textvariable=num).grid(column=0, row=0, sticky='we')
    
    # label that we'll manually update via the scale's command callback
    manual = ttk.Label(root)
    manual.grid(column=0, row=1, sticky='we')
    
    def update_lbl(val):
       manual['text'] = "Scale at " + val
    
    scale = ttk.Scale(root, orient='horizontal', length=200, from_=1.0, to=100.0, variable=num, command=update_lbl)
    scale.grid(column=0, row=2, sticky='we')
    scale.set(20)
    # label tied to the same variable as the scale, so auto-updates
    grid [ttk::label .auto -textvariable num] -column 0 -row 0 -sticky we
    
    # label that we'll manually update via the scale's command callback
    grid [ttk::label .manual] -column 0 -row 1 -sticky we
    proc update_lbl {val} {
       .manual configure -text "Scale at $val"
    }
    
    ttk::scale .scale -orient horizontal -length 200 -from 1.0 -to 100.0 -variable num -command update_lbl
    grid .scale -column 0 -row 2 -sticky we 
    .scale set 20
    # label tied to the same variable as the scale, so auto-updates
    num = TkVariable.new
    Tk::Tile::Label.new(root) {textvariable num}.grid :column => 0, :row => 0, :sticky => 'we'
    
    # label that we'll manually update via the scale's command callback
    $manual = Tk::Tile::Label.new(root)
    $manual.grid :column => 0, :row => 1, :sticky => 'we'
    
    
    def update_lbl(val)
       $manual['text'] = "Scale at " + val.to_s
    end
    
    scale = Tk::Tile::Scale.new(root) {orient 'horizontal'; length 200; from 1.0; to 100.0; variable num; command proc{|v| update_lbl v}}
    scale.grid :column => 0, :row => 2, :sticky => 'we'
    scale.set 20
    # label tied to the same variable as the scale, so auto-updates
    $mw->new_ttk__label(-textvariable => \$num)->g_grid(-column => 0, -row => 0, -sticky => "we");
    
    # label that we'll manually update via the scale's command callback
    $manual = $mw->new_ttk__label();
    $manual->g_grid(-column => 0, -row => 1, -sticky => "we");
    
    sub update_lbl {
       my ($val) = @_;
       $manual->configure(-text => "Scale x at ".$val);
    }
    
    $scale = $mw->new_ttk__scale(-orient => 'horizontal', -length => 200, -from => 1.0, -to => 100.0, 
            -variable => \$num, -command => sub {($v) = @_; update_lbl($v);});
    $scale->g_grid(-column => 0, -row => 2, -sticky => 'we');
    $scale->set(20);

    As with other themed widgets, you can use the state method to modify the disabled state flag; this prevent users from modifying the scale.

    As the scale widget does not display the actual values, you may want to add those separately, e.g., using label widgets.

    Spinbox


    screen shot
    Spinbox widgets.

    A spinbox widget allows users to choose numbers (or, in fact, items from an arbitrary list). It does this by combining an entry-like widget showing the current value with a pair of small up/down arrows, which can be used to step through the range of possible choices.


    The themed spinbox was added in Tk 8.5.9 (released in 2010). Earlier programs may have used the spinbox in the classic Tk widgets, which has a slightly different API.

    Spinbox widgets are created using the ttk.Spinbox class:

    spinval = StringVar()
    s = ttk.Spinbox(parent, from_=1.0, to=100.0, textvariable=spinval)

    Spinbox widgets are created using the ttk::spinbox command:

    ttk::spinbox .s -from 1.0 -to 100.0 -textvariable spinval

    Spinbox widgets are created using the Tk::Tile::Spinbox class:

    $spinval = TkVariable.new
    s = Tk::Tile::Spinbox.new(parent) {from 1.0; to 100.0; textvariable $spinval}

    Spinbox widgets are created using the new_ttk__spinbox method, a.k.a. Tkx::ttk__spinbox:

    $parent->new_ttk__spinbox(-from => 1.0, -to => 100.0, -textvariable => \$spinval);

    Like scale widgets, spinboxes let users choose a number between a certain range (specified using the from and to configuration options), though through a very different user interface. You can also specify an increment, which controls how much the value changes every time you click the up or down button.

    Like a listbox or combobox, spinboxes can also be used to let users choose an item from an arbitrary list of strings; these can be specified using the values configuration option. This works in the same way it does for comboboxes; specifying a list of values will override to from and to settings.

    In their default state, spinboxes allow users to select values either via the up and down buttons or by typing them directly into the entry area that displays the current value. If you'd like to disable the latter feature so that only the up and down buttons are available, you can set the readonly state flag.

    s.state(['readonly'])
    .s state readonly
    s.state('readonly')
    $s->state("readonly");

    Like other themed widgets, you can also disable spinboxes via the disabled state flag or check the state via the instate method. Spinboxes also support validation in the same manner as entry widgets, using the validate and validatecommand configuration options.

    You might be puzzled about when to choose a scale, listbox, combobox, entry, or a spinbox. Often, several of these can be used for the same types of data. The answer really depends on what you want users to select, platform user interface conventions, and the role the value plays in your user interface.

    For example, both a combobox and a spinbox take up fairly small amounts of space compared with a listbox. They might make sense for a more peripheral setting. A more primary and prominent choice in a user interface may warrant the extra space a listbox occupies. Spinboxes don't make much sense when items don't have a natural and obvious ordering to them. Be careful about putting too many items in both comboboxes and spinboxes. This can make it more time-consuming to select an item.

    There is a boolean wrap option that determines whether the value should wrap around when it goes beyond the starting or ending values. You can also specify a width for the entry holding the current value of the spinbox.

    Again there are choices as to how to set or get the current value in the spinbox. Normally, you would specify a linked variable with the textvariable configuration option. As usual, any changes to the variable are reflected in the spinbox, while any changes in the spinbox are reflected in the linked variable. As well, the set and get methods allow you to set or get the value directly.

    Spinboxes generate virtual events when users press up (<<Increment>>) or down (<<Decrement>>). A command configuration option allows you to provide a callback that is invoked on any changes.

    Progressbar


    screen shot
    Progressbar widgets.

    A progressbar widget provides feedback to users about the progress of a lengthy operation.
     
    In situations where you can estimate how long the operation will take to complete, you can display what fraction has already been completed. Otherwise, you can indicate the operation is continuing, but without suggesting how much longer it will take.


    Progressbar widgets are created using the ttk.Progressbar class:

    p = ttk.Progressbar(parent, orient=HORIZONTAL, length=200, mode='determinate')

    Progressbar widgets are created using the ttk::progressbar command:

    ttk::progressbar .p -orient horizontal -length 200 -mode determinate

    Progressbar widgets are created using the Tk::Tile::Progressbar class:

    p = Tk::Tile::Progressbar.new(parent) {orient 'horizontal'; length 200; mode 'determinate'}

    Progressbar widgets are created using the new_ttk__progressbar method, a.k.a. Tkx::ttk__progressbar:

    $parent->new_ttk__progressbar(-orient => 'horizontal', -length => 200, -mode => 'determinate');

    As with scale widgets, they should be given an orientation (horizontal or vertical) with the orient configuration option and can be given an optional length. The mode configuration option can be set to either determinate, where the progressbar will indicate relative progress towards completion, or indeterminate, where it shows that the operation is still continuing but without showing relative progress.

    Determinate progress

    To use determinate mode, estimate the total number of "steps" the operation will take to complete. This could be an amount of time but doesn't need to be. Provide this to the progressbar using the maximum configuration option. It should be a floating-point number and defaults to 100.0 (i.e., each step is 1%).

    As you proceed through the operation, tell the progressbar how far along you are with the value configuration option. So this would start at 0 and then count upwards to the maximum value you have set.

    There are two other ways to specify the current progress to display. First, you can just store the current value for the progressbar in a variable linked to it by the progressbar's variable configuration option; that way, when you change the variable, the progressbar will update. The other alternative is to call the progressbar's step method, which increments the value by a given amount (defaults to 1.0).

    Indeterminate progress

    Use indeterminate mode when you can't easily estimate how far along in a long-running task you actually are. However, you still want to provide feedback that the operation is continuing (and that your program hasn't crashed). At the start of the operation, call the progressbar's start method. At the end of the operation, call its stop method. The progressbar will take care of the rest.

    Unfortunately, "the progressbar will take care of the rest" isn't quite so simple. In fact, if you start the progressbar, call a function that takes several minutes to complete, and then stop the progressbar, your program will appear frozen the whole time, with the progressbar not updating. In fact, it will not likely appear onscreen at all. Yikes!

    To learn why that is and how to address it, we'll need to take a deeper dive into Tk's event loop.