Chapter 14. Scrollbar, Scale, and Text Widgets

Scrollbars allow you or your users to scroll the viewable area of a window. A scale widget is a slider whose value changes as the slider is moved. Text widgets provide areas for displaying and editing text. Except for very short text documents or small objects of any variety, you will need to use vertical or horizontal scrollbars (or both) to allow users to view different portions of a window’s content. In a text document, for example, you’d add a vertical scrollbar to enable users to scroll the document up and down. If you’re writing a game that needs a player map, similarly, chances are good that you would need both vertical and horizontal scrollbars so users could look at different parts of the map.

Text widgets are used, well, to display text. In the context of a game, you might not need to display long sections of text, but most other applications usually do involve text display and manipulation. As you will see later in the chapter, Tk’s text widget is a full-featured text display and manipulation tool. The price of this feature set is that the text widget is complex.

Word Search

This chapter’s program, gword_search.tcl, presents a simplified version of the classic word search puzzle to illustrate how to program Tk’s text widget. The game shows users a randomly ordered collection of letters from which the player must select a word made up of consecutive letters. The player selects a word from the jumble of letters and clicks the Score button. If the correct target word is selected, the word is highlighted in green and disabled. If the selected word is incorrect or isn’t the target word, the selection is highlighted in red. To start the game, execute the gword_search.tcl script found in this chapter’s code directory. Figures 14.1 through 14.3 show the progress of the game.

Select a word from the letter jumble and click the Score button.

Figure 14.1. Select a word from the letter jumble and click the Score button.

A correctly selected word is highlighted in green.

Figure 14.2. A correctly selected word is highlighted in green.

An incorrectly selected word is highlighted in red.

Figure 14.3. An incorrectly selected word is highlighted in red.

Using the Scrollbars to Move the Viewport

The scrollbar command creates a scrollbar widget, which is used to change the visible area, referred to as the viewport, of another widget. Scrollbars work with four of Tk’s standard widgets: entry widgets, text widgets, listbox widgets, and canvas widgets. Although they only work with these four standard widgets, the scrollbar protocol is general enough that you can use it to control widgets that you create, but creating a widget from scratch, as opposed to using Tk’s stock widgets, is not a subject I’ll cover in this book. I do describe how you can interact with the scrollbar protocol directly instead of using its built-in defaults, however, but I think you’ll see that this is not an undertaking for the faint of heart.

Scrollbars can move the viewport horizontally, vertically, or both. They consist of a slider, a trough in which the slider moves, and arrows at each end of the trough. The position and size of the slider provide a visual cue about how much of the document is visible in the associated window. For example, if the slider in a vertical scrollbar covers the top third of the area between the two arrows, it means that the associated window displays the top third of its document.

Simple Scrolling

The best way to get started is to look at an example, simple_scroll.tcl, in this chapter’s code directory. It uses the text widget that you will learn about later in the chapter, but the principles for connecting a scrollbar to a scrollbar-supporting widget are the same, regardless of the widget to which the scrollbar is connected:

proc ReadFile {f} {
    set fileId [open $f r]
    set input [read $fileId]
    close $fileId
    return $input
}

