Why You Should Know Tcl/Tk
Maurice Castro <[email protected]>
Introduction
Tck/Tk (Tool Command Language / Toolkit) is a
scripting language bundled with a collection of
graphical libraries. The language and libraries are
supported across many platforms, including UNIX and
Windows. I have successfully used the language in
three roles:
Desktop utility applications.
Rapid application development.
Prototyping of graphical aplications.
The language and libraries have been very successful
in these roles.
From a system administrator's point of view, the
ability to provide a custom graphical user interface
to the user rapidly is highly attractive since users
have come to expect that interfaces will incorporate
graphical elements such as buttons, lists, and forms
rather than simple text interfaces. Like WWW
applications, Tcl/Tk provides a cheap and portable
interface option. The essential difference between
Tcl/Tk and a web interface is that Tcl/Tk is typically
used on the local machine rather than in a
client/server environment.
This article will provide a brief description of the
flavor of the language and will provide examples of
its use.
Tcl/Tk
Tcl
Unlike C, Java, Pascal, and other programming
languages, Tcl has an unusual structure. In most
other programming languages, a parser recognizes
punctuation elements such as braces (`{' and `}'),
and uses these to determine the blocks of the program.
This allows the programmer considerable freedom to
format their program as they wish. For example, in
C:
if (a > b) {
printf("a > b");
}
if (a > b)
{
printf("a > b");
}
if (a > b) { printf("a > b"); }
if (a > b)
{
printf("a > b");
}
The structure of Tcl is simple. A script is composed
of a series of commands separated by either semicolons
or newlines. A command consists of a command name
followed by a series of arguments. Before a command
is parsed, the interpreter performs a series of
substitutions on the command. These substitutions are
controlled by braces, square brackets, double
quotation marks, and backslashes. The values of
variables are substituted when the variable name is
prefixed by a dollar sign.
A direct consequence of this simple structure is that
the programmer is restricted in how they format their
code. Breaking into long lines requires the use of a
backslash. Braces must be placed to control the
parsing of the command forcing the programmer to
either place the brace before a new line, or finish a
line with a backslash. Many programmers regard the
language as "ugly" because of these restrictions and
the lack of "syntactic sugar" found in other
programming languages.
I would encourage you to look beyond the constraints
of the simple structure of the language and observe
the power of the product it lets you produce with
minimal effort.
The rules of the language parser are:
If any other character follows the backslash,
the backslash is dropped and the next character is
incorporated into the current word. This
characteristic allows special characters such as $
and [ to be included into words.
Double quotes disable word and command
separators.
Braces disable most special characters.
Command substitution -- characters in square
brackets are treated as commands and are replaced
with the result of the command.
Variable substitution -- names prefixed with
dollar sign are replaced with the contents of the
variable.
Lists -- a list consists of items and sublists
which are separated by spaces. Sublists are enclosed
in braces.
Some of the commands provided by Tcl are:
Command |
|
Description |
append |
|
Append to variable |
break |
|
Abort looping command |
case |
|
Evaluate one of several scripts
depending on a given value |
close |
|
Close to an open
channel |
concat |
|
Join lists together |
continue |
|
Skip to the next iteration of a
loop |
eof |
|
Returns 1 if end of file,
otherwise 0 |
exit |
|
Terminate the process |
expr |
|
Evaluate an expression |
flush |
|
Flush stream |
for |
|
A looping command similar in
structure to the C "for" statement |
foreach |
|
Iterate over all elements in one
or more lists |
gets |
|
Read a line from a
channel |
if |
|
Execute scripts
conditionally |
incr |
|
Increment variable |
lappend |
|
Append to list |
lindex |
|
Return element from
list |
linsert |
|
Insert into list |
llength |
|
Return length of list |
lreplace |
|
Replace elements in
list |
lsearch |
|
Find element in list |
lsort |
|
Sort list |
open |
|
Open stream |
puts |
|
Output to stream |
proc |
|
Create a procedure |
read |
|
Read from stream |
regexp |
|
Match regular expression |
regsub |
|
Substitute using regular
expression |
scan |
|
Parse string using conversion
specifiers in the style of sscanf |
set |
|
Set a variable to a
value |
split |
|
Split a string into a proper Tcl
list |
switch |
|
Evaluate one of the several
scripts depending on a given value |
unset |
|
Destroy a variable |
while |
|
Execute script repeatedly as
long as a condition is met |
The following program implements part of the
functionality of wc(1) -- the UNIX word count
program:
#!/usr/local/bin/tclsh8.0
foreach file $argv {
set lines 0
set chars 0
set filevar [open $file]
set charcnt [gets $filevar line]
while {$charcnt >= 0} {
set chars [expr ($chars + $charcnt +1)]
incr lines
set charcnt [gets $filevar line]
}
puts "\t$lines\t$chars\t$file"
close $filevar
}
The program accepts a list of filenames on the
command line and counts the number of lines and
characters in each file. The number of lines and
characters is output after each file is processed.
Tk
Tk is a windowing toolkit accessed through Tcl
commands. Commands are available to produce windows,
buttons, labels, text boxes, menus, and other
graphical items. In general, a command generates a
graphical object with a name called a widget. This
widget can then be used as a command to manipulate the
object and as an argument to commands that control the
placement and actions of graphical objects. For
example, the following code produces a button that can
be used to finish an Xsession if it is included as the
last command in your .Xsession file:
#!/usr/local/bin/wish8.0
button .logout -text "Logout" -command exit
pack .logout
The command button generates a button
object which is packed into the top level window (note
that objects must be packed to become visible). The
command exit is bound to the button. This
binding causes the command exit to be
executed when the button is pressed. When run, the
program generates the following window:
Figure 1.0
Some of the commands provided by Tk are:
Command |
|
Description |
bind |
|
Bind a script to an even in a
graphical object |
button |
|
Create a button |
canvas |
|
Create a drawing space |
checkbutton |
|
Create a checkbutton |
destroy |
|
Destroy a graphical
object |
entry |
|
Create a text entry
field |
frame |
|
Create a frame which may hold
graphical objects |
label |
|
Create a text label |
listbox |
|
Create a listbox |
menu |
|
Create a menu |
menubutton |
|
Create a button in a
menu |
pack |
|
Geometry manager for
objects |
place |
|
Geometry manager for
objects |
radiobutton |
|
Create a radio button |
scrollbar |
|
Create a scrollbar |
text |
|
Create a multi-line text
display |
tk_dialog |
|
A dialog box |
tk_getOpenFile |
|
A file open dialog box |
tk_getSaveFile |
|
A file save dialog box |
tk_messageBox |
|
A message box |
toplevel |
|
A new top level window |
winfo |
|
Get information about a
window |
wm |
|
Control the window
manager |
Example Applications
System Monitor
One of the network I maintain has a collection of
poorly-behaved switches. These switches are notorious
for their poor spped auto-sensing and appear to
sometimes forget their manual port configuration.
After a particularly annoying incident where a machine
sent over a thousand emails to our mail server because
it was disconnected by the switch for an extended
period of time and then became reconnected, I was
driven to write the following application.
This application pings each machine in the
.machines file once every ten minutes to
determine if the connection is alive. The machine
name changes color to indicate if the machine
responds. A count of the number of minutes since the
last successful ping is displayed next to failed
entries.
This example illustrates:
Reading a file.
Generating a simple text display.
Generating a timed update of a
display.
The application of exec to execute an external
program and read the results from it.
#!/usr/local/bin/wish8.0
# Configuration section
set checkinterval 10
set machinefile [join "$env(HOME) .machines" "/"]
# Derived variables
# Configure frame info
wm title . "xchks"
# Configure toplevel window
. configure -background white
# Read machine list
catch {
set n 1
set f [open $machinefile]
set l [gets $f]
while {! [eof $f]} {
label .m$n -text $l -background white
pack .m$n
lappend machinelist .m$n
set n [ expr $n + 1 ]
set l [gets $f]
}
close $f
}
update idletasks
proc checkmachs {} {
upvar #0 machinelist machinelist
upvar #0 checkinterval checkinterval
set ping /sbin/ping
foreach i $machinelist {
set l [$i cget -text]
set m [lindex $l 0]
set c [lindex $l 1]
$i configure -foreground black
update idletasks
$i configure -foreground orange
catch {exec $ping -c 1 -q $m} r
if {[lindex $r 14] == "1"} {
$i configure -text $m -foreground green
}
if {[lindex $r 14] == "0"} {
$i configure -text [list $m $c] -foreground red
}
update idletasks
}
update idletasks
after [expr $checkinterval * 60 * 1000] checkmachs
}
checkmachs
Shown below is the output of the program with two
machines that have answered successfully, one machine
in failure, and one machine whose domain does not
resolve.
Figure 2.0
The application took less than half a morning to
write and debug.
mpg123 Graphical Shell
mpg123 is a
popular command line mp3 player. The player supports
of number of unusual features, including the ability
to play mp3s of the Network Audio System (NAS). We
use NAS to provide sound on our X-terminals. One of
our users suggested they would like a graphical shell
to control mpg123... an afternoon's work
later, it was done.
This shell exploits mpg123's built-in
remote protocol. The protocol is not as clean as that
used by other front-ends to mpg123, but it
works. Also, we were not forced to rebuild the player
and redistribute the binaries.
This example illustrates:
Generating an interactive text
display.
Using open to execute an external program, and
writing commands to the external program and reading
results from it.
Using listboxes and scrollbars.
Using the file open dialog.
#!/usr/local/bin/wish8.0
set MPG123 "/usr/local/bin/mpg123 -R -"
set Player ""
set NextTrack ""
set PlayerStarted 0
frame .playlist
label .playlist.label -text "Play List"
scrollbar .playlist.yscroll -command ".playlist.list yview"
scrollbar .playlist.xscroll -orient horizontal \
-command ".playlist.list xview"
listbox .playlist.list -width 20 -height 10 -setgrid 1 \
-yscroll ".playlist.yscroll set" -xscroll ".playlist.xscroll set" \
-selectmode multiple
pack .playlist.label -side top
pack .playlist.xscroll -fill x -side bottom
pack .playlist.list -fill both -side left -expand yes
pack .playlist.yscroll -fill y -side right
frame .buttons1
button .buttons1.start -text "Start" -command StartProc
button .buttons1.stop -text "Stop" -command StopProc
button .buttons1.skip -text "Skip" -command SkipProc
button .buttons1.pause -text "Pause"
pack .buttons1.start .buttons1.stop .buttons1.skip .buttons1.pause \
-fill x -side left -pady 2 -padx 2
frame .buttons2
button .buttons2.add -text "Add" -command AddProc
button .buttons2.delete -text "Delete" -command DeleteProc
pack .buttons2.add .buttons2.delete -fill x -side left -pady 2 -padx 2
button .quit -pady 2 -padx 2 -text "Quit" -command QuitProc
frame .info
label .info.title -width 30 -justify left -anchor w
label .info.artist -width 30 -justify left -anchor w
label .info.album -width 30 -justify left -anchor w
pack .info.title
pack .info.artist
pack .info.album
label .status
pack .playlist -expand yes -fill both
pack .button1 .buttons2 .quit .info .status
proc AddProc {} {
set types {
{{MP3} {.mp3 .MP3} }
{{Play List} {.m3u .M3U} }
}
set file [tk_getOpenFile -filetypes $types]
if {"$file" == ""} {
return
}
if {[regexp -nocase "^.*\.m3u" $file]} {
regsub {[^/\]*$} $file "" prefix
set f [open $file r]
while {[ eof $f ] != 1} {
set l [gets $f]
if {"$l" == ""} {
continue
}
.playlist.list insert end "$prefix$l"
}
return
}
.playlist.list insert end $file
}
proc QuitProc {} {
upvar #0 Player Player
catch {StopPlayer}
catch {close $Player}
exit
}
proc StartPlayer {} {
upvar #0 PlayerStarted PlayerStarted
upvar #0 Player Player
upvar #0 MPG123 MPG123
if {!$PlayerStarted} {
set Player [open "|MPG123" w+]
fconfigure $Player -blocking 0
set PlayerStarted 1
}
.status configure -text ""
}
proc StopPlayer {} {
upvar #0 Player Player
upvar #0 PlayerStarted PlayerStarted
if {$PlayerStarted} {
puts $Player "QUIT"
flush $Player
}
set PlayerStarted 0
}
proc DoPlayList {} {
upvar #0 NextTrack NextTrack
set nlist [.playlist.list curselection]
if {"$nlist" == ""} {
set tracks [.playlist.list get 0 end]
} else {
foreach n $nlist {
lappend tracks [.playlist.list get $n]
}
}
set NextTrack ""
if {"$tracks" != ""} {
set t [lindex $tracks 0]
Command "LOAD $t"
set NextTrack [lrange $tracks 1 end]
}
}
proc DeleteProc {} {
set nlist [.playlist.list curselection]
while {"$nlist" != ""} {
set n [lindex $nlist 0]
.playlist.list delete $n
set nlist [.playlist.list curselection]
}
}
proc StopProc {} {
upvar #0 NextTrack NextTrack
set NextTrack ""
Command "STOP"
}
proc SkipProc {} {
Command "STOP"
}
bind .buttons1.pause <ButtonPress-1> PauseProc
proc PauseProc {} {
Command "PAUSE"
}
proc StartProc {} {
StartPlayer
DoPlayList
}
proc Command {Cmd} {
upvar #0 Player Player
upvar #0 PlayerStarted PlayerStarted
if {[catch {DoCommand $Cmd}] != 0} {
.status configure -text "Player Error"
catch {close Player}
set PlayerStarted 0
}
}
proc DoCommand {Cmd} {
upvar #0 Player Player
upvar #0 PlayerStarted PlayerStarted
if {$PlayerStarted} {
puts $Player $Cmd
flush $Player
}
}
proc StatusMsgNextItem {} {
upvar #0 NextTrack NextTrack
upvar #0 Player Player
upvar #0 PlayerStarted PlayerStarted
while 1 {
if {$PlayerStarted} {
set m [gets $Player]
if {[regexp "^@I ID3:" $m]} {
.info.title configure \
-text [string range $m 7 36]
.info.artist configure \
-text [string range $m 37 66]
.info.album configure \
-text [string range $m 67 96]
} elseif {[regexp "^@I " $m]} {
.info.title configure \
-text [string range $m 2 end]
.info.artist configure -text ""
.info.album configure -text ""
} elseif {[regexp "^@P 0" $m]} {
.info.title configure -text ""
.info.artist configure -text ""
.info.album configure -text ""
.status configure -text ""
if {"$NextTrack" !- ""} {
set t [lindex $NextTrack 0]
Command "LOAD $t"
set NextTrack \
[lrange $NextTrack 1 end]
}
} elseif {[regexp "^@P 1" $m]} {
.status configure -text "Paused"
} elseif {[regexp "^@P 2" $m]} {
.status configure -text ""
} elseif {[regex[ "^@E " $m]} {
.status configure \
-text [string] range $m 2 end]
}
}
update
}
}
StatusMsgNextItem
Shown below is the output of the program:
Figure 3.0 (click to enlarge)
Summary
This article provided a brief description of the
language and examples of simple graphical tools.
Tcl/Tk has made possible the rapid development of
functional, useful tools. Development of similar
tools in other environments would have been both
slower and more complex.
Further Information
For more information, you can consult:
The Tcl/Tk Core
web site. Many useful links can be found off
this page.
Tcl and the Tk Toolkit by John
Ousterhout, Addison-Wesley, ISBN 0-201-63337-X
Tcl/Tk: Pocket Reference by Paul Raines,
O'Reilly & Associates, ISBN 1-56592-498-3
- Maurice
Return to the
February 2001 Issue