Wcb Programmer's Guide

by

Csaba Nemethi

csaba.nemethi@t-online.de


Contents

Overview

Examples

Start page


Overview

Some common problems

Many Tcl/Tk programmers are confronted with questions like the following:

In most books, FAQs, newsgroup articles, and widget sets, you can find individual solutions to some of the above problems by means of widget bindings.  This approach is based on adding new binding tags or modifying some of the existing ones, which quite often proves to be incomplete.

The Tk core addresses just a few of the above problems:  In Tk 8.1 the <<ListboxSelect>> virtual event for listbox widgets was introduced, Tk versions 8.3 and higher support widget options for entry validation, and the spinbox widget (introduced in Tk 8.4) provides the same validation facility.  Finally, Tk 8.4 supports the disabled state for listbox widgets, as well as the modified flag, the <<Modified>> and <<Selection>> virtual events, and an undo/redo mechanism for text widgets.  However, also these improvements are of individual nature.

The package Wcb

The Widget callback package Wcb provides a completely different, general solution to the above problems:  Based on redefining the Tcl command corresponding to a widget, the main Wcb procedure callback enables you to associate arbitrary commands with some entry, spinbox, listbox, tablelist, and text widget operations.  These commands will be invoked automatically in the global scope whenever the respective widget operation is executed.  You can request that these commands be called either before or after executing the respective widget operation, i.e., you can define both before- and after-callbacks.  From within a before-callback, you can cancel the respective widget command by invoking the procedure cancel, or modify its arguments by calling extend or replace.

Besides these (and four other) general-purpose commands, the Wcb package exports the utility procedures changeEntryText, postInsertEntryLen, and postInsertEntryText, as well as some before-insert callbacks for entry, spinbox, and text widgets, which you can use directly or modify to suit your needs.  To learn how to do this, have a look at the Examples section below.

The package Wcb is implemented in pure Tcl/Tk code, which makes it completely platform-independent and very easy to install.  It requires version 8.0 or higher of both Tcl and Tk.

How to get it?

Wcb is available for free download from the Web page

http://www.nemethi.de

The distribution file is wcb2.8.tar.gz for UNIX and wcb2_8.zip for Windows.  These files contain the same information, except for the additional carriage return character preceding the linefeed at the end of each line in the text files for Windows.

How to install it?

Install the package as a subdirectory of one of the directories given by the auto_path variable.  For example, you can install it as a directory at the same level as the Tcl and Tk script libraries.  The locations of these library directories are given by the tcl_library and tk_library variables, respectively.

To install Wcb on UNIX, cd to the desired directory and unpack the distribution file wcb2.8.tar.gz:

gunzip -c wcb2.8.tar.gz | tar -xf -

This command will create a directory named wcb2.8, with the subdirectories demos, doc, and scripts.

On Windows, use WinZip or some other program capable of unpacking the distribution file wcb2_8.zip into the directory wcb2.8, with the subdirectories demos, doc, and scripts.

How to use it?

To be able to access the commands and variables defined in the package Wcb, your scripts must contain one of the lines

package require Wcb
package require wcb

You can use either one of the above two statements because the file wcb.tcl contains both lines

package provide Wcb ...
package provide wcb ...

You are free to remove one of these two lines from wcb.tcl if you want to prevent the package from making itself known under two different names.  Of course, by doing so you restrict the argument of  package require  to a single name.  Notice that the examples below use the statement  package require Wcb.

Since the package Wcb is implemented in its own namespace called wcb, you must either invoke the

namespace import wcb::pattern ?wcb::pattern ...?

command to import the procedures you need, or use qualified names like wcb::callback.  In the examples below we have chosen the latter approach.

To access Wcb variables, you must use qualified names.  There are only two Wcb variables that are designed to be accessed outside the namespace wcb:

Start page


Examples

Some before-insert callbacks for entry widgets

The script entrytest.tcl in the demos directory creates three entry widgets with the constraints shown in the following figure:

entrytest - Please load this image!

For the topmost entry .e1 we define two before-insert callbacks contained in the package Wcb:

wcb::callback .e1 before insert wcb::checkStrForAlnum \
                                wcb::convStrToUpper

To force the second entry .e2 to accept only integers of maximal length 10, we use again two before-insert callbacks from Wcb:

wcb::callback .e2 before insert {wcb::checkEntryLen 10} \
                                 wcb::checkEntryForInt

And finally, here are the two callbacks for the third entry widget .e3:

wcb::callback .e3 before insert {wcb::checkEntryLen 10} \
                                 checkNumber

#
# Callback procedure checkNumber
#
proc checkNumber {w idx str} {
    set newText [wcb::postInsertEntryText $w $idx $str]
    if {![regexp {^[0-9]*\.?[0-9]?[0-9]?$} $newText]} {
        wcb::cancel
    }
}

