This chapter shows you how to use Tk’s listbox
widget. A listbox
displays a series of read-only text lines. The list is vertically scrollable and can be scrolled horizontally as necessary. You can select zero, one, or more items in a list, so the listbox
widget has methods for determining which items are selected (and for selecting items programmatically). You can add and delete items from a listbox
, but items themselves cannot be edited. As usual, you can also control the colors, relief, and other visual attributes of listbox
widgets.
The idea of the this chapter’s game is to select related words and phrases from adjacent listboxes. After you create each match, click the Match button to record your selection. After you have matched all of the words in the left-hand listbox with their matching definitions in the right-hand listbox, click the Score button to see how you did. To start the game, execute the matches.tcl script in this chapter’s code directory. Figures 13.1–13.5 illustrate the game’s screens.
Back in Chapter 10, I provided an extra script, show_colors.tcl, which used a listbox
widget. I’m going to use various versions of that script to introduce you to the features of Tk’s listbox
widget. I’ll start by creating a listbox and populating it with some names of colors, as shown below (see list_create.tcl in this chapter’s code directory):
proc GetColors {colorFile} { set fileId [open $colorFile r] while {[gets $fileId line] > 0} { lappend colors [string trim [lrange $line 0 end]] } close $fileId return $colors } listbox .colorlist -bg white set colors [GetColors "colors.txt"] foreach color $colors { .colorlist insert end $color } button .bexit -text "Exit" -command exit grid .colorlist -padx 5 -pady 5 -sticky nsew grid .eExit -pady {0 5}
The GetColors
procedure is a helper procedure. It reads the file passed as its sole argument and creates a list of colors ($colors
) from the contents of that file. It returns the list of colors to the calling procedure. Figure 13.6 shows the list_create.tcl’s window.
At the moment, all you can do is select a color name (as shown in the figure) and scroll the list using a mouse wheel (if your mouse has one) or by left-clicking and dragging down to scroll down the list or dragging up to scroll up the list. You’ll notice when scrolling the list that the selected color name, if any, scrolls out of view. You’ll probably also notice that you can only select a single item at a time; this is the default behavior but it can be changed. I’ll discuss this in the next section, “Selecting Listbox Content.”
Creating the listbox is simple: The listbox
command creates a listbox widget named .colorlist
and gives it a white background. To populate the list, I invoke the GetColors
procedure, saving the list of color names it returns in the variable $colors
. Next, I use a foreach
loop to iterate over each color name and add it to the listbox using the listbox
widget’s insert
command:
.colorlist insert end $color
The insert
command adds an item to a listbox
at a specified index. In this case, I used the special keyword end
, which means the color name is added to the end of bottom of the list. To specify a particular location, I could have specified a particular index value.
I create an Exit button (.bexit
) for the sake of convenience, and then lay out the list and the button using the grid
command.
The inverse of inserting an item into a list is removing an item from a list. You accomplish this using the delete
command. The following script, item_delete.tcl, adds a Delete button to the list_create.tcl script. I’ve shown the code additions in boldface so you can see how the script has changed:
proc GetColors {colorFile} { set fileId [open $colorFile r] while {[gets $fileId line] > 0} { lappend colors [string trim [lrange $line 0 end]] } close $fileId return $colors } proc DelItems {w} { $w delete 0 4 } listbox .colorlist -bg white set colors [GetColors "colors.txt"] foreach color $colors { .colorlist insert end $color } button .bexit -text "Exit" -command exit button .bdelete -text "Delete" -command {DelItems .colorlist} grid .colorlist -padx 5 -pady 5 -columnspan 2 -sticky nsew grid .bdelete .bexit -pady {0 5}
With four additional lines of code, I can now delete color names from the list. The key line of code is $w delete 0
4 in the DelItems
procedure. The syntax for the listbox
’s delete
procedure is:
listbox delete first ?last?
Here, listbox
does not refer to the listbox
command itself, but the name of a listbox
widget or a variable reference to a listbox
widget. first
and last
represent the indices, inclusive, of items that should be deleted from the list. If you omit last
, only a single item will be deleted. Otherwise, all the items between and including first
and last
will be deleted. So, in the case of item_delete.tcl, my Delete button is hard-wired to delete the first five colors in this list. This isn’t terribly useful, but it shows the basic technique. I’ll show you how to delete specific items in the next section.
Figures 13.7 and 13.8 show the item_delete.tcl’s window before and after deleting colors from the list.
In the item_delete.tcl script, I deleted color names from the list arbitrarily. This is not terribly useful. The usual sequence of events is for the user to select one or more items and then click a Delete or Remove button to delete the selected items. First, you need to know how to create listbox
widgets that support selecting multiple items. Then you need to know how to reference the selected items.
You can set the selection mode using the listbox
widget’s -selectmode
attribute, which, for the purposes of this book, must be one of the values in Table 13.1.
Table 13.1. Values for the -selectmode
Attribute
Value | Description |
---|---|
| Only a single item can be selected with mouse button 1. |
| Only a single item can be selected with mouse button 1, and you can drag the selection with button 1. |
| Multiple items can be selected with mouse button 1, and clicking an item toggles its selected state. |
| Multiple items can be selected with mouse button 1. Clicking an item unselects everything else and sets a new selection anchor. |
In browse
mode, you can click and drag the selection with mouse button 1. If the selection mode is multiple
or extended
, you can select multiple items simultaneously, including items that aren’t adjacent to each other. In multiple
mode, clicking mouse button 1 on a list item toggles its selected state but does not affect other items’ selected state.
In extended
mode, clicking mouse button 1 on a list item selects it, unselects everything else, and sets the selecting anchor to selected item. If you then drag the mouse (with mouse button 1 pressed), you extend the selection to include all of the items between the anchor and the element under the mouse. You can also click an item to set the anchor and then Shift+Click (press the Shift key while clicking mouse button 1) on another item to select all of the items between the anchor and the Shift+Clicked item. Finally, in extended mode, to add a nonadjacent item to a selection, Ctrl+Click it (press the Control key while clicking mouse button 1).
The default bindings defined for the Tk listbox
widget expect the -selectmode
attribute to be one of the values shown in Table 13.1. However, if you modify the bindings, you can use an arbitrary value to which your customized binding will respond. I do not address binding in this book, so I’ve stuck to the default attributes.
In Figure 13.9, I show how the multiple
selection mode works while Figure 13.10 shows how extended
selection mode works. To create Figure in 13.9, I modified list_create.tcl, adding -selectmode multiple
to the listbox
command:
listbox .colorlist -bg white -selectmode multiple
See select_mult.tcl in this chapter’s code directory.
To create Figure 13.10, I specified -selectmode extended
when creating the listbox:
listbox .colorlist -bg white -selectmode extended
See select_ext.tcl in this chapter’s code directory.
You’re probably thinking something like, “Swell, Kurt. But how do I find out what items are selected?” Well, you’ll need a goat, a chicken foot, some blood, and... Wait, wrong book. The listbox
widget has two commands for retrieving indices of selected items. They are:
As before, listbox
does not refer to the listbox
command itself, but to the name of a listbox
widget or a variable reference to a listbox
widget. Each of these commands is best used in different circumstances. The index
command, for example, returns the numerical index that corresponds to its argument. In this case, I used the keyword active
, which corresponds to the activated item in the list. It is best used when you are interested in the active element, which does not necessarily correspond to all of the selected elements. Table 13.2 lists other possible values for all listboxindex
and other listbox
operations that require index arguments.
Table 13.2. Listbox Index Values
Value | Description |
---|---|
| Index of the first item. |
| Index of the active (activated) item. |
| Index of the current selection’s anchor point. |
| Index of the last item. |
| Item |
| The item nearest the listbox-relative coordinates given by |
If there are, or might be, multiple items selected in a listbox
, the best command to use is listbox
curselection
, which returns a list of all the indices of selected items. For the Delete button in the color picker script I’ve been using in this chapter, the curselection
command is the one I’ll use (more about that very shortly). Finally, if you know the index or indices of the listbox
items in which you’re interested, you can use listbox
get
, passing it the index or range of consecutive indices. The useful feature of the get
operation is that it returns the text of the specified items, rather than their indices.
Using what I’ve just discussed, the following listing shows yet another variation of the list_create.tcl script that enables you to delete all of the selected colors (see list_delete.tcl in this chapter’s code directory):
proc GetColors {colorFile} { set fileId [open $colorFile r] while {[gets $fileId line] > 0} { lappend colors [string trim [lrange $line 0 end]] } close $fileId return $colors } proc DelItems {w} { set s [$w curselection] set colors [lsort -decreasing -integer $s] foreach color $colors { $w delete $color } } listbox .colorlist -selectmode multiple -bg white foreach color [GetColors "colors.txt"] { .colorlist insert end $color } button .bdel -text "Delete" -command {DelItems .colorlist} button .bexit -text "Exit" -command exit grid .colorlist -padx 5 -pady 5 -columnspan 2 grid .bdel .bexit -pady {0 5}
The real guts of list_delete.tcl reside in the DelItems
procedure. It accepts a single argument, the widget on which to operate. The first set
command retrieves the indices of selected items in the widget, storing this list in the variable $s
. This is a junk variable, so I didn’t bother giving it a meaningful name. The next set
operation sorts that list in descending numeric order. The reason I wanted the list in descending order was to preserve the ordering of items in the list as I deleted items. If I delete from the “bottom” of the list, the order of items above the deleted item won’t change. If I delete from the “top” of the list, each deletion changes the index value of all of the items below the deleted item. After sorting the list, deleting the items is a simple matter of iterating through the sorted list with a foreach
and calling $w delete
against each index value.
The other significant change to the script was wiring the Delete button to the new DelItems
script. Figures 13.11 and 13.12 show the color list before and after deleting some randomly selected colors.
Had I not sorted the retrieved indices in descending numeric order, instead of deleting the colors snow, GhostWhite, gainsboro, and OldLace, I would have deleted snow, white smoke, FloralWhite, and AntiqueWhite.
Another task you’ll surely want to perform is to select list items programmatically, that is, with code. Table 13.3 shows the operations you have at your disposal for performing selection-related activities in code.
Table 13.3. Selection Commands for the listbox
Widget
Command | Description |
---|---|
| Returns the index of the value closest to the specified widget-relative |
| Starts a scrolling operation for the specified widget-relative |
| Scrolls from a previously set mark (see the scan mark operation) to the specified widget-relative |
| Scrolls the specified |
| Anchors the selection at the item specified by |
| Clears selected items between and including the index values specified by |
| Returns 1 if the current selection includes the item specified by |
| Creates a selection consisting of the items between and including the index values specified by |
The next script, auto_select.tcl in this chapter’s code directory, uses selection set
to select the list items between index values 200 and 204 inclusive and the see
command to scroll the selected items into view:
proc GetColors {colorFile} { set fileId [open $colorFile r] while {[gets $fileId line] > 0} { lappend colors [string trim [lrange $line 0 end]] } close $fileId return $colors } listbox .colorlist -bg white set colors [GetColors "colors.txt"] set i 0 foreach color $colors { set item [format "%3d %s" $i $color] puts $item .colorlist insert end $item incr i } button .bexit -text "Exit" -command exit button .bselect -text "Select" -command {.colorlist selection set 200 204} button .bscroll -text "Scroll" -command {.colorlist see 200} grid .colorlist -padx 5 -pady 5 -sticky nsew -columnspan 2 grid .bselect .bscroll -padx 5 -pady {0 5} -sticky nsew grid .bexit -pady {0 5} -columnspan 2
As with the other scripts in this chapter, auto_select.tcl is a modified version of list_create.tcl. In this case, I modified the foreach
loop to show the index value of each item in the list in addition to its text string. I also added two button
widgets to implement the selection (.bselect
) and scrolling (.bscroll
) functionality. .bselect
’s -command
attribute executes the command .colorlist selection set 200 204
. The -command
attribute for .bscroll
, similarly, scrolls the list so that the item at index value 200 appears in the center of the listbox
widget’s viewable area. Figures 13.13, 13.14, and 13.15 show the initial window, the results after scrolling the window (clicking the Scroll button), and the window after clicking the Select button, respectively.
In case you were wondering, you can also click the Select button first, followed by the Scroll button. I won’t get into the philosophical question of whether or not selected items are really selected if you can’t see them, but what I will guarantee is that when you click Scroll, the items I selected are, in fact, selected.
The Matching Lists games is arguably the most involved script you’ve seen in this book. It’s certainly one of the most complete, using a wide selection of the Tcl and Tk elements I’ve introduced in this book, including arrays, lists, sorting, frames, buttons, mathematical expressions, and, of course, the listbox
widget.
#!/usr/bin/wish # matches.tcl # Match words and phrases in two lists # Block 1 # Variable needed in the procedures set matches {} # Words and their definitions array set items { "HTML" "Language of the World Wide Web" "Tcl" "Programming language originally designed as a glue language" "Ousterhout" "Surname of the person who originally wrote Tcl" "expr" "Tcl command for performing mathematical operations" "9" "The Arabic numeral equivalent to the Roman numeral IX" "25" "Missing value in the sequence of numbers 4, 9, 16, 36" "Microsoft" "Computing's Evil Empire" "Linux" "Operating system whose mascot is a penguin" } # Block 2 # Match the selected word and the selected definition and mark # them "disabled" proc MatchSels {} { global lWords lDefs matches # Get the current selections set w [$lWords curselection] set d [$lDefs curselection] # Map the indices to their text values set word [$lWords get $w] set def [$lDefs get $d] # Append the matched pair to the list matches lappend matches $word $def # "Disable" the current selections $lWords itemconfigure $w -foreground grey $lDefs itemconfigure $d -foreground grey # Clear the current selections $lWords selection clear $w $lDefs selection clear $d } # Block 3 # Compare player's matches to the source array proc ScoreMatches {} { global matches items set correct 0 set incorrect 0 foreach {word def} $matches { if {$def eq $items($word)} { incr correct } else { incr incorrect } } tk_messageBox -title "Your Score" -type ok -icon info -message "Correct matches: $correct Incorrect matches: $incorrect" } # Block 4 # Define the widgets set lWords [listbox .lwords -selectmode single -bg white -exportselection false] set lDefs [listbox .ldefs -selectmode single -bg white -exportselection false] set fButtons [frame .fbuttons] set bMatch [button .bmarch -width 5 -text "Match" -command MatchSels] set bScore [button .bscore -width 5 -text "Score" -command ScoreMatches] set bExit [button .bexit -width 5 -text "Exit" -command exit] # Lay 'em out grid $lWords $lDefs -padx 5 -pady 5 grid $fButtons -columnspan 2 -padx 5 -pady 5 grid $bMatch $bScore $bExit -in .fbuttons -sticky nsew -padx {5 0} -pady {0 5} # Block 5 # Parse the items array for words and their definitions foreach {word def} [array get items] { lappend words $word lappend defs $def } # Block 6 # Populate and resize the words listbox set wordLen 0 foreach word [lsort -ascii $words] { set newLen [string length $word] set wordLen [expr $newLen > $wordLen ? $newLen : $wordLen] $lWords insert end $word } $lWords configure -width $wordLen # Populate and resize the definitions listbox set defLen 0 foreach def [lsort -ascii $defs] { set newLen [string length $def] set defLen [expr $newLen > $defLen ? $newLen : $defLen] $lDefs insert end $def } $lDefs configure -width $defLen
Block 1 consists of variable definitions. The $matches
list stores a list of matched words and definitions created when the player clicks the Match button. The $items
array is the source list of the words and definitions used to populate the two listbox
widgets. It also serves as the master list against which the player’s matches are scored.
In Block 2, I define the MatchSels
procedure, which is invoked each time the player clicks the Match button. I declare the global variables $lWords
, $lDefs
, and matches because I will be modifying them. $lWords
stores the list of words extracted from the $items
array, while $lDefs
stores the list of definitions, also extracted from the $items
array. I define these two variables later in the program, but I declare them here so I can access them inside the procedure.
To create the player’s match, I have to find out which items are selected and store the matched word and definition pair. The procedure is straightforward:
Get the index of the currently selected word using the curselection
operation on the words listbox
(set w [$lWords curselection]
).
Get the index of the currently selected definition using the curselection
operation on the definitions listbox
(set d [$lDefs curselection]
).
Fetch the text string corresponding to the index value $w
and store it in the $word
variable (set word [$lWords get $w]
).
Fetch the text string corresponding to the index value $d
and store it in the $def
variable (set def [$lDefs get $d]
).
Append the matched $word
and $def
to the $matches
list (lappend matches $word $def
).
Finally, as a visual aid for the player, I change the font color of items that have been matched. Although the items aren’t actually disabled, making them gray emulates a common GUI idiom of “graying out” disabled items. The itemconfigure
operation and its related itemcget
operation allow you to set and retrieve, respectively, individual list items rather than the listbox
itself or the list as a whole. Similarly, after completing the match, I clear the selected items using the selection clear
operation to give the player a visual cue that I made the match and as a hint to continue.
The ScoreMatches
procedure defined in Block 3 compares the player’s matches, stored in the $matches
list, to the master list of words and definitions stored in the $items
array. Again, these two variables are defined in the global scope, so in order to access them inside the procedure, I declare them as global variables. I also declare two procedure local variables, $correct
and $incorrect
, whose sole purpose is to keep track of the number of correct and incorrect matches.
The actual comparison is simple enough. Iterating through the player’s list of matches, I first break each pair of matched items into a word ($word
) and a definition ($def
). I compare the string value of the definition to the corresponding definition of $word
from the $items
array, using $word
to index into the $items array
. If the value returned by $items($word)
is identical to $def
, the match is correct, and I increment $correct
accordingly. Otherwise, the match is incorrect, and I increment $incorrect
. After iterating through each pair of matched items in the $matches
list, I use tk_messageBox
to display the results.
Block 4 defines and lays out the widgets I’ll need, two listbox
widgets, a frame
to hold the buttons, and the button
widgets. I used the single selection mode on the listboxes to prevent the player from trying to match two words to a single definition (or vice versa).
I specified -exportselection false
to make it possible to select items from more than one listbox at a time. By default, the listbox
widget exports its selection to the X Window System selection buffer (the clipboard under Windows). Because there can only be a single selection buffer at any one time, you can only select items from a single listbox
widget. I needed to be able to select items from more than one listbox
; setting -exportselection false
avoids this limitation. It also prevents selections from being accessed using the selection buffer (clipboard), but matches.tcl doesn’t need the selection buffer, so this wasn’t a problem.
I used a frame
widget as a container for the buttons so the buttons would be nicely centered beneath the two listbox
widgets. You’ll see why this is necessary when you get to Block 6. Beyond this one wrinkle, grid
ding out the widgets is routine.
The code in Block 5 parses the $items
array, storing the words into the $words
list and the definitions into the $defs
list.
The code in Block 6 is somewhat more involved. The goal I want to achieve is to make each listbox
just wide enough to hold the widest list item (measured in characters) and also to modify the order of items as I insert them into the listbox
. If I don’t need to modify the order of the list items, the words and their correct definitions end up side-by-side in the two listbox
es, which doesn’t present much of a challenge to the player.
First, I set the variable $wordLen
to 0. At the top of the foreach
loop, I sort the contents of the $words
list in (ascending) alphabetical order. Then, for each item in the list, I check its length in characters. I use expr
’s conditional operator (expr $wordLen > $newLen ? $wordLen : $newLen
) to update the value of $wordLen
if the length of the current word (stored in $newLen
) is longer (if it isn’t, $wordLen
is unchanged). Finally, I insert the current word at the end or bottom of the $lWords listbox
. After processing all of the words on the $words
list, I use the value of $wordLen
to update the width of the $lWords listbox
($lWords configure -width $wordLen
). The second foreach
loop in Block 6 uses the same technique for the $defs
list and the $lDefs listbox
, so I’ll spare you a repeat of the explanation.
At this point, the setup is complete, and the game is ready to play.
Here are some exercises you can try to practice what you learned in this chapter:
13.1. Modify the MatchSels
procedure to detect if the player has already selected a word or definition and to prevent reuse of a previously selected word or definition.
13.2. Modify the ScoreMatches
procedure to set the foreground color of correctly matched words and definitions to green and incorrectly matched words and definitions to red.
13.3. Modify the script to disable the Score button at the beginning of the game. Similarly, disable the Match button and enable the Score button after all items have been matched.