set t [text .t -background #ffffff]
$t insert end [ReadFile "README"]

set sb [scrollbar .y -command [list $t yview]]
$t configure -wrap word -yscrollcommand [list $sb set]

grid $t $sb -sticky nsew
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
grid columnconfigure . 1 -weight 0

The most important two lines of code in simple_scroll.tcl are

set sb [scrollbar .y -command [list $t yview]]
$t configure -wrap word -yscrollcommand [list $sb set]

because they connect the scrollbar and the text widgets together. The first line invokes the text widget’s yview operation when the scrollbar is repositioned. In effect, the first command wires the text widget’s display to the scrollbar so that moving the slider up moves the viewport up, and moving the slider down moves the viewport down. If the scrollbar had been oriented horizontally, I would have invoked the text widget’s xview operation.

The second command completes the circuit, so to speak, wiring the scrollbar to the text widget by invoking the scrollbar’s set operation whenever the view in the text widget changes. Thus, if you use the up or down arrow keys to change the text viewed in the text widget, the slider’s position in the scrollbar changes accordingly.

Tip: A Confession of Confusion

Tip: A Confession of Confusion

For some reason, when I was first learning Tk, the connections between the scrollbar widget and the widgets on which they operated confused me. I finally settled on this formulation of the relationship:

  • The scrollbar’s -command attribute must invoke the connected widget’s scrolling operation, which is yview for vertical movement or xview for horizontal movement.

  • The connected widget’s scrolling attribute (which is either -yscrollcommand for vertical scrolling or -xscrollcommand for horizontal movement) must invoke the scrollbar’s set operation.

Perhaps I’m just easy to confuse, but these two rules work for me, so I hope they help you.

The four grid commands are important, too:

grid $t $sb -sticky nsew
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
grid columnconfigure . 1 -weight 0

The first command lays out the text widget, followed by the scrollbar. The order in which the widgets are passed to the grid command ensures that the scrollbar appears on the right side of the text widget. The second and third grid commands assign a weight of 1 to column 0 and row 0. Recall from Chapter 11 that the -weight option for the grid command controls whether or not the specified column or row resizes when the master resizes. A non-zero value means that they will resize. Bear in mind that column 0 and row 0 in this case corresponds to the text widget. The last grid command sets the weight of column 1 (the second column) to 0, meaning that this column (which happens to contain the scrollbar) won’t resize horizontally when the master widget resizes. Putting it all together, then, when you resize the window containing the text and scrollbar widget, the second, third, and fourth grid commands allow the text widget to resize in both the horizontal and vertical directions while constraining the scrollbar widget to resize only vertically while its width remains static.

Figures 14.4 and 14.5 show what simple_scroll.tcl’s window looks like.

Adding a scrollbar to simple_text.tcl makes it look like a proper text display.

Figure 14.4. Adding a scrollbar to simple_text.tcl makes it look like a proper text display.

When the display shows all of the text, the slider doesn’t move.

Figure 14.5. When the display shows all of the text, the slider doesn’t move.

The documentation for the scrollbar widget describes the scrollbar in terms of the following five components:

  1. arrow1—The arrow at the top left end of the scrollbar.

  2. trough1—The space between the slider and arrow1.

  3. slider—The rectangular box in the scrollbar that indicates the amount and location of text visible in the associated widget.

  4. trough2—The space between the slider and arrow2.

  5. arrow2—The arrow at the bottom or right end of the scrollbar.

For the most part, I won’t use these terms, but they appear in the documentation and some scrollbar-related operations use them, so you should be aware of them and to what they refer. Figure 14.6 shows each of these widget parts as they are described in the scrollbar documentation.

The Tk documentation uses its own terminology to refer to scrollbar components.

Figure 14.6. The Tk documentation uses its own terminology to refer to scrollbar components.

If you play with the window created by simple_scroll.tcl, notice that the size of the slider (its height in the trough) corresponds proportionately to the size of what is available in the viewport, relative to the total size of the item you are viewing through the viewport. For example, in Figure 14.4, the slider is just over half the size of the scrollbar, indicating that approximately half of the document is visible in the associated text widget. In Figure 14.5, on the other hand, the slider extends the full length of the trough, which means you are viewing the entire document.

Similarly, the location of the slider corresponds roughly to where in the item being viewed the viewport resides. In Figure 14.4, the top of the slider is anchored against the top of the scrollbar, so you can surmise that you are looking at the top of the document. In Figure 14.5, however, the resized window shows all the document, so the slider is anchored to the top and the bottom of the scroll trough.

The behavior I’ve described in this section relies on the default values for the scrollbar protocol and its default bindings. The scrollbar protocol defines the messages exchanged between scrollbar widgets and the scrollable widgets (entry, text, listbox, and canvas widgets) to which they are related. Thus, when you use the mouse or keyboard to scroll the text widget in simple_scroll.tcl, it sends messages to the scrollbar indicating the text’s current position in the text widget, which the scrollbar uses to adjust its appearance (such as the slider’s height and its location in the trough). Likewise, when you use the slider to scroll the text in the text widget, the scrollbar widget sends messages to the text widget telling it how to update the text displayed in the viewport.

The information to take away from this is that scrollbars have a protocol that defines how scrollbars and their associated widgets stay in sync. I have relied on the default values of the protocol. You should be able to do the same for a long time before you need to dig into the innards of the protocol and learn to use other scrollbar commands and attributes to modify the protocol’s default behavior.

Similarly, my description of Tk’s scrollbar widget assumes that you use its default bindings. You have less need to modify the default bindings for the scrollbar widget than you do to tweak the protocol settings. The capability exists (using the bind command described in Chapter 10), and it isn’t difficult or complicated to do so. However, most users have been heavily conditioned to expect scrollbars to behave a certain way. Consequently, changing that behavior in the absence of a compelling reason to do so violates the principle of least surprise and will usually confuse, if not downright annoy, your users.

Note: The Principle of Least Surprise

Note: The Principle of Least Surprise

When applied to user interfaces, the Principle of Least Surprise, also known as the Principle of Least Astonishment, the Rule of Least Surprise, or the Rule of Least Astonishment, boils down to, “When creating a user interface, do the least surprising thing.” As Eric Raymond writes in The Art of UNIX Programming (http://www.faqs.org/docs/artu/ch11s01.html):

The Rule of Least Surprise is a general principle in the design of all kinds of interfaces, not just software: “Do the least surprising thing.” Thus, to design usable interfaces, it’s best when possible not to design an entire new interface model (Eric Steven Raymond, The Art of UNIX Programming, Chapter 11).

In other words, if it isn’t broken, don’t fix it.

Probing the scrollbar Protocol

As explained in the previous section, when you move a scrollbar, it calls the command specified by its -command attribute, passing some additional parameters that specify the requested operation. The related widget (suppose it is a text widget) responds to this command (using its -xview or -yview attribute, for example) to update the display. To complete the scrolling operation, the scrollbar’s position and size have to be updated. This is accomplished by the text widget invoking the command specified in its -xscrollcommand or -yscrollcommand attribute (the set command in simple_scroll.tcl), passing parameters back to the scrollbar that tell the scrollbar how to update its size and position.

The scrollbar’s set command takes two arguments, first and last, real numbers between zero and one (0.0 and 1.0) that indicate the position of the top and bottom (or left and right for horizontal scrollbars) of the widget’s display. The first argument specifies the (relative) position of the top of the widget; the last argument specifies the (relative) position of the bottom of the widget’s viewport. In effect, first indicates an offset: how far down from the top or in from the left of the item being viewed in the widget the viewport is. Similarly, last indicates how much of the item in the widget is currently in the viewport.

In the following script, mod_scroll.tcl, I’ve modified the simple_scroll.tcl script presented earlier, replacing the scrollbar’s set command with a wrapper procedure, Scroll, that displays the values passed to the set command:

proc ReadFile {f} {
    set fileId [open $f r]
    set input [read $fileId]
    close $fileId
    return $input
}

proc Scroll {sb args} {
    foreach {first last} $args {
        puts "first=$first, last=$last"
    }
}

set t [text .t -background #ffffff]
$t insert end [ReadFile "README"]
set sb [scrollbar .y -command [list $t yview]]
$t configure -wrap word -yscrollcommand {Scroll $sb}

grid $t $sb -sticky nsew
Marta Justak, 14tclbook-Fi.doc
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
grid columnconfigure . 1 -weight 0

The key piece of code is the Scroll procedure. It takes the scrollbar widget as a required argument. The keyword args specifies optional arguments. Before executing the scrollbar's set command, it displays the values of the arguments passed to set. Astute readers will notice that the script itself does not pass values to the set command. If you don’t explicitly pass values, the scrollbar protocol makes some intelligent assumptions on your behalf—the purpose of this script is to show you what those “intelligent assumptions” are.

To generate the output below, I started the script, which displayed the first two lines of output. When the scrollbar is first mapped, it has no length, thus the offset and size are both 0 (first=0, last=0). The second line of output appears after the text widget is filled with the text read from the input file. The offset is still 0 because the viewport is at the top of the widget. The size value, though, has changed to reflect the fact that just over half of the document (last=0.521739) is visible in the viewport.

Next, I pressed Ctrl+End to scroll to the end of the document, resulting in the third line of output, first=0.478261, last=1. This output indicates that the top of the viewport is just under halfway through the document being displayed. The bottom of the document is visible, as indicated by the output last=1. Finally, I clicked arrow1 (the arrow at the top of the scrollbar) twice, scrolling two units up into the document, resulting in the fourth and fifth lines of output:

$ ./mod_scroll.tcl
first=0, last=0
first=0, last=0.521739
first=0.478261, last=1
first=0.456522, last=0.978261
first=0.434783, last=0.956522

The point of this is to demonstrate that you can, if you wish, interact directly with the scrollbar protocol, but that doing so is ugly and, in most cases, unnecessary.

Using the scale Widget

Tk’s scale widget, referred to as a slider in other GUI toolkits, displays a slider that can move back and forth or up and down in a trough. You decide the range of values the widget displays by assigning numeric values with the -from and -to attributes. As the slider moves along the trough, the scale widget’s current value changes. You can access the current value of the widget through its -variable attribute.

The following example uses a scale widget to set the maximum amount of time, in seconds, allowed to elapse between turns (see interval.tcl in this chapter’s code directory on the Web site):

scale .s -from 30 -to 60 -orient horizontal -length 200 
    -label "Time out duration (secs):" -tickinterval 5 -showvalue true
grid .s -padx 10 -pady 10

The scale command creates a scale widget whose value ranges from 30 to 60. It will be laid out horizontally. The attribute -tickinterval 5 creates tick marks below the slider that increment in units of 5. The -showvalue true attributes cause the scale widget to display its current value between the top of the scale itself and the label.

Figure 14.7 shows the resulting window.

Moving the slider left and right changes its value.

Figure 14.7. Moving the slider left and right changes its value.

The scale widget supports a reasonably standard set of attributes that control its size, appearance, and behavior. Table 14.1 highlights the scale-specific attributes that you need to know or that you haven’t encountered in the discussions of widgets in the previous chapters. As usual, for more information and to view the full list of both standard and scale widget-specific options, please refer to the scale man page (man 3tk sscale).

Table 14.1. Attributes of the scale Widget

Attribute

Description

-bigincrement

Defines the size of “large” increments by which to adjust the scale.

-cursor

Specifies the mouse cursor to display when the mouse hovers over the scale widget.

-digits

Sets the number of significant digits to retain when converting scale values to string values.

-from

Defines the smallest value the scale widget can take (displayed on the left or at the top of the widget).

-label

Specifies the text label that appears above or to the left of the scale itself.

-length

Sets the length of the widget in screen units (pixels by default).

-orient

Defines the orientation of the widget; must be either horizontal or vertical.

-repeatdelay

Specifies the number of milliseconds a button or key must be pressed before it begins to auto-repeat.

-repeatinterval

Sets the number of milliseconds between auto-repeats.

-resolution

Defines the value to which the scale itself and the tick marks will be rounded; defaults to 1, meaning values will be integral.

-showvalue

If true, the current value of the scale will be displayed.

-sliderlength

Specifies the length of the slider in screen units (pixels by default).

-sliderrelief

Sets the relief style of the slider.

-tickinterval

Defines the interval between tick marks.

-to

Specifies the largest value the scale widget can take (displayed on the right or at the bottom of the widget).

-troughcolor

Sets the color of the trough.

-variable

Defines the variable whose value is the widget’s current value.

Although the scale widget supports a rich set of attributes, the list of operations you can perform with it is limited, which is perhaps unsurprising when you consider that its function is limited to moving a slider back and forth (or up and down) in a trough. Table 14.2 lists the operations you can perform on a scale widget.

Table 14.2. Operations for the scale Widget

Operation

Description

$s coords ?value?

Returns a two-element list consisting of the x and y coordinates of the point along the center of the trough that corresponds to value, or to the scale’s current value if value is omitted.

$s get ?x y ?

Returns the scale’s current value or, if x or y are specified, the value of the widget at the indicated coordinate(s).

$s identify x y

Returns a string indicating what part of the scale lies under the specified x and y coordinates; the returned value will be slider, trough1 (that part of the trough to the left or above the slider), or trough2 (that part of the trough to the right or below the slider).

$s set value

Sets the value of the widget to value, which moves the slider to that position.

Using the Text Widget

The text widget is perhaps Tk’s most sophisticated widget. Naturally, you can use it to display and edit text, but its capabilities extend far beyond mere text display and manipulation. A reasonably complete list of the text widget’s features includes, in no particular order, the following elements:

  • Controlling line spacing and justification

  • Setting the font family, size, weight, and color

  • Moving around within the text using marks

  • Executing commands and setting text attributes using tags

  • Displaying images

  • Inserting, modifying, and deleting text

  • Cutting, copying, and pasting text

  • Adjusting tab stops

  • Selecting text

  • Searching text

  • Embedding other Tk widgets into the text widget

  • Undoing and redoing edit operations

Getting Started

I’ll start with the following script, simple_text.tcl in this chapter’s code directory:

proc ReadFile {f} {
    set fileId [open $f r]
    set input [read $fileId]
    close $fileId
    return $input
}

set t [text .t -height 25 -width 80 -background #ffffff]
$t insert end [ReadFile "README"]
grid $t

This script creates a window consisting of a single text widget that is 25 lines tall (-height 25) and 80 characters wide (-width 80) with a white background (-background #ffffff). After creating the widget, I insert the contents of the file named README at the “end” of the widget ($t insert end [ReadFile "README"]). Because the text widget is initially empty, the “end” in this case is actually the top. The ReadFile procedure opens the specified file, reads its contents into a string variable, and then returns that string to the caller. Figure 14.8 shows the resulting figure.

The text widget supports text editing with no additional code.

Figure 14.8. The text widget supports text editing with no additional code.

You can insert, delete, and modify text in the text widget immediately, that is, without making any changes in the code. Scrolling is a bit more awkward if you don’t have a mouse with a wheel, but you can use the keyboard arrow keys to scroll through the document. Although you can modify the text, you won’t be able to save your changes because I haven’t provided that functionality. The next few sections add a scrollbar and a menu that allows you to open files, save files, and exit the script.

Before I start showing off the features of the text widget, you’ll likely want to know the attributes and options you can use. There are a lot of them, as Table 14.3 makes clear, and this is not the complete list (refer to the text man page for the comprehensive list).

Table 14.3. Arguments for the text Widget

Argument

Description

-autoseparators

If 1 or true, automatically inserts separators in the undo stack (used with -undo).

-height

Specifies the height of the text widget in lines of text.

-maxundo

Sets the maximum number of undo operations.

-spacing1

Defines the amount of additional space (in screen units) above each line of text.

-spacing2

Defines the amount of additional space above and below wrapped lines of text.

-spacing3

Defines the amount of additional space (in screen units) below each line of text.

-state

Controls whether text can be inserted (normal) or not (disabled) in the widget.

-tabs

Sets the tab stops in the widget.

-undo

If I or true, enables the undo mechanism; defaults to 0 (no undo capability).

-width

Specifies the width of the text widget in characters.

-wrap

Defines the wrapping behavior of the widget; must be one of none, char, or word.

-xscrollcommand

Sets the command used to communicate with the horizontal scrollbar widget, if one exists.

-yscrollcommand

Sets the command used to communicate with the vertical scrollbar widget, if one exists.

delete

Deletes a range of characters from the text, as specified by index arguments.

dlineinfo

Returns a five-element list describing the geometry of the area containing the specified index.

dump

Returns the contents of the text widget between specified indices, including information about tags, marks, and embedded windows.

edit

Provides a facility for modifying the contents of the undo stack.

get

Returns a range of characters from the widget.

index

Returns the position that corresponds to the specified index.

insert

Inserts text and optional tags into the widget starting at the specified index.

mark

Provides the ability to create, modify, delete, and interact with text marks.

search

Searches text for a specified pattern, beginning at specific index.

see

Scrolls the text at the specified index into view.

tag

Provides the facility for working with tags.

xview

Changes the horizontal position of the text in the widget.

yview

Changes the vertical position of the text in the widget.

Adding a Scrollbar

Using what you learned about the scrollbar widget in the previous section, adding a scrollbar to simple_text.tcl involves just a few lines of code. In the interests of simplicity, I’m only going to add a vertical scrollbar. I’ll use the text widget’s -wrap attribute so that text wraps automatically. If you follow the rules I suggested, you need to do three things:

  1. Add the scrollbar widget to the window.

  2. Invoke the text widget’s yview operation in the scrollbar widget’s -command attribute.

  3. Invoke the scrollbar widget’s set operation from the text widget’s -yscrollcommand attribute.

The resulting script, scroll_text.tcl in this chapter’s code directory, is shown below:

proc ReadFile {f} {
    set fileId [open $f r]
    set input [read $fileId]
    close $fileId
    return $input
}

set t [text .t -height 25 -width 80 -background #ffffff -wrap word]
set s [scrollbar .s]

$s configure -command [list $t yview]
$t configure -yscrollcommand [list $s set]

$t insert end [ReadFile "README"]

grid $t $s -sticky nsew
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
grid columnconfigure . 1 -weight 0

The ReadFile procedure is unchanged from the previous scripts. I added a new attribute to the command that creates the text widget, -wrap word. This attribute controls how text that is too wide to fit in the text widget is handled. It can be one of three values: none, char, or word. A value of none causes text to be truncated; char allows text to be wrapped at any characters; word breaks text at word boundaries (white space).

The scrollbar command creates a scrollbar widget named .s, a reference to which is stored in the variable $s. I don’t specify any attributes when creating the widget, so it assumes default values.

After creating the widgets, the two configure operations connect the scrollbar and text wid-gets using the rules I gave you in the previous section. I set the scrollbar’s -command attribute to the text widget’s yview operation. Then I set the text widget’s -yscrollcommand attribute to the scrollbar’s set operation.

After populating the text widget using my now-familiar ReadFile procedure, I lay out the widgets, using the same procedure described for the simple_scroll.tcl script in the previous section. Figure 14.9 shows the resulting window. Figure 14.10 shows the window when it is scrolled to the bottom of the text widget’s contents. Figure 14.11 illustrates selected text. Figure 14.12 proves that you can, in fact, edit the contents of a text widget without writing any code.

The text widget now has a linked scrollbar.

Figure 14.9. The text widget now has a linked scrollbar.

Use the scrollbar to scroll the viewport to the bottom of the text widget’s contents.

Figure 14.10. Use the scrollbar to scroll the viewport to the bottom of the text widget’s contents.

Selecting text works the way you would expect.

Figure 14.11. Selecting text works the way you would expect.

Yes, Virginia, you can edit text without writing code.

Figure 14.12. Yes, Virginia, you can edit text without writing code.

One change from all the scripts I have shown you previously is that I use the list command to build the command specified as the attribute for -command and -yscrollcommand. The reason for this change is that the list commands handle quoting and spaces embedded in arguments automatically. Using double quotes for grouping would not deal with the embedded spaces. While using braces around the arguments would handle embedded spaces and accomplish the needed grouping, it would also inhibit substitutions that you might otherwise need performed. Although this is not an issue in these two commands, as your commands become more sophisticated, quoting and embedded spaces become a real consideration, and, as you’ve seen throughout this book, Tcl (and thus Tk) are more sensitive to white space than other programming languages.

Adding and Populating a Menu

As promised, this section shows you how to add a menu, open an arbitrary file, and save the contents of the text widget to an arbitrary file. Although it probably seems like I’m creating a text editor (and, truthfully, I am gradually evolving a simple-minded text editor), what I’m really doing is using simple examples to show you how to perform typical operations with and on text widgets. I also hope to convey that just a few lines of Tcl and Tk code make it possible to create scripts that are surprisingly capable compared to the amount of code required to implement them.

The following script, menu_text.tcl in this chapter’s code directory, shows the latest iteration of the simple_text.tcl script with which I started:

proc ReadFile {w} {
    set f [tk_getOpenFile -title "Open file"]
    # Bail if no filename is specified
    if {$f == ""} {
        return
    } else {
        set fileId [open $f r]
        # Clear the current contents of $w first
        $w delete 1.0 end
        # Read straight from the file into $w
        $w insert end [read $fileId]
        close $fileId
    }
}

proc SaveText {w} {
    set f [tk_getSaveFile -title "Save file"]
    # Bail if no filename is specified
    if {$f == ""} {
        return
    } else {
        set fileId [open $f w]
        puts -nonewline $fileId [$w get 1.0 "end - 1 chars"]
        close $fileId
    }
}

set t [text .t -height 25 -width 80 -background #ffffff -wrap word]
set s [scrollbar .s]

$s configure -command [list $t yview]
$t configure -yscrollcommand [list $s set]

set main [menu .main]
. config -menu $main
set mFile [menu $main.mFile -tearoff 0]
$main add cascade -label "File" -menu $mFile
$mFile add command -label "Open" -command [list ReadFile $t]
$mFile add command -label "Save" -command [list SaveText $t]
$mFile add separator
$mFile add command -label "Exit" -command exit

grid $t $s -sticky nsew
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
grid columnconfigure . 1 -weight 0

There’s a lot going on in this script. I’ve modified the ReadFile procedure and added a new one, SaveText. Both accept a single argument, the widget into which to dump the contents of a file (ReadFile) or from which to read the text to save to a file (SaveText). ReadFile uses the tk_getOpenFile command to create a ready-made file open dialog box (see Figure 14.14). tk_getOpenFile returns the name of the file selected in the dialog, so I use that name in the open command. Another change to ReadFile is that I read the contents of the file directly into the text widget, instead of returning the text as a string variable. The condition if {$f == ""} is necessary because the user can close the file open dialog without selecting a file (by clicking the Cancel button). If that happens, I just exit the ReadFile procedure.

The SaveText procedure works similarly. The tk_getSaveFile command creates a ready-made file save dialog box, so if the user selects a file, I open it and use the command puts [-nonewline $fileId [$w get 1.0 "end–1 chars"] to get the contents of the text widget and save it directly into the file (see Figure 14.17). If the user doesn’t select a file in the file save dialog box, I just exit SaveText without taking any action.

The rest of the new code in this script adds the menu and menu entries. First, I create the menu bar itself (set main [menu .main]) and associate it with the root window (. config -menu $main). Next, I create a File menu item and add it to the $main menu. Then I add four entries to the File menu: Open, Save, a separator, and Exit:

  1. The Open entry invokes the ReadFile procedure.

  2. The Save entry invokes the SaveText procedure.

  3. The separator entry creates visual separation between the Open and Save entries and the Exit item.

  4. The Exit entry terminates the script (without saving any changes to the contents of the text widget).

The rest of the script consists of the same layout commands that I used in the previous iterations of this script, so I won’t rehash them here. Figures 14.1314.16 show the new Tk features that menu_text.tcl uses.

Use the File menu to open and save files and exit the script.

Figure 14.13. Use the File menu to open and save files and exit the script.

The tk_getOpenFile command creates a fully featured file open dialog.

Figure 14.14. The tk_getOpenFile command creates a fully featured file open dialog.

The script is starting to look like a proper, if simple, text editor.

Figure 14.15. The script is starting to look like a proper, if simple, text editor.

Save your changes using the dialog created by tk_getSaveFile.

Figure 14.16. Save your changes using the dialog created by tk_getSaveFile.

Using Marks and Tags

I think you’ll derive the most benefit from marks and tags. Marks are beneficial because they enable you to move around within the contents of a text widget programmatically, find where the user (or at least the current insertion point) is in the text, and modify the contents of the widget (in terms of adding or deleting text). Tags are most commonly used to apply visual attributes to the text in a text widget. Other uses of tags include binding commands to ranges of text and manipulating the text selection. Marks and tags are certainly the two features upon which the word search game at the beginning of the chapter relied. Before I get to marks and tags, though, you’ll need to know how to find your way around inside a text widget.

Text Indices

A fair portion of the commands, options, and attributes for text widgets expect one or more index arguments that specify the text within the widget on which to operate. Given the number of operations that you can perform, there is a correspondingly large number of expressions used to specify index values. These expressions fall into two broad categories: bases and modifiers. Bases define the starting point for an operation. Modifiers define the direction in which an operation works or the number of characters on which to operate. All text operations require a base or starting point; modifiers are optional. Table 14.4 lists the expressions used to specify text index values and whether the expression represents a base value or a modifier.

Table 14.4. Expressions Used for Text Indices

Expression

Type

Description

- N chars

Modifier

Moves the index backward by N characters.

- N lines

Modifier

Moves the index backward by N lines.

@x,y

Base

Refers to the character that covers the pixel at widget-relative coordinates x and y.

+ N chars

Modifier

Moves the index forward by N characters.

+ N lines

Modifier

Moves the index forward by N lines.

end

Base

Refers to character immediately following the last newline.

line.char

Base

Refers to character on line line at character position char. Line numbers are 1-based; character positions are 0-based.

lineend

Modifier

Moves the index to the last character (the newline) on the line.

linestart

Modifier

Moves the index to the first character on the line.

mark

Base

Refers to the character immediately following the mark named mark.

tag.first

Base

Refers to the first character in the tag named tag.

tag.last

Base

Refers to the last character in the tag named tag.

wordend

Modifier

Moves the index to end of the word containing the current index.

wordstart

Modifier

Moves the index to the beginning of the word containing the current index.

The following script, index_text.tcl, illustrates how to use some of the text indexing operations shown in Table 14.4.

proc ReadFile {f} {
    set fileId [open $f r]
    set input [read $fileId]
    close $fileId
    return $input
}

proc DoTag {w c} {
    global status

    $w tag delete t
    $w tag configure t -background [$w cget -foreground]
    $w tag configure t -foreground [$w cget -background]
    $w tag add t [$w index insert] "[$w index insert] $c"

    $status configure -text "Current tag range: [$w tag ranges t]"

}

set t [text .t -height 25 -width 80 -background #ffffff]
$t insert end [ReadFile "README"]

set f [frame .f]
set b1 [button $f.b1 -text "+ 5 chars" -command [list DoTag $t "+ 5 chars"]]
set b2 [button $f.b2 -text "+ 5 lines" -command [list DoTag $t "+ 5 lines"]]
set b3 [button $f.b3 -text "lineend" -command [list DoTag $t lineend]]
set b4 [button $f.b4 -text "wordend" -command [list DoTag $t wordend]]

set status [label .status -relief sunken -anchor w]

grid $f -sticky nsew
grid $b1 $b2 $b3 $b4 -sticky nsew
grid $t -sticky nsew
grid $status -sticky nsew

The script uses tags (and marks), which you haven’t learned how to use yet, but they are only a means to an end, providing visual evidence of how text indices work. The gist of the script is just this: Using your mouse, click somewhere in the text with the left mouse button. This sets the insertion point (see Figure 14.17). After setting the insertion point, click one of the buttons at the top of the window. This moves the index from the insert point to the index specified on the button label (five characters forward, five lines forward, the end of the current word, or the end of the current line). In addition to moving the index point, the DoTag highlights the range of text (Figure 14.18) between the insertion point and the specified index, resulting in a visual illustration that the index has, in fact moved.

Click somewhere in the text to set the insert point.

Figure 14.17. Click somewhere in the text to set the insert point.

The highlighted text includes the text between the insertion point and the requested index.

Figure 14.18. The highlighted text includes the text between the insertion point and the requested index.

Hitting the Mark

In Tk’s terminology, a text mark is a name that refers to a space between two particular characters. Marks are ordinarily used as reference points in operations that require indices. You can use any character when naming marks, but I recommend not using strictly numeric names, the plus sign (+), the minus sign (-), or spaces, because these elements are used when performing index arithmetic. Names containing these characters potentially complicate index math.

In addition to the marks you create, widgets that support marks include a few predefined marks that cannot be deleted with the mark unset operation listed in Table 14.5. The two most important predefined marks are insert, which refers to the location of the insertion point (where text will be inserted), and current, which refers to the character closest to the mouse cursor. Marks are also persistent. If the text surrounding a mark is deleted, the mark remains in place.

Table 14.5. Supported Mark Operations

Operation

Description

$t mark gravity name? direction?

If direction is not specified, returns the gravity of the mark denoted by name; otherwise, sets the gravity of the specified mark to either right or left (defaults to right).

$t mark names

Returns a list of all currently defined marks.

$t mark next index

Returns the name of the next mark occurring at or after index, if any, or the empty string otherwise.

$t mark previous index

Returns the name of the next mark occurring at or before index, if any, or the empty string otherwise.

$t mark set name index

Defines a mark named name immediately before the character specified by index.

$t mark unset name?...?

Deletes the mark or marks specified by name.

Notice that a mark resides between two characters. Depending on the value of the mark’s gravity (which defaults to right), text will be inserted to the left or the right of the mark. If a mark’s gravity is left, text will be inserted to the left of the mark; right gravity means that text will be inserted to the right of the mark. Referring back to index_text.tcl, for example, the expression $t index insert returns the index of the character immediately to the right of the insert mark.

Table 14.5 lists the supported mark operations, which are invoked using the mark command after the name of the widget that supports them (such as text widgets).

The following script, mark_text.tcl in this chapter’s code directory, shows all of the currently defined marks in a text widget when you click the Show Marks button.

proc ReadFile {f} {
    set fileId [open $f r]
    set input [read $fileId]
    close $fileId
    return $input
}

proc ShowMarks {t} {
    global status

    foreach m [$t mark names] {
        append s "$m: [$t index $m], "
    }
    $status configure -text "[string trimright $s {, }]"
}

set t [text .t -height 25 -width 80 -background #ffffff]
$t insert end [ReadFile "README"]

set f [frame .f]
set b [button $f.b -text "Show Marks" -command [list ShowMarks $t]]
set e [button $f.e -text "Exit" -command exit]
set status [label .status -relief sunken -anchor w]

grid $f -sticky nsew
grid $b $e -sticky nsew
grid $t -sticky nsew
grid $status -sticky nsew

The workhorse code in this script is the ShowMarks procedure, which is invoked by the button b. ShowMarks iterates through the list of mark names returned by the mark names command, appending the name of each mark and the index value to which it corresponds to the string variable $s. After exiting the foreach loop, I update the text of the label that appears below the text widget with the value of this string, after removing the terminating comma and space.

As you can see in the following figures, the list and value of marks defined automatically by the text widget changes, depending on the state of the text in the widget. For example, in Figure 14.19, I’ve just started the script and have neither selected any text nor placed an insertion point using the mouse or keyboard. As a result, the insert mark is at index 46.0, that is, on line 46, character 0, which corresponds to the beginning of the line just past the end of the file. The current mark is at index 1.0, because that was the character closest to the mouse cursor when I clicked the Show Marks button.

A newly populated text widget only has two predefined marks.

Figure 14.19. A newly populated text widget only has two predefined marks.

After placing the insertion point (see Figure 14.20), there’s a third mark automatically defined, anchor, with a value of 13.31, in addition to insert (at 13.31) and current (still at 1.0). Recall from Chapter 13 that an anchor point is the base from which a selection begins.

After placing an insertion point, the text widget defines a third mark.

Figure 14.20. After placing an insertion point, the text widget defines a third mark.

Notice what happens when I double-click on the word sources on line 13 to select it. Selecting text changes the marks subtly, as you can see in Figure 14.21.

Selecting text sets the anchor mark where the selection began.

Figure 14.21. Selecting text sets the anchor mark where the selection began.

The anchor mark moved to where I double-clicked index 13.32, which corresponds to the space between the letters r and c in the word sources. The insert mark, meanwhile, moved to the end of the word sources, or index value 13.35.

Tag, You’re It!

Tags are a special annotation for text, similar to marks but far more capable and versatile. Whereas text marks are used for positioning and movement purposes, tags have a wider variety of uses. They are most commonly used to apply visual attributes to the text in a text widget, including some that are not available as global attributes (such as strikethrough and stippling). Table 14.7 lists all of the text attributes you can apply with tags. Other uses include bind commands to ranges of text, and manipulating the text selection (described in Chapter 13). In addition, unlike marks, one tag can be applied to multiple ranges of text, and multiple tags can be applied to a single range of text.

Table 14.6 lists the operations you can perform with text tags.

Table 14.6. Tag Operations

Operation

Description

$t tag add name start? stop? ?...?

Applies the tag name to the text from the index start up to but not including stop, or just start if stop isn’t specified.

$t tag bind name? seq? ?script?

Returns the binding(s) defined for the tag name or assigns the script script to sequence in seq and applies this binding to name.

$t tag delete name ?...?

Deletes the tag specified by name.

$t tag lower name? below?

Assigns the lowest available priority to the tag name or makes its priority less than the priority assigned to the tag below.

$t tag nextrange name start ? stop?

Returns a two-element list of indices for the next range to which the tag name is applied.

$t tag prevrange name start ? stop?

Returns a two-element list of indices for the previous range to which the tag name is applied.

$t tag raise name ? above?

Assigns the highest available priority to the tag name or makes its priority higher than the priority assigned to the tag a bove.

$t tag ranges name

Returns a list of all the index ranges to which the tag name has been applied.

$t tag remove name start ? stop?

Removes the tag name from index specified by start or, if stop is specified, from the index range from start up to but not including stop.

Table 14.7. Supported Tag Attributes

Attribute

Description

-background color

Sets the background color of the tagged text.

-bgstipple bitmap

Defines the bitmap used for the background stipple.

-borderwidth pixels

Specifies the width of the border for 3D effects.

-elide boolean

If true (or the equivalent), text tagged with this attribute is hidden.

-fgstipple bitmap

Defines the bitmap used for the foreground stipple.

-font fontname

Sets the font used for the tagged text.

-foreground color

Specifies the foreground color of the tagged text.

-justify type

Defines the justification of the tagged text (left, center, or right).

-lmargin1 pixels

Sets the spacing for left indentation of tagged text.

-lmargin2 pixels

Sets the spacing for left indentation of tagged text that gets wrapped.

-offset pixels

Specifies the offset from the baseline for superscripted (positive) or subscripted (negative) text.

-overstrike boolean

If true (or the equivalent), text tagged with this attribute is displayed with a horizontal line through it (also referred to as strikethrough).

-relief type

Defines the type of relief, which must be one of flat, sunken, raised, groove, solid, or ridge.

-rmargin pixels

Sets the size of the right-hand margin.

-spacing1 pixels

Specifies the amount of space above a line.

-spacing2 pixels

Specifies the amount of space above the wrapped part of a line.

-spacing3 pixels

Specifies the amount of space below a line.

-tabs tablist

Defines the tab stops for the tagged text.

-underline boolean

If true (or the equivalent), the tagged text is underlined.

-wrap mode

Sets the line wrap style, which must be one of none (the default), char, or word.

Analyzing Word Search

Readers can use this program to experiment with text attributes, manipulating the insertion point, modifying line spacing, and keeping track of the text cursor.

Looking at the Code

#!/usr/bin/wish
# gword_search.tcl
# Word search game

# Block 1
# Read the puzzle data from the specified file
proc ReadFile {f} {
    global words lines

    set fileId [open $f r]
    while {[gets $fileId input] > -1} {
        lappend words [lindex $input 0]
        lappend lines [lrange $input 1 end]
    }

    close $fileId

}
# Block 2
# Clears all tags
proc Clear {t} {
    $t tag remove correct 1.0 end
    $t tag remove incorrect 1.0 end

}

# Block 3
# Determine if the text selected in t is the target word
proc Score {t} {
    global lStatus words

    # The starting and ending points of the selection
    if {[$t compare anchor < insert]} {
        set start [$t index anchor]
        set end [$t index insert]
    } else {
        set start [$t index insert]
        set end [$t index anchor]
    }

    # Fetch the selected text
    set word [join [string trim [selection get]] ""]

    # Determine the line number of the selected text    set n [string range $start 0
[expr [string first "." $start] - 1]]

    # What's the target word?
    set target [lindex $words [expr $n - 1]]

    # Is it a match?
    if {$word == $target } {
        $t tag add correct $start $end
    } else {
        $t tag add incorrect $start $end
    }
}

# Block 4
# Define the widgets
set fPuzzle [frame .fpuzzle]
set fButtons [frame .fbuttons]
set lStatus [label .lstatus -relief sunken -borderwidth 2 -anchor w]
set puzzle [text $fPuzzle.puzzle -width 21 -height 9 -bg #ffffff 
    -font "Courier"]
set bScore [button $fButtons.bscore -text "Score" -anchor n 
    -command [list Score $puzzle]]
set bClear [button $fButtons.bclear -text "Clear" -anchor n 
    -command [list Clear $puzzle]]
set bExit [button $fButtons.bexit -text "Exit" -anchor n -command exit]

# Display the widgets
grid $fPuzzle -column 0 -row 0 -padx {10 5} -pady 10
grid $fButtons -column 1 -row 0 -padx {5 10} -pady 10
grid $puzzle -padx {5 0}
grid $bClear -sticky nsew -padx {5 5} -pady {10 2}
grid $bScore -sticky nsew -padx {5 5} -pady {2 2}
grid $bExit -sticky nsew -padx {5 5} -pady {20 10}
grid $lStatus -columnspan 2 -sticky nsew
wm title . "Word Search"

# Block 5
# Populate the text widget
set words {}
set lines {}

ReadFile "puzzle.txt"
for {set i 0} {$i < [llength $lines]} {incr i} {
    $puzzle insert end [format "%s
" [lindex $lines $i]]
}

# Make the puzzle text read-only
$puzzle configure -state disabled

# Define tags
$puzzle tag configure correct -background "dark green"
$puzzle tag configure incorrect -background "dark red"

Understanding the Code

The ReadFile procedure in Block 1 opens the file whose name is specified as an argument and then parses the contents of the file into two lists: words and lines. These variables are declared global to enable ReadFile to access and modify them. The $words list contains the target words, and the $lines list contains the jumbled letters that make up the puzzle itself. The format of the puzzle data file is a word followed by a space followed by a series of 11 space-delimited letters:

open e o p e n u g r i v c

...

"" z o t z g v a n e r s

...

tell a m a j y r a t e l l

Notice that once again, the puzzle contains a line that lacks a valid word.

Block 2 defines the Clear procedure, which removes the two tags from the puzzle text. It is called when the player presses the Clear button to reset the game board and restart the game.

In Block 3, I define the Score procedure that is invoked when the player presses the Score button after selecting a candidate word. The first if-else block determines the starting and ending points of the selection. The conditional evaluation is necessary because the anchor point of a selection isn’t necessarily the beginning point of a selection. I’m going to use the starting and ending points of the selection (saved in the $start and $end variables, respectively) to add a tag to the selected text, and the syntax for adding a tag requires that the start value should be less than the end value.

Next, I retrieve the selected word using four levels of Tcl command substitution. Reading from the inside out of the expression set word [join [string trim [selection get]] ""], I do the following:

  1. Use selection get to fetch the text of the selection from the clipboard.

  2. Use the string trim command to remove any leading and trailing spaces from the text returned by selection get.

  3. Use the join command to remove embedded space characters from the string returned by string trim.

  4. Save the string returned by join in the variable $word.

Once I know the word the player has selected, I need to find from which line of the puzzle the player selected the word so I can use that line number to index into the list of target words (stored in $words) to see if I have a match. To determine the line number, I use the string first command to parse the line number of the index value stored in $start. I store the line number in the variable $n and then use the lindex list command to extract the corresponding word from the list of target words. Because the text widget’s lines are numbered from one while lists are numbered from zero, I have to subtract 1 from the value stored in $n to extract the correct word from the list of target words. I store the target word in the string variable $target.

Finally, I compare the word the player selected ($word) to the target word ($target). If they match, I add the tag correct to the text range that the player selected, which highlights it in green. If the two strings don’t match, I add the tag incorrect to the text range, which highlights it in red. After tagging the selected text range, the procedure exits.

In Block 4, I define and display the widgets that make up the game. The only remarkable feature in this block involves being careful when defining the text widget itself. Specifically, when displaying the puzzle text, I use a monospace font, Courier, to make sure that each letter takes up the same amount of vertical and horizontal space in the text widget. This measure ensures that the letters line up evenly both horizontally and vertically. If I use a proportionally spaced font, such as Times, the letters will not align properly.

After I lay out the various widgets, I populate the text widget with the puzzle data. First, I read the data file (puzzle.txt) with the ReadFile procedure, which stores data in the $words and $lines lists as I described earlier. Next, I iterate over the $lines list and insert each line into the text widget. Once the text widget is populated, I make it read-only by setting its -state attribute to disabled ($puzzle configure -state disabled). The purpose of this step was to keep the player from accidentally editing the contents of the text widget during the game. I have to disable the text widget after populating it because it is not possible to insert, delete, or modify text in a text widget, even programmatically, if it is disabled.

The last step is to define the two tags, correct and incorrect, that I use to tag the text as the user plays the game. The tags are simple: The correct tag has a -background attribute of "dark green" while the incorrect tag has a "dark red" attribute.

At this point, the game is ready to play.

Modifying the Code

Here are some exercises you can try to practice what you learned in this chapter:

  • 14.1 Modify gword_search.tcl to keep track of the number of correct and incorrect selections and to display the results at the end of the game.

  • 14.2 Modify the Clear procedure in gword_search.tcl to clear only incorrect guesses.

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

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