This last example also shows the arguments of the callbacks declared with the wcb::callback command:  Whenever a callback is invoked, the name of the original Tcl command for the widget as well as the arguments of the respective widget operation are automatically appended to it as parameters.  Since we defined checkNumber as a before-callback for the insert subcommand, its last three arguments must be: the name of the original entry widget command (w), the index (idx), and the string (str) to be inserted just before the character indicated by the index.

Notice that in the argument list of a Wcb callback, the name of the original Tcl widget command can be be preceded by any number of additional arguments.  The procedure wcb::checkEntryLen is an example of such a callback.

The command wcb::postInsertEntryText invoked in the procedure checkNumber returns the text that would be contained in the entry widget w after inserting the string str before the character indicated by the index idx.  If this text is not (the starting part of) an unsigned real number with at most two digits after the decimal point, then we call the procedure wcb::cancel, which aborts the insert command.

Without the constraint that the content of the third entry must not start with a sign, we could have used the callback procedure wcb::checkEntryForFixed instead of checkNumber:

wcb::callback .e3 before insert {wcb::checkEntryLen 10} \
                                {wcb::checkEntryForFixed 2}

A selset callback for a listbox

In the case of a listbox, you will probably most often want to define a callback for the  selection set  widget subcommand.  In most cases it does not matter whether this is a before- or after-callback.  Please note that the wcb::callback command expects the abbreviated form selset as parameter.  Similarly, you must pass selclear to this command when defining a callback for the  selection clear  listbox operation.

In the following example we build a listbox .lb containing the names of the bitmap files in the subdirectory images of the directory demos in the Tk library directory.  Whenever an item is selected, the callback procedure showBitmap  will display the corresponding bitmap in the label  .picture.

listboxtest1 - Please load this image!

Here is the relevant code fragment from the script listboxtest1.tcl, contained in the demos directory:

set dirName [file join $tk_library demos images]

#
# Frame .spacer and listbox .lb
#
frame .spacer -width 10
listbox .lb -height 0 -width 0 -background gray96 \
	-selectbackground navy -selectforeground white
set pattern [file join $dirName *.bmp]
foreach pathName [lsort [glob $pattern]] {
    .lb insert end [file tail $pathName]
}

#
# Label .picture
#
label .picture -relief sunken

#
# Define a before-selset callback for .lb
#
wcb::callback .lb before selset showBitmap

#
# Callback procedure showBitmap
#
proc showBitmap {w first args} {
    global dirName
    set pathName [file join $dirName [$w get $first]]
    .picture configure -bitmap @$pathName
}

Recall that the  selection set  listbox operation takes as arguments one or two indices, which will be passed automatically to the callback as parameters, along with the name of the original Tcl command associated with the listbox widget.  For this reason, the arguments of the callback procedure showBitmap are: the name of the original listbox widget command (w), the first index (first), as well as the args keyword representing the empty list or the optional second index passed to the  selection set  command.

An activate callback for a listbox

The listbox used in the preceding example has the default selection mode browse, hence the before-selset callback showBitmap will be fired every time the mouse is dragged from one element to another, with button 1 down.  But what happens if we want to display not only the bitmaps but also the photo images contained in the subdirectory images of the directory demos in the Tk library directory?  Loading a photo image is a much more complex operation than loading a bitmap, which can have the effect that some images cannot be displayed quickly enough to follow the mouse when browsing with it within the listbox.

listboxtest2 - Please load this image!

To solve this problem, we can either change the selection mode to have the less common value single, or arrange for the images not to be displayed when browsing with the mouse but when releasing its button 1.  The second method can be implemented with the aid of an activate callback, as shown in the following code fragment taken from the script listboxtest2.tcl, contained in the demos directory:

set dirName [file join $tk_library demos images]
image create photo photoImage

#
# Frame .spacer and listbox .lb
#
frame .spacer -width 10
listbox .lb -height 0 -width 0 -background gray96 \
	-selectbackground navy -selectforeground white
set pattern [file join $dirName *]
foreach pathName [lsort [glob $pattern]] {
    .lb insert end [file tail $pathName]
}

#
# Label .picture
#
label .picture -relief sunken

#
# Define a before-activate callback for .lb
#
wcb::callback .lb before activate showPicture

