Chapter 13. Listbox Widgets

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.

Matching Lists

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.113.5 illustrate the game’s screens.

Match the words on the left to the definitions on the right.

Figure 13.1. Match the words on the left to the definitions on the right.

After selecting a word and a definition, click the Match button.

Figure 13.2. After selecting a word and a definition, click the Match button.

Click the Score button after creating your matches.

Figure 13.3. Click the Score button after creating your matches.

A perfect score!

Figure 13.4. A perfect score!

Maybe I need to try again.

Figure 13.5. Maybe I need to try again.

Creating a Listbox

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.

The listbox widget is easy to create and to populate.

Figure 13.6. The listbox widget is easy to create and to populate.

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.

Click the Delete button to delete the first five entries in the list.

Figure 13.7. Click the Delete button to delete the first five entries in the list.

After deletion, there are five fewer colors in this list.

Figure 13.8. After deletion, there are five fewer colors in this list.

Selecting Listbox Content

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.

Setting the Selection Mode

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

single

Only a single item can be selected with mouse button 1.

browse

Only a single item can be selected with mouse button 1, and you can drag the selection with button 1.

multiple

Multiple items can be selected with mouse button 1, and clicking an item toggles its selected state.

extended

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).

Note: The Selection Mode Can Be Arbitrarily Defined

Note: The Selection Mode Can Be Arbitrarily Defined

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.

Using -selectmode multiple makes it easy to select nonadjacent listbox items.

Figure 13.9. Using -selectmode multiple makes it easy to select nonadjacent listbox items.

Click and drag to select multiple adjacent items in the extended selection mode.

Figure 13.10. Click and drag to select multiple adjacent items in the extended selection mode.

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.

Determining the Selected Items

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:

  • listbox index active—Returns the index of the active item.

  • listbox curselection—Returns a list of indices of selected lines.

  • listbox get first ?last?—Returns the lines between and including first and, if specified, last, where first and last are index values.

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

0

Index of the first item.

active

Index of the active (activated) item.

anchor

Index of the current selection’s anchor point.

end

Index of the last item.

num

Item num in the listbox, counting from zero.

@x,y

The item nearest the listbox-relative coordinates given by x and y.

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.

Select the colors you want to delete then click the Delete button.

Figure 13.11. Select the colors you want to delete then click the Delete button.

The selected colors are gone!

Figure 13.12. The selected colors are gone!

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.

Selecting Items Programmatically

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

listbox nearest y

Returns the index of the value closest to the specified widget-relative y coordinate.

listbox scan mark x y

Starts a scrolling operation for the specified widget-relative x and y coordinates (usually used with the scan dragto operation).

listbox scan dragto x y

Scrolls from a previously set mark (see the scan mark operation) to the specified widget-relative x and y coordinates.

listbox see index

Scrolls the specified index so it is visible in the listbox.

listbox selection anchor index

Anchors the selection at the item specified by index.

listbox selection clear first ?last?

Clears selected items between and including the index values specified by first and last (if last is specified).

listbox selection includes index

Returns 1 if the current selection includes the item specified by index.

listbox selection set first ?last?

Creates a selection consisting of the items between and including the index values specified by first and last (if last is specified).

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.

The index values help you see the effects of the Select and Scroll buttons.

Figure 13.13. The index values help you see the effects of the Select and Scroll buttons.

The Scroll button moves the item at index 200 to the center of the list.

Figure 13.14. The Scroll button moves the item at index 200 to the center of the list.

Clicking the Select button programmatically selects the requested items.

Figure 13.15. Clicking the Select button programmatically selects the requested items.

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.

Analyzing Matching Lists

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.

Looking at the Code

#!/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

Understanding the Code

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:

  1. Get the index of the currently selected word using the curselection operation on the words listbox (set w [$lWords curselection]).

  2. Get the index of the currently selected definition using the curselection operation on the definitions listbox (set d [$lDefs curselection]).

  3. Fetch the text string corresponding to the index value $w and store it in the $word variable (set word [$lWords get $w]).

  4. Fetch the text string corresponding to the index value $d and store it in the $def variable (set def [$lDefs get $d]).

  5. 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, gridding 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 listboxes, 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.

Modifying the Code

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.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset