Rapid Gui Development using Tcl/Tk
Course Text Part One: Tcl
(some text drawn from Brent Welch’s book, "Practical Programming in Tcl/Tk")
Aidan Low
Student Information Processing Board
MIT
January 1999
This class is sponsored by SIPB, the MIT Student Information Processing Board, a volunteer student group that provides computer- related services to the MIT community. This class is one of those services. For more information, see our web page, www.mit.edu, or drop by the SIPB office, W20-557.
Class Homepage:
Class outline:
Tcl
Background
Variables – Set, Unset
Comments
Basic Input/Output
Procedures
Upvar
Arithmatic Operations
String Operations
List Operations
Control Flow
Global variables
Array Operations
More Input/Output
File Operations
System Operations
Background
Tcl is an intepreted high-level programming language designed to be easily understandable and easily customizable. It was developed by John Ousterhout when he wanted to give his students an editor that they could alter and extend themselves. One of tcl’s nicest features is that it’s very easy to write and very easy to read. Tcl code is simple and straightforward, for the most part, and as such is easy to modify.
Tk is an associated graphical toolkit which provides the same power to users in the graphical domain. Now complex GUIs can be written in a few dozen lines of code, rather than the pages it requires in more complicated arenas.
An Example: A Zsig randomizer
By the end of the night, this (highly inefficient) zsig randomizer should make sense to all of you.
#!/mit/tcl/bin/tclsh
proc getsigs {fname} {
set f [open $fname r]
set thelist {}
set curr {}
set temp [gets $f]
while {![eof $f]} {
if {$temp=="---"} {
set thelist [lappend thelist $curr]
set curr {}
} else {
set curr [lappend curr $temp]
}
set temp [gets $f]
}
set thelist [concat $thelist [list $curr]]
return $thelist
}
proc cleanNum {num} {
## this procedure strips off leading zeros
if {[string index $num 0]!="0"} {
return $num
} else {
return [cleanNum [string range $num 1 end]]
}
}
proc random {range} {
if {[file exists "/afs/athena.mit.edu/user/a/i/aidan/.zrand.seed"]} {
set f [open "/afs/athena.mit.edu/user/a/i/aidan/.zrand.seed" r]
set _ran [gets $f]
close $f
} else {
set _ran [pid]
}
set _ran [expr ($_ran * 9301 + 49297) % 233280]
set f [open "/afs/athena.mit.edu/user/a/i/aidan/.zrand.seed" w]
puts $f $_ran
close $f
return [expr int($range * ($_ran / double(233280)))]
}
proc pickone {list} {
set index [random [llength $list]]
puts stdout "index is $index"
set bit [lindex $list $index]
set result {}
foreach el $bit {
if {$result=={}} {
set result $el
} else {
set result "[set result]\n[set el]"
}
}
return $result
}
proc driver {} {
global argv argc
if {$argc==0} {
puts stdout "Format is z <name> \[other stuff\]"
return
}
set siglist [getsigs ~/.zephyr.list.short]
set sig [pickone $siglist]
puts stdout $sig
puts stdout "Type your message now. End with control-D or a dot on a line by itself."
if {[llength $argv]>4} {
return
# This doesn't work, and I’m too lazy to figure out why
set response [exec zwrite $argv]
puts stdout [lreplace $response 0 14]
} elseif {[llength $argv]==4} {
set response [exec zwrite [lindex $argv 0] [lindex $argv 1] \
[lindex $argv 2] [lindex $argv 3] -s $sig]
puts stdout [lreplace $response 0 14]
} elseif {[llength $argv]==2} {
set response [exec zwrite [lindex $argv 0] [lindex $argv 1] -s $sig]
puts stdout [lreplace $response 0 14]
} else {
set possible_error [catch {set response [exec zwrite [lindex $argv 0] -s $sig]}]
if {$possible_error==1} {
puts stdout "Error.... user not logged in?"
}
puts stdout [lreplace $response 0 14]
}
}
driver
Outline
This course will cover Tcl and Tk at a basic level, and looking at features of the language one by one along with examples of the code at work. In the end, we’ll see that a basic word processing program can be written very simply using Tcl and Tk. We’ll close by briefly looking at a number of advanced topics which are beyond the scope of this class, but that you can learn about on your own if you like.
In the first class, we’ll concentrate on Tcl, and in the second, we’ll look at how Tk can extend Tcl to allow the creation of graphical interfaces very simply.
Writing Conventions
Text in Courier font
set a [expr $b * 12]
represents code exactly as typed into the interpreter.
Text in italics represents an abstract thing to be filled in. This text
set f [open filename r]
means that the command takes a filename in that space. (i.e.
set f [open "foo.txt" r]
Text wrapped in ? ? means that it is optional.
puts ?–nonewline? textstring
Finally, … means that there are multiple things there.
foo arg … arg
Starting up (on Athena)
Tcl Interpreter
This will bring up the tcl interpreter in the window you run it in.
add tcl
tclsh
Tk Interpreter
This will bring up the Tcl interpreter in the window you run it in, but this tcl interpreter can run tk commands as well. A Tk window will also be created.
add tcl
wish
To run a file
To execute a file of tcl or tk code, there are two ways to do it. One is to make the file executable:
chmod 700 filename
and to put
#!/mit/tcl/bin/tclsh
or #!/mit/tcl/bin/wish
at the start of the file.
The other way is to run
source filename
in the interpreter once started.
Zephyr instance
I encourage all of you to work together as you’re learning Tcl and Tk, and a zephyr instance is a good way to do that. I’ll be subbed to this whenever I can, and you can ask your questions about Tcl and Tk on the instance where everyone can see them and benefit from the answers.
To subscribe for this login session only:
zctl sub message tcltk \*
To unsubscribe for this login session only:
zctl unsub message tcltk \*
To subscribe forever:
zctl add message tcltk \*
To unsubscribe forever:
zctl delete message tcltk \*
To send a message
zwrite –i tcltk
To read the logs of the instance
add zlog
cd /mit/zlog
more tcltk
(or emacs tcltk, or whatever)
Tcl
Variables – Set, Unset
The core of the tcl language is variables. Unlike other languages, every variable in tcl is a string. The string "123" is the number one hundred twenty-three, but it is also the character string made of the characters ‘1’,’2’ and ‘3’. It depends on the context in which the variable appears.
Assigning a variable a value is done with the set command
set a 3
<- This sets the variable a to be the string "3"set b "Hello"
set c Hello
To get the value of a variable, the set command is used with just the name of the variable.
set a 3
set a
<- This is equal to 3One variable can be set to the value of another. A command can be run on its own line, but to nest commands, we use the [ ] braces.
set a 3
set d [set a]
<- This sets d to be equal to the value of aNote that [set a] is the value of the variable, and once done, the data has nothing to do with the variable. If the variable a changes later, d will not change.
The $ symbol is a shorthand which gets the value of a variable.
set e $a
A variable can be unset, removing it from the scope of the program, with the unset command.
unset e
set f $e
<- this will generate an error, since e is no longer definedTo find out if a variable is defined, you can use info exists, which returns 1 if the variable is defined, and 0 if not.
set a 1
set b [info exists a]
<- b is now 1set c [info exists gobbledygook]
<- c is now 0These commands can be arbitrarily nested, either in a string
set g "this $a is $b weird $c"
or even in the name of a variable
set h 12
<- variable "h" is set to value "12"set i$h 14
<- variable "i12" is set to the value "14"
Basic Input/Output
Input and output in tcl is pretty simple. Puts and gets are the output and input commands.
set a [gets stdin]
<- this reads in a value from the console and stores it in aputs stdout $a
<- this command echos back the value to the consoleputs stdout "Hello world"
puts stdout "This is value a: $a"
set b "User says [gets stdin]
The default location for puts is stdout, but gets has no default location. Go figure.
puts "Hello world"
By default, puts puts a carriage return at the end of the string it prints, but you can disable this with the nonewline flag.
puts –nonewline "These are on "
puts "the same line."
Procedures
You can define procedures in tcl and call them.
proc foo {} {
puts "This is procedure foo"
}
A procedure can take arguments.
proc bar {x y z} {
puts "$x $y $z"
}
A procedure can return a single value.
proc baz {x y} {
return [expr $x + $y]
}
set a [baz 2 3]
Procedure arguments can have default values
proc foo2 {{x 12} {y 3}} {
return [expr $x + $y]
}
set b [foo2]
<- b is assigned 12 + 3 = 15set c [foo2 7]
<- c is assigned 7 + 3 = 10set d [foo2 1 8]<-
d is assigned 1 + 8 = 9Note that the arguments with default values must be at the end of the list of arguments, and no arguments with default values may precede arguments without default values.
Procedures can even take any number of parameters, if the last parameter is named "args". Here, calling this procedure with any number of arguments will pass in the arguments as a list (more on lists later) bound to the identifier args.
proc bar2 {args} {
puts "Arguments are $args"
}
An Example
This somewhat contrived example takes in values from the console and echos them back.
proc foo {a b c} {
puts stdout "First input was $a"
puts stdout "Second input was $b"
puts stdout "Third input was $c"
}
puts stdout "Input 1:"
set in1 [gets stdin]
puts stdout "Input 2:"
set in2 [gets stdin]
puts stdout "Input 3:"
set in3 [gets stdin]
foo $in1 $in2 $in3
Upvar
In the arguments we have seen before, arguments are passed "by value". This means that in the code
proc foo {a b c} {
puts "$a $b $c"
}
set f 11
set g 22
set h 33
foo $f $g $h
The interpreter first computes the value of variable f (11), the value of variable g (22), and the value of variable h (33), and then send them (11,22,33) to the procedure foo. Foo knows nothing about where those values came from, and from foo’s perspective, the call might well have been
foo 11 22 33
The other method of passing arguments is called "call by name". In this calling convention, a function can access and modify variables from the caller. The upvar command works to allow you to do this.
Upvar takes in the name of a variable from the caller and the name of a variable to "bind" that variable to. After the upvar command, any reference to the second variable actually refers to the named variable in the caller. To pass a variable by name, you send in the name of the variable, rather than its value. For example:
proc foo {name1 name2 name 3} {
upvar $name1 a
upvar $name2 b
upvar $name3 c
puts "$a $b $c"
}
set f 11
set g 22
set h 33
foo "f" "g" "h"
Now, the caller calls foo with the names of its three variables (f, g, and h). Foo uses upvar to say that after the 3 upvar commands, any reference to "a" really means a reference to the variable "f" as defined in the scope of the caller. Now, in the puts command when the value of a is printed out, the value of f in the caller is really the thing that gets printed.
Upvar can also allow you to modify variables in the caller.
proc foo {name} {
upvar $name a
set a [expr $a + 1]
}
set f 11
puts $f <- f is 11 here
foo "f"
puts $f <- f is 12 here
Comments
Comments are text within a program that is ignored when running the program, but used to leave reminders and notes for other people who read the source code later.
Comments in Tcl are specified by the # character at the beginning of the line. This comments all the text to the end of the line. Note that you can only use # where the Tcl interpreter expects a Tcl expression, so you are somewhat restricted in where you can use it. In general, comments should be on their own lines to be safe.
# this procedure does something
proc foo {} {
puts "Hello world"
# is this correct?
puts "Hello werld"
}
Arithmatic Operations
The simplest arithmatic operations are simply incrementing and decrementing an integer. This is done with the incr and decr commands, which take in the name of the variable to increment. Note that this is a call by name command, so you send in the variable name, not the value of the variable.
(i.e. incr a, not incr $a)
set a 3
incr a
<- a is now 4decr a
<- a is now 3 againArithmatic operations in Tcl are executed with the expr command
set a [expr 2 + 3]
set b [expr (2 + 3) * 4]
A number of more complex operations can be done with expr as well:
set c [expr 7 << 3]
<- c is assigned 7 left shifted by 3, i.e. 7 * 23 = 56set d [expr 7 >> 3]
<- d is assigned 7 right shifted by 3, i.e. 7 / 23Note that the "type" of the input determines the operation in some cases
set e [expr 7 / 3]
<- e is assigned 7 integer-divided by 3, or 2set f [expr 7.0 / 3.0]
<- f is assigned 7 float-divided by 3, or 2.3333Other math operations have a slightly different notation
set g [expr pow(3,2)]
<- g is assigned 3 raised to the second powerset h [expr acos(.8)]
<- h is assigned the arc-cosine of .8Tcl supports many math operations:
acos(x)
asin(x)
atan(x)
atan2(y,x) Rectangular (x,y) to polar (r,th), atan2 gives th
ceil(x)
cos(x)
cosh(x)
exp(x) ex
floor(x)
fmod(x,y) Floating point remainder of x/y
hypot(x,y) Returns sqrt(x*x + y*y) (r part of polar expresion)
log(x) Natural log
log10(x) Log base 10
pow(x,y) xy
sin(x)
sinh(x)
sqrt(x)
tan(x)
tanh(x)
abs(x)
double(x) Turns x into floating point number
int(x) Turns x into an integer (truncates)
round(x) Turns x into an integer (rounds off)
rand() Return a random floating point value between 0.0 and 1.0
srand(x) Seed the random number generator
Another example
This procedures takes in inputs and adds them up until they sum to more than 100.
#running total takes in the name of the total counter
#and the value to add to it
proc runningtotal {totalname new} {
upvar $totalname current
set current [expr $current + $new]
}
set total 0
while {$total < 100} {
puts stdout "Total is now $total."
puts stdout "Input new value:"
set inputvalue [gets stdin]
runningtotal "total" $inputvalue
}
puts stdout "Done."
String Operations
Since all Tcl values are stores as strings, it makes sense to have a number of string operations that can be done. Just as expr preceded arithmatic expressions, string precedes these commands.
set a [string length "Hello world"]
<- a gets the string length of the string, which is 11set b [string index "Hello world" 3]
<- b gets the 3rd character in Hello WorldNote that strings are 0-indexed, so the 0th character is the first one in the string.
Other string operations:
string compare str1 str2 Returns 0 if equal, -1 if str1 sorts before str2
lexographically, 1 if str2 sorts before str1
string first sub str Returns the index of the first character of the first
occurrence of substring sub appearing in str
string last sub str Returns the index of the first character of the last
occurrence of substring sub appearing in str
string match pattern str Returns 1 if str matches the pattern in pattern.
Uses glob-style matching. (* and ? wildcards)
string range str I j Returns the range of characters from index i to index j.
string tolower str
string toupper str
string trim str ?chars? Trims the characters in the string chars from the front and
end of str. chars defaults to whitespace if it is not present.
string trimleft str ?chars?
string trimright str ?chars?
string wordend str ix Return the index in str of the character after the word
containing the character at ix.
string wordstart str ix Return the index in str of the first character in the word
containing the character at ix.
Append
Another useful string command is append. This appends one or more arguments to a variable. Note that the first argument is the NAME of the variable, not [set variable] or $variable.
set a "abc"
set b " some letters"
append a " is " $b "that I like"
puts $a
<- this will print "abc is some lettersthat I like"Note: append is efficient, much more so than doing something like
set x "abc "
set x "$x some letters that I like"
Format
Format is like the C printf function.
set a [format "%d %f %s" 10 4.3 "hello"]
<- a is assigned "10 4.3 hello"
Scan
Scan is like the C scanf function.
scan "10 4.3 hello" "%d %f %s" x y z
<- assigns x to be 10, assigns y to be 4.3, assigns z to be hello
A gratuitous string example
This example reads in a string and then prints out some information about it
proc stringparse {str} {
puts stdout "String's length is [string length $str]"
puts stdout "String's first a is [string first "a" $str]"
puts stdout "String in uppercase is [string toupper $str]"
append str " tacked on for good measure"
puts stdout "String is now $str"
}
puts stdout "Input string:"
set str [gets stdin]
stringparse $str
set str [gets stdin]
stringparse $str
set str [gets stdin]
stringparse $str
List Operations
The most common data structure in Tcl is a list.
Lists can be created in a number of ways.
set a [list "a" "b" c"]
<- a is assigned to be a list with three elements, "a", "b", and "c"
Curly braces {} can be used as shorthand for the list command.
set a {"a" "b" "c"}
<- a is assigned to be the same list as earlier
Lists can be nested:
set a [list "a" [list "b" "c"] "d"]
set a {"a" {"b" "c"} "d"}
A number of list operators are very useful.
lindex list i <- returns the ith element of the list
llength list <- returns the number of elements in the list
lappend listName arg … arg <- appends some number of arguments to a list
(note use of list name, not the list itself, as the first arg)
lrange list i j <- return the sublist of the list found between
indexes i and j
linsert list i arg … arg <- insert some number of arguments into a list before the
ith element. Returns the new list.
lreplace list i j arg … arg <- replace the elements i thru j of the list with the args.
returns the new list.
lsearch ?mode? list value <- returns the first index of an element in the list that
matches the value. mode can be –exact (must match
exactly), -glob (uses * and ? wildcards) or –regexp
(match using regular expression syntax). Returns –1 if
not found. Defaults to -glob.
concat list … list <- concatenates multiple lists together into one big list.
lsort ?switches? list <- Sort the list as per the switches passed in. –ascii,
-integer, -real, -dictionary, -increasing, -decreasing
Returns a new list.
join list joinString <- join the elements of a list into a single string, each
element separated by joinString
split string splitChars <- split a string into a list by dividing the string up with
all the characters in splitChars as the delimiters.
Control Flow
Control flow in Tcl works like in many other languages.
if, while, for
proc foo {a b} {
if {$a > $b} {
return $a
} else {
return $b
}
}
proc bar {a b} {
while {$a>0} {
set a [expr $a – 1]
puts [expr $a + $b]
}
}
proc baz {a b} {
for {set i 0} {$i < $a} {incr i} {
puts "$a $b"
}
}
foreach
One construct that’s cool is foreach. Foreach is like for, except that a loop is run once for each element in a list rather than for each value between two boundaries. This example would print out all the colors in the list.
foreach el {"red" "blue" "green" "yellow"} {
puts $el
}
Foreach can have more than one list variable, in that case the list would be taken in pairs. This example would print out the colors two at a time.
foreach {elX elY} {"red" "blue" "green" "yellow"} {
puts "$elX is a better color than $elY"
}
continue, break
The continue and break commands allow control to jump out from the inside of a loop. Break breaks out of the loop and runs the next command, and continue jumps back to the start of the loop.
In some sense, break stops the loop totally and continue skips to the next run through the loop.
switch
Switch allows control to jump to a number of expressions depending on what a value is.
switch -- $name {
"Aidan" {puts "This guy is a dork"}
"Rorschach" {puts "This dude is a badass"}
"Leon" {puts "This guy is surprisingly smart"}
default {puts "I don’t know this person"}
}
The default is to do exact matching, but flags can be put after switch to change this.
switch –exact $name
or switch –glob $name
or switch –regexp $name
Put -- before $name if there’s any chance that $name might begin with a -, or else $name will be interpreted as a flag. It’s best to put -- before $name anyway.
If a - is written as the expression for a match, then control "falls through" to the next non-"-" expression.
switch -- $name {
"Aidan" –
"Atilla" –
"Godilla" {puts "wimp"}
"Jesse the Mind" –
"Steve Austin" –
"Hulk Hogan" {puts "tough guy"}
}
catch
Tcl supports error handling. If you use the catch command, you can try execute a block of Tcl expressions and trap any errors that result.
catch command ?resultVar?
catch returns true (1) if the command failed, or false (0) otherwise. The resultVar, if present, is given the return value of the command, or an error message if an error occurred.
if {[catch {foo 1 2 3} result]} {
puts stderr $result
} else {
# do other stuff
}
catch can catch return, break, and continue commands as well. See the documentation for information on this.
error
The error command is the counterpart to catch. It signals an error that will trigger an enclosing catch or halt the program with an error message if there is no enclosing catch.
error message
return
The return command returns from a procedure, returning a value.
return string
Another Silly Example
proc makelist {} {
set months {}
foreach el {"January" "February" "March" "April" "May" "June" "July"} {
lappend months $el
}
return $months
}
set data {}
foreach el [makelist] {
puts $el
set data [concat $data $el]
if {[llength $data] > 3} {
break
}
}
puts "done"
Global variables
So far we’ve looked only at local variables, variables in the same procedure where they’re used. Variables can also be global, and thus accessible anywhere in the program. Global variables are somewhat dangerous to use, but they’re really quite useful, especially in small programs that are easily understood.
To declare a variable global, you simply use the global command
proc setGlobals {val1 val2 val3} {
global a b c
set a $val1
set b $val2
set c $val3
}
proc printGlobals {} {
global a b c
puts "$a $b $c"
}
If you declare a variable outside of any procedures, it is implicitly global.
set a 12
proc foo {} {
global a
puts $a
}
Array Operations
Another construct Tcl provides is an array indexed by string values. This is essentially a hash table, and allows efficient association of keys and values.
To create an array, simply set a variable along with an index.
set myArray("redKey") "redValue"
<- this will create an array called myArray if itdoesn’t exist already, and add the value
"redValue" associated with the key "redKey"
To get the value out again, just use set like we did before.
set theValue [set myArray("redKey")]
or
set theValue $myArray("redKey")
You can test whether a key is present in an array by using info exists, just like for regular variables.
if {[info exists myArray("redKey")]} {
puts $myArray("redKey")
}
The array command allows a number of operations on arrays as well:
array exists arr
<- returns 1 is arr is the name of an array variablearray get arr ?pattern?
<- turns an array into a list of alternating keys and valuespattern selects the entries to put into the list, if present
(beats the hell out of me how the pattern works, though)
array names arr ?pattern?
<- returns a list of the names of the keys in the arrayapplies the pattern to filter, if present
array set arr list
<- create arr from the data in list, which is in the same formas returned from get
array size arr
<- return the number of keys in the array arrYou can iterate through an array as well. Look at the documentation for information on startsearch, nextelement, anymore, and donesearch.
Note that passing arrays between procedures requires call by name, using upvar.
More Input/Output
File input/output is reasonably straightforward.
open filename ?access? ?permissions?
access can be:
r Open for reading, file must exist
r+ Open for reading and writing, file must exist
w Open for writing, replace if exists, create if does not
w+ Open for reading and writing, replace or create as needed
a Open for writing, data is appended to file
a+ Open for reading and writing, data is appended
Permissions are the standard numbers used in chmod, but you need a leading 0 to get an octal number.
set fileId [open /tmp/foo w 0600]
Once open, a file can be written to
puts $fileId "Hello world"
or read from
set fileId2 [open /tmp/foo r]
set in [gets $fileId2]
To close an open file (you must close a file before the Tcl program ends to store your changes), simply use close on the fileID
close $fileId
You can also pass –nonewline to puts, if you don’t want to put an end-of-line after the string.
puts –nonewline $fileId "Hello world"
File Operations
Tcl provides a number of operators on files that can be quite useful.
file atime name <- returns file access time as a decimal string
file attributes name ?option? ?value? … <- query or set file attributes
file copy ?-force? source destination <- copy files or directories
file delete ?-force? name
file dirname name <- return parent directory of file name
file executable name <- return 1 if executable, else 0
file extension name <- return the extension (.txt) of the file
file isdirectory name <- returns 1 or 0
file isfile name <- return 1 if name is not a directory, symlink, or device, else 0
file join path path … <- join pathname components into a single pathname
file lstat name var <- place attributes of the link name into var
file mkdir name
file nativename name <- return the platform-native version of name
file owned name <- return 1 if current user owns the file name, else 0
file pathtype name <- relative, absolute, or driverelative
file readable name <- return 1 if readable, else 0
file readlink name <- return the contents of symlink name
file rename ?-force? old new
file rootname name <- return all but the extension in name (i.e. strip off .txt or whatever)
file size name <- return file size of name in bytes
file split name <- split name into its pathname components
file stat name var <- place attributes of name into array var
file tail name <- return the last pathname component of name
file type name <- return type identifier, which is either file, directory, characterSpecial, blockSpecial, fifo, link, or socket
file writeable name <- return 1 if writeable, else 0
System Operations
Tcl provides the functionality to execute a number of system calls directly.
exec
The exec program allows you to execute programs from your Tcl script. The standard output of the program will be returned.
set d [exec date]
If the program writes to the standard error channel or exits with a non-zero exit code, you’ll need to use catch to get the information you want.
catch {exec program arg arg} result
Exec has a lot of weird specifics about how it works, so look at the documentation for specifics.
http://www.scriptics.com/man/tcl8.0/TclCmd/exec.htm
exit
The exit command terminates your Tcl program. An integer argument to exit sets the exit status of the process
exit
exit –1
pid
The pid command returns the process ID of the current process.
set thisID [pid]
environment variables
You can get access to environment variables with the global variable env. This loop prints out all the environment variables and their values.
global env
foreach el [array names env] {
puts "$el - $env($el)"
}
Links of interest:
The Tcl Platform Company
Brent Welch’s book (excellent)
Tcl/Tk Man pages
http://www.scriptics.com/man/tcl8.0/contents.htm
Tcl/Tk Plugin for Netscape, Internet Explorer