#
# Callback procedure showPicture
#
proc showPicture {w idx} {
    set leafName [$w get $idx]

    #
    # When traversing the listbox with the arrow keys, the value
    # of idx can become -1 or the number of listbox elements,
    # hence the value of leafName can be an empty string:
    #
    if {[string compare $leafName ""] == 0} {
        return ""
    }

    global dirName
    set pathName [file join $dirName $leafName]
    if {[string compare [file extension $pathName] .bmp] == 0} {
        .picture configure -bitmap @$pathName -image ""
    } else {
        photoImage configure -file $pathName
        .picture configure -bitmap "" -image photoImage
    }
}

Seven callbacks for a text widget

The script texttest.tcl in the demos directory creates the text widget shown in the following figure:

texttest - Please load this image!

Here is the relevant code fragment:

#
# Text .txt
#
set width 50
text .txt -width $width -height 12 -setgrid true -wrap none -background white
set txtFont [font actual [.txt cget -font]]
.txt configure -font [lreplace $txtFont 1 1 courier]
.txt tag configure prog -foreground red
.txt tag configure user -foreground DarkGreen
.txt insert end "Everything you type or paste into this window will\n"  prog
.txt insert end "be displayed in dark green.  You cannot make any\n"    prog
.txt insert end "changes or selections in this red area, and will\n"    prog
.txt insert end "not be able to send the message as long as any\n"      prog
.txt insert end "line contains more than $width characters.\n"          prog
.txt insert end "--------------------------------------------------\n"  prog
set limit [.txt index insert]

#
# Label .pos displaying the current cursor position
#
label .pos -textvar pos

#
# Button .send (actually, it does not send anything)
#
button .send -text Send -command exit

#
# Define 5 before- and 2 after-callbacks for .txt
#
wcb::callback .txt before insert protectRedArea changeColor
wcb::callback .txt before delete protectRedArea
wcb::callback .txt before selset protectRedArea
wcb::callback .txt before motion displayPos
wcb::callback .txt  after insert checkLines
wcb::callback .txt  after delete checkLines

#
# Callback procedure protectRedArea
#
# The parameters following w can be interpreted either as
# "index string ?tagList string tagList ...?" (for an insert
# callback), or as "from ?to?" (for a delete callback),
# or as "from ?to from to ...?" (for a selset callback).
#
proc protectRedArea {w idx args} {
    global limit
    if {[$w compare $idx < $limit]} {
        wcb::cancel
    }
}

#
# Callback procedure changeColor
#
proc changeColor {w args} {
    wcb::extend user
}

#
# Callback procedure displayPos
#
proc displayPos {w idx} {
    global pos
    set index [$w index $idx]
    scan $index %d.%d line column
    incr column
    set pos [format "Line: %d   Column: %d" $line $column]
}

#
# Callback procedure checkLines
#
# The parameter args can be interpreted both as "index
# string ?tagList string tagList ...?" (for an insert
# callback) and as "from ?to?" (for a delete callback).
#
proc checkLines {w args} {
    displayPos $w insert

    global width
    scan [$w index end] %d lastLine
    for {set line 1} {$line < $lastLine} {incr line} {
        set str [$w get $line.0 $line.end]
        if {[string length $str] > $width} {
            .send configure -state disabled
            return ""
        }
    }
    .send configure -state normal
}

. . .

displayPos .txt insert
focus .txt

The procedure protectRedArea is a before-insert, before-delete, and before-selset callback.  It checks whether the attempted change would affect the text area displayed in red; if this is the case, it calls the procedure wcb::cancel, which aborts the insert, delete, or  tag add sel  command, respectively.

The before-insert callback changeColor invokes the wcb::extend command to append the user tag to the argument list of the insert command, thus changing the foreground color of the characters entered by the user to DarkGreen.

The procedure displayPos displays the line and column corresponding to the index passed to it as its second argument.  This index will be the target position of the insertion cursor when the procedure is triggered automatically as a before-motion callback.  As seen in the checkLines procedure discussed below, it is also invoked after performing an insert or delete operation; in that case, its second argument will be the new position of the insertion cursor after the execution of insert or delete.  In this way, we are able to keep track completely of the position of the insertion cursor.

It is interesting to see what happens if we register displayPos as an after- instead of before-motion callback.  Well, in that case the procedure would have to ignore its second argument and we would have to replace the line

set index [$w index $idx]

with

set index [$w index insert]

The reason is that the value of the idx argument passed to displayPos can be, for instance, insert+1c, where insert means the position of the insertion cursor before moving it forward by one character.  The after-motion callback is, however, triggered after the insertion cursor has been moved, and at that time the insert mark already points to the new cursor position.  For this reason,  [$w index $idx]  is not adequate to retrieve the position of the insertion cursor within an after-motion callback.

Our last procedure checkLines is both an after-insert and after-delete callback.  After calling displayPos to display the new cursor position, it disables or enables the .send button, depending upon whether any line of the text widget contains more than 50 characters.

Start page