本教程是针对那些具有一种或多种编程或脚本语言经验的人员设计的。可以在几种平台(包括 Win32 和 MacOS 以及几个 *NIX 环境)上使用 Tcl/Tk,本教程是在运行安装了 GNU/Linux 的环境中编写的。
首先,我将介绍 Tcl/Tk 并概述该语言的一小段历史。然后,回顾 Tcl/Tk 脚本语言和解释器的关键功能,讨论该语言的一些扩展,并研究几个在使用中的 Tcl/Tk 示例。在本教程中,随文本有一些代码段,偶而还有结果输出的图像(别忘了,Tk 是一种 GUI 工具箱)。
最后,我将集中一些外部资源(包括 Web 上的和印刷物)来帮助您更深入地了解 Tcl/Tk 环境。
回页首
Tcl 是“工具控制语言(Tool Control Language)”的缩写。Tk 是 Tcl“图形工具箱”的扩展,它提供各种标准的 GUI 接口项,以利于迅速进行高级应用程序开发。
John K. Ousterhout(见图像)于 1988 年开始开发 Tcl/Tk(读作“tickle tee-kay”),然后是加州大学伯克利分校(UCB)的一名教授继续对它进行开发。Tcl 是以可扩展性、短的学习曲线和易于嵌入为特定目标而设计的。Tk 的开发始于 1989 年,第一个版本于 1991 年问世。Ousterhout 博士在他离开 UCB 之后继续开发 Tcl/Tk,然后由于工作需要,他继续为 Sun Microsystems 公司工作。现在,也就是在写本文之时,他在 Scriptics(它开发出 Ajuba Solution,已由 Interwoven 收购)继续改进该语言,目前的稳定版本是 8.3.2,在写本文的时候,8.4 版本正在开发之中。
有关更多详细信息,请参阅“History of Tcl”页面。
在 Linux 系统上研究 Tcl/Tk,需要有两个主要程序。它们是 tclsh 和 wish。正如可以从其名称所辨别的那样,所以前者是 Tcl 外壳,常用于为外壳脚本提供执行环境。Wish 类似于 tclsh,它是针对窗口化的 GUI 环境。
输入下列命令,检查这些文件是否存在:
~/tcltk$ which tclsh /usr/bin/tclsh ~/tcltk$ which wish /usr/bin/wish
which 命令将返回指定可执行文件的路径。如果没有看到与这相似的结果,则需要转到 Scriptics Tcl/Tk 页面下载并构建您自己的副本。当然,如果系统上不存在这些程序,并不表示有任何问题。不象 Perl,通常不会将 Tcl/Tk 视为是 Linux 操作的基本要素。我所知道的每个发行版都和某一版本的 Tcl/Tk 一起交付,最常见的扩展作为 CD-ROM 或在线资源库的一部分。从这些资源,这些工具通常很容易安装。如果有困难,请与 GNU/Linux 软件的发行商联系。
回页首
在下面的清单中,将会发现第一个很常见的示例程序,它是用 Tcl 实现的。这是一个完整的脚本:第一行命令调用 tclsh 环境,第二行命令执行实际工作。用您所选择的文本编辑器创建该脚本,输入chmod +x hello.tcl 使之成为可执行文件,然后执行它以测试您这件“作品”。
~/tcltk$ cat hello.tcl #!/usr/bin/tclsh puts stdout {Hello, World!} ~/tcltk$ ./hello.tcl Hello, World!
Tcl 和 Tk 是解释型的、可扩展脚本语言。与 BSD 许可证十分相似,该许可证允许在任何情况下自由使用该软件,只要在所有副本中保留该版权并且在任何分发中一字不差地传递通告。这个许可证条款使 Tcl/Tk 成为自由软件。
Tcl/Tk 是一种解释型环境。可以通过添加预编译的 C 函数来扩展 Tcl 解释器,可从 Tcl 环境内部调用 Tcl 解释器。可以为特定目的或一般的以及广泛使用的而定制这些扩展。我们将在教程的后面看一些扩展并着重看一下第一个扩展 ― 非常流行的 Expect。
在接下来的几屏中,将回顾 Tcl 语言的一些主要特性,从元字符和全局变量到运算符、数学函数以及核心命令。毕竟,这些命令使 Tcl/Tk 成为有特色的,逐步发展的语言。请记住,在本教程中没有篇幅涉及每条命令。这里只突出一部分,以后您可以更进一步了解 Tcl/Tk。
#!/usr/bin/tclsh # filename hello2.tcl # This program code shows # metacharacter usage puts stdout "Hello, World! \a" puts stdout {Hello, World! \a} set Pints 6 set Days 7 puts stdout "The answer to the \ universe is [eval $Pints * $Days]!\n" *** ~/tcltk$ ./hello2.tcl Hello, World! Hello, World! \a The answer to everything is 42!
元字符是在 Tcl/Tk 环境的上下文中有特殊含意的字符或字符对,它们包括分组语句、封装字符串、终止语句以及其它,如下表所示。左边列出的代码中演示了一些元字符。要注意的一个特殊特性是,当使用花括号(防止替代和扩展)代替双引号时,输出中的差异。
字符 | 用作 |
---|---|
# | 注释 |
; 或newline | 语句分隔符 |
Name | 变量(区分大小写) |
Name(idx) | 数组变量 |
Name(j,k,l...) | 多维数组 |
"string" | 带替换的引用 |
{string} | 不带替换的引用 |
[string] | 命令替换 |
\char | 反斜杠替代 |
\ | 行继续(在行尾) |
#!/usr/bin/tclsh # # Demonstrate global variables # and backslash substitution if {$argc >= 1} { set N 1 foreach Arg $argv { puts stdout "$N: $Arg\n" set N [expr $N + 1] if {$Arg == "ring"} { puts stdout "\a" } } } else { puts stdout "$argv0 on \ X Display $env(DISPLAY)\n" } *** ~/tcltk$ ./hello3.tcl ./hello3.tcl on X Display :0.0 ~/tcltk$ ./hello3.tcl ring 1: ring
当 Tcl/Tk 脚本开始运行时,存在几个全局变量(如果在当前环境中为非空,则是预先定义的)。这些变量允许如下对操作系统进行访问:argc是对脚本自变量的计数,而不是对调用的名称进行计数。 argv 是自变量的列表(不是数组)。argv0 是调用的文件名(可以是符号链接)。env是根据当前 外壳的环境变量名建立下标的数组。errorCode 存储有关最近的 Tcl 错误信息,errorInfo 包含对这同一个错误事件的堆栈跟踪。 该列表还有另外 12 个 tcl_xxx 变量,从 tcl_interactive 到 tcl_version。可以在 Tcl/Tk in a Nutshell 中找到好的总结,(有关更多信息,请参阅本教程末尾的“参考资料”)。
在左边的样本代码中,使用了其中几个变量以及(又一次)使用了一些反斜杠引用的字符(\n 和 \a)。\char 允许替代非打印 ASCII 字符。这对于 UNIX 下的许多脚本语言和外壳环境都是常见的。如表中说明的那样,对于没有定义替代的反斜杠引用的字符只被简单地回送到输出。
\字符 | 替代 |
---|---|
\a | 响铃 |
\b | 退格 |
\f | 换页 |
\n 或 \newline | 新行 |
\r | 回车 |
\t | 水平制表 |
\v | 垂直制表 |
\space ("\ ") | 空格 |
\ddd | 八进制值 |
\xddd... | 十六进制值 |
\c | 回显‘c’ |
\\ | 反斜杠 |
~/tcltk$ cat maths.tcl #!/usr/bin/tclsh # # Demonstrate operators and # math functions set PI [expr 2 * asin(1.0)] if {$argc == 3} { set X [lindex $argv 0] set Y [lindex $argv 1] set Rad [lindex $argv 2] set Dist [expr sqrt(($X*$X)+($Y*$Y))] set Cir [expr 2*$PI*$Rad] set Area [expr $PI*$Rad*$Rad] puts stdout "Distance = $Dist" puts stdout "Circumference = $Cir" puts stdout "Area = $Area" } else { puts stdout "Wrong argument count!" puts stdout "Needs X, Y, and Radius" } ******** ~/tcltk$ ./maths.tcl 3 4 5 Distance = 5.0 Circumference = 31.4159265359 Area = 78.5398163397
Tcl 支持一组标准的运算符和数学函数。这些运算符包括算术、位和逻辑运算符,可以通过 expr 命令使用常规的运算符优先次序规则进行求值。另外,考虑到 Tcl 的实质是面向字符串的脚本语言,所以对一些数学函数进行了合理的补充:
左边这个示例使用了其中一些运算符和函数来计算指定点到原点之间的距离,并返回给定半径的圆的周长和面积。另外,在这个示例中,使用列表下标(lindex)命令来访问 $argv 的个别元素。
... # # parse command line switches set Optimize 0 set Verbose 0 foreach Arg $argv { switch -glob -- $Arg { -o* {set Optimize 1} -v* {set Verbose 1} default { error "Unknown $Arg" } } } set LineCount 0 while {[gets stdin Line] >= 0} { # to confuse Vanna White... Remove_Vowels $Line \ $Optimize $Verbose incr LineCount } return LineCount ...
Tcl 中的循环命令是 while、for 和 foreach。条件(转移)命令是 if/then/else/elsif 和 switch。上述命令的限定语句是 break、continue、return和 error。最后,catch 命令提供了错误处理能力。
if/then/else/elsif 已在前面几屏中演示过。在正式语法中会用到 then,但通常会省略掉它。
在左边这个示例中,foreach 结构给 switch 命令提供命令行自变量。当处理自变量时(注意:不正确的输入会终止脚本,因为还没有实现处理错误的 catch),while 循环通过为每一行调用过程并同时对行计数器加 1 来处理输入。代码段结束时,返回处理的行数。
~/tcltk$ tclsh % set Phrase "hello, world!" hello, world! % string toupper $Phrase HELLO, WORLD! % string totitle $Phrase Hello, world! % string match ello $Phrase 0 % string match *ello* $Phrase 1 % string length $Phrase 14 % append Phrase "Nice day, eh?" hello, world! Nice day, eh? % string toupper $Phrase HELLO, WORLD! NICE DAY, EH? % string wordend $Phrase 7 12
字符串是 Tcl 中的基本数据类型。string 命令实际上是一组命令,这些命令都是属于 string 的。在使用中,正如在左边的示例中所看到,string读取的方式非常象来自 OOP 编程特定对象方法的应用程序。
表示信息的 string 命令是 length 和 bytelength(可以有所不同,这取决于字符集)。返回布尔值(1 或 0)的比较是 compare、equal 和match。这里的模式匹配是由“文件名替换”(简单类型的匹配通常与外壳操作相关)完成。还可以通过独特的 regex 和 regsub 命令来使用“高级正规表达式”。
在 Tcl 中执行 index、last、first、wordend 和 wordstart 命令可以实现下标功能。字符串修改是由tolower、toupper、totitle、trim、trimleft、trimright、replace 和 map 来处理的。后者需要预先定义一个字符映射表。用 range 抽取子字符串,用 repeat 多次输出字符串。
可以使用 append 命令,将文本添加到现有变量中。通过使用与 C 语言的 printf 命令相同的样式和约定,format 命令可用来生成输出字符串。scan 对字符串进行解析并将值赋值给变量。最后,从 Tcl 8.0 开始,用 binary format 和 binary scan 命令添加了将二进制数据作为字符串处理的功能(因而能够处理空字符,而不会失败)。
~/tcltk$ tclsh % set c1 {Bob Carol} Bob Carol % set c2 [list Ted Alice] Ted Alice % set Party1 [list $c1 $c2] {Bob Carol} {Ted Alice} % set Party2 [concat $c1 $c2] Bob Carol Ted Alice % linsert $Party1 1 Richard {Bob Carol} Richard {Ted Alice} %
列表在 Tcl 中有两个主要用途。我们已经在通过 foreach 命令(在 Tcl 中的循环和分支中找到)处理命令行自变量的环境中看到第一个用途。第二个用途是动态地构建 Tcl 命令的元素,可以在本教程后面看到使用 eval 命令来执行这种用途。
list 命令接受它的所有自变量并将它们返回在一个列表环境中。自变量可以是值或变量。在左边这个示例中,可以手工创建列表,或可将其它列表视作自变量来使用列表(从而保存第一个“Party”的两对方向)。或者,concat 命令用于将两个或多个列表合并到顶级项的单个实体,返回第二个更有趣的“Party”。
其它一些有用的列表命令及其语法是:
其余列表命令还包括 lreplace、lsearch 和 lsort。split 命令将字符串作为输入并生成经过正确解析的列表,并且在指定的字符处断开字符串。join 执行相反操作,接受列表元素并将它们串在一起,用 joinstring 分隔它们。
~/tcltk$ tclsh % set People(friend) Tom Tom % set People(spouse) Marcia Marcia % set People(boss) Jack Jack % array names People friend boss spouse % set Person $People(friend) Tom % array get People friend Tom boss Jack spouse Marcia % set People(friend) \ [concat $People(friend) Bob] Tom Bob % set Person $People(friend) Tom Bob %
理解 Tcl 数组的捷径是,将它们视作与 Perl 散列相同的东西。Tcl 数组不是用数字建立下标的线性数据结构,除非选择对数据强加那种解释。尽管带空格的字符串需要用引号括起或需要一个变量引用,但下标(或键)可以是任何字符串。
正如一般的变量一样,使用 set 命令初始化数组,如左边所示。圆括号内是给出的下标部分。请注意,圆括号不象花括号或双引号那样提供分组。一旦初始化为数组,就不能将变量作为单一变量来访问。如左边列表底部所示,数组元素也可以是列表。
array 命令是一种多用途工具,很象 string。array exists 命令用于测试变量是否作为数组存在,array get 用于将数组转换成列表格式,array set用于将列表转换为数组,array names 用于返回下标列表,array size 用于返回对下标进行计数的结果。搜索整个数组有它自己的一组四个命令:array startseach、array anymore、array nextelement 和 array donesearch。
虽然设计之初 Tcl 数组是一维的,但有一个模拟多维结构的好方法。因为下标是任意字符串,所以二维数组可以声明如下:
set i 1 ; set j 10 set array($i,$j) 3.14159 incr $j set array($i,$j) 2.71828
这些数组键实际上分别只是字符串“1,10”和“1,11”,但对访问数据来说,谁知道这之间的差异?
#!/usr/bin/tclsh # # Demonstrate procedures and # global scoping briefly set PI [expr 2 * asin(1.0)] proc circum {rad} { global PI return [expr 2.0 * $rad * $PI] } proc c_area {rad} { global PI return [expr $rad * $rad * $PI] } set rad 3 puts stdout "Area of circle of\ radius $rad is [c_area $rad],\n\ the circumference is\ [circum $rad].\n" ********* ~/tcltk$ ./protest.tcl Area of circle of radius 3 is 28.2743338823, the circumference is 18.8495559215.
proc 命令定义 Tcl 过程。一旦定义后,就可以象内置的 Tcl 命令那样调用或使用它。另外,可以用缺省值定义这些参数;例如,将左边的定义更改为读取proc c_area { {rad 1} } 。这将调用没有参数的 c_area 过程,并返回单位圆的面积。
所使用的 rename 命令正如它的字面含意一样,为现有命令或过程提供新名称。使用 rename 有两个明显的原因。第一个原因是,通过重命名原有过程,然后用相同名称的过程替换它,来给现有命令添加功能。过程可以调用原过程,必要时可以添加一些需要的功能。使用 rename 的第二个原因是,映射一个不存在的命令,例如,象 rename exec {}; 一样,可以防止用户执行外部命令。
作用域规则描述了过程和变量名以及值在程序的不同层次上的可见性。例如,在脚本的最外层定义的变量是全局变量。缺省情况下,全局变量是不可见的,在过程内部也不可用它们的值。这允许过程的编写者自由地定义变量名并赋值,而不必担心覆盖对于局部作用域上未知的重要变量。要使全局变量在过程内部变得可见,必须将它声明为在过程内,就象在上一屏的示例中,对 PI 使用 global 命令那样。
upvar 命令提供将局部变量与另一个作用域中变量的值相关联的设施。这允许根据名称将变量调用进过程,这对于当过程需要可以修改在另一个作用域的值而不仅仅使用它时,就显得非常方便。这个命令语法是 upvar level $VarName LocalVar,其中 level 是到当前作用域之外的步骤数。“#0”表示全局作用域这一层。
#!/usr/bin/tclsh # # Demonstrate Data Structures # using procedural wrappers proc UserAdd { Acct rName eMail phone } { global uData if {[info exists uData($Acct,rname)]} { return 1 } set uData($Acct,rname) $rName set uData($Acct,email) $eMail set uData($Acct,phone) $phone return 0 } puts stdout [UserAdd bpb\ Brian [email protected] 555-1212] puts stdout [UserAdd tom\ Tom [email protected] 555-1212] puts stdout [UserAdd bpb\ Brian [email protected] 555-1212] ****** ~/tcltk$ ./datas.tcl 0 0 1
除简单的多维数组以外,通常建议用已专门用于过程接口的数组来实现 Tcl 数据结构。从结构的使用者角度来看,虽然这种设计隐藏了具体实现细节,但提供了执行重要的错误检查能力。
左边这个示例中,在将 uData 声明为全局变量后,代码执行检查,以查看帐户是否已经不存在。如果存在,则过程返回(非零)错误消息。这个返回可以用于切换到生成错误文本输出。对于本例,我们简单地提供三个连续输入,其中包括一次重复输入。这会产生如示例底部所示的输出,“1”表示由于一个重复的帐户名称而引起的一个错误返回,这是我们有意这样做的。
其它可能的数据结构包括数组列表、已链接或双重链接的数组或其中的各种排列。因为 Tcl 8.0 所具有的列表重实现提供了不变的访问次数,所以数组列表结构相当有效。
~/tcltk$ tclsh % file exists hello3.tcl 1 % file executable testit 0 % file pathtype ./hello3.tcl relative % set dir1 home home % set dir2 brian brian % set dir3 tcltk tcltk % file join /$dir1 dir2 dir3 /home/dir2/dir3 % file delete testit~ %
文件和路径操作是跨平台环境中具有挑战性的问题。对于主机 OS,Tcl 使用 UNIX 路径名(缺省情况下,用‘/’字符分隔)和本机路径名结构。即使当程序内的数据构造正确时,也很难确保用户输入与系统需求匹配。file join 命令用于将 UNIX 格式转换成本机路径名。其它路径字符串命令包括 file split、dirname、file extension、nativename、pathtype 和 tail。
在它扮演的“工具控制语言”角色中,Tcl 有许许多多种内部文件测试和操作功能。每条命令都以 file 开始,正如 file exists name 中一样。其它测试命令(它们都返回布尔值)包括 executable、isdirectory、isfile、owned、readable 和 writable。
文件信息和操作(再提醒您一次,所有都是以 file 开始)是通过atime、attributes、copy、delete、lstat、mkdir、mtime、readlink、rename、rootname、size、stat 和 type 来完成。请注意,在 Windows 或 Mac 环境中运行一些文件信息命令时,可能会返回未定义的数据,因为例如在那些文件系统中没有表示索引节点和符号(和硬)链接数据。
使用 file ... 命令而不使用通过 exec 的本机命令的好处在于,前者会提供一个可移植接口。
整个文档只简单地着重于 Tcl 语言的这一部分。然而,在您“品尝”完左边这个 tclsh 示例中这些命令的味道并对此感到满足之后,然后请继续阅读本教程结尾部分列出的“参考资料”。
~/tcltk$ tclsh % nslookup orbdesigns.com Server: 192.168.1.3 Address: 192.168.1.3#53 Name: orbdesigns.com Address: 64.81.69.163 % set d [date] Sun Mar 25 13:51:59 PST 2001 % puts stdout $d % set d [exec date] Sun Mar 25 13:52:19 PST 2001 % puts stdout $d Sun Mar 25 13:52:19 PST 2001 ****** % if [catch {open foo r} Chan] { puts stdout "Sorry, Dave...\n" } % gets $Chan One % gets $Chan Two % eof $Chan 0 % close $Chan %
exec 命令用于显式地执行外部命令。在 Linux 下,当 Tcl 处于交互方式时,可以直接运行大多数外部命令,如左边示例所示。用 exec 运行时,会将程序的 stdout 输出返回到 Tcl,而不是返回到屏幕,这允许将数据赋值给变量。当程序在后台启动时,立即返回的值是程序的 PID。exec程序可以充分利用 UNIX 环境中的 I/O 重定向和管道。
其它进程命令有 exit(终止正在运行的 Tcl 脚本)和 pid(返回当前或指定进程的 PID),对于出于各种目的的情况,这非常便利。Tcl 不合并任何本机进程控制命令,但可以将 exec 命令与 PID 数据一起使用来实现许多任务。
文件操纵使用下列命令:open、close、gets、puts、read、tell、seek、eof 和 flush。如左边所示,在文件打开命令期间 catch 命令对错误检查是有用的。当在遇到新的一行字符之前需要打印程序输出时,如在用户数据提示符中,使用 flush 来写输出缓冲区。
另外一个功能(在受支持的环境中)是以打开文件的方式打开管道的能力。例如,用 set Channel [open "|sort foobar" r] 打开管道通道后,第一个 gets 的输出将是“Eight”(文件数据“One”到“Ten”的输出在 10 个单独的行上按字母顺序排列)。
~/tcltk$ cat input01.txt 1 + 2 4 + 5 7 - 9 ~/tcltk$ tclsh % set InFile [open input01.txt r] file3 % while {[gets $InFile Op] >= 0} { set Operation "expr $Op" set Result [eval $Operation] puts stdout "$Op = $Result\n" } 1 + 2 = 3 4 + 5 = 9 7 - 9 = -2 %
在这个示例中,您可以感到 eval 命令的强大功能。在正常情况下,Tcl 解释器以一遍方式(one-pass)操作:它首先对输入的命令行(可能延伸在几个物理行上)进行解析,并执行任何替代。然后开始执行,除非找到不正常或残缺命令。eval 允许第二遍方式(second pass)(或许更精确地讲,是预通过(pre-pass))。因而,可以先动态构造 Tcl 命令,然后进行解析并执行它。
在左边的列表中,输入文件由三行组成,每行都显示了一种算术运算。调用 tclsh 后,文件以只读方式打开并与 $InFile 变量相关联。while循环每次将一行读入到 $Op 中。然后,通过预先计划将 expr 映射到 $Op 变量来构造整个 Tcl 命令。然后,扩展,求值,从而分配结果。最后,在 stdout 上显示每步操作和结果。
虽然该样本演示了相对琐细的 eval 应用程序,但从概念上讲,可以根据已知语法的输入文件的输入,很容易地将它扩展为动态文件和/或目录处理,或扩展为对文件类型、许可权、访问时间或任何种类的可测试元素的基本操作。
回页首
Tk 是对 Tcl 的图形工具箱扩展。Tk 发行版与 Tcl 的发行版是在一起的。在接下来的几屏中,将回顾 Tk 小部件集,研究一些配置选项并设置一些示例来演示 Tk 的有用性。
很难使任何 PHB(挑剔的老板)相信本教程的这一部分是与工作相关的。毕竟,它是有关小部件的,概念上小部件与“玩耍……”更接近,但这是工作,所以让我们钻研它。首先,这里是 Tk 增强的“Hello, World!”代码
#!/usr/bin/wish # # Hello World, Tk-style button .hello -text Hello \ -command {puts stdout \ "Hello, World!"} button .goodbye -text Bye! \ -command {exit} pack .hello -padx 60 -pady 5 pack .goodbye -padx 60 -pady 5
在第一行中调用 wish(Tk 外壳)会产生缺省大小的窗口小部件。然后,定义了两个按钮小部件 .hello 和 .goodbye ― 它们被放进窗口中,该窗口收缩至由指定按钮间距定义的大小。执行该脚本时,在左边会显示一个对话框。单击 Hello 按钮,在父终端窗口中得到“Hello, World!”输出,单击 Bye! 以终止脚本。
在创建 Tk 小部件时,几乎很少使用命令。一半以上都是按钮或文本小部件的变体,如下面的列表所示。其中几项在下一屏中演示。
象一些简单的 Tk 代码一样,下面这个清单中的代码生成左边的图像。在图像的文本窗口中显示了通过 OK 按钮调用的过程代码和样本输出。
~/tcltk$ wish % . configure -width 200 -height 400 % label .header -text "Tk Tutorial Example" .header % place .header -x 5 -y 2 % scale .slider -from 1 -to 100 -orient horiz .slider % .slider configure -variable SlidVal % place .slider -x 5 -y 20 % entry .slidbox -width 5 -textvariable SlidVal .slidbox % place .slidbox -x 120 -y 38 % radiobutton .one -text "Don't Worry" -variable Mood -value 1 .one % radiobutton .two -text "Be Happy" -variable Mood -value 2 .two % place .one -x 5 -y 70 % place .two -x 5 -y 90 % text .twindow -width 22 -height 14 -font {clean -14} .twindow % place .twindow -x 5 -y 120 % button .ok -command {process_data $SlidVal} -text "OK" .ok % button .cancel -command {exit} -text "Cancel" -background red .cancel % place .ok -x 15 -y 350 % place .cancel -x 120 -y 350
有二十多条 Tk 命令用于对 Tk 小部件集的使用、增强或补充。它们包括 bell,响铃,这取决于对正在运行的 X Window 系统的配置。bind 创建 Tcl 脚本和 X 事件之间的关联;例如,指定的键盘/鼠标组合操作。clipboard 是另一种多功能 Tk 命令 ― 它包含用于清除内容、装入内容和将内容粘贴到 Tk 剪贴板以及从 Tk 剪贴板粘贴内容(这不同于正在使用的 X 或窗口管理器本身所具有的任何剪贴板功能)。
destroy 用于删除窗口及其所有子窗口。在‘.’(根)窗口中使用时,它删除整个应用程序。event 是一种功效强大的工具,用于生成虚拟窗口事件并将这些事件插入正在处理的队列中,就好象实际事件(例如,用鼠标单击按钮)已经真的发生一样。font 命令用于创建指定的系统字体实例。它允许系统字体的本地(对于脚本而言)命名、已命名字体的属性修改以及字体的“删除”。在 wish 提示符下输入 font families,可以获得可用字体的列表。
焦点是窗口管理舞台中的一个重要概念 ― 在任何给定的显示中,每次只能有一个窗口“注意”键盘和鼠标。Tk focus 命令用于将脚本控制交给显示焦点,并将它发送到指定窗口。补充函数 grab 允许 Tk 独占对某处的显示焦点,在该处,在窗口环境中报告位于窗口之外发生的事件。当要在任何其它系统活动发生之前强迫完成某一选项时,这是有用的。
继续概述 Tk 命令,下一个是 grid,控制 Tk 窗口几何性质的接口。它用于在窗口中以行和列的格式安排小部件。 lower(和补充命令 raise)解决子窗口的可见性。下层(lowered)窗口不会遮掩与其重叠的兄弟窗口;上层(raised)窗口被带到最上层。在显示多个文档情形下会经常使用。许多 Tk 小部件和命令都使用一组公共的标准选项。可以使用 option 命令来查看或添加它们。
对于将小部件和子窗口放入窗口中,有两条命令:pack 和 place,它们都已演示过。其最简单的使用,pack 将一个或多个小部件添加到窗口,并将这些对象周围的窗口缩小至我们在本节开始部分的 Tk Hello 示例中所看到的那样,除非另有指明。place 使用相对或绝对尺寸来设置和显示父窗口中的对象,例如,从左边开始的 5 个像素,或窗口下一半(0.5)。
其它命令包括 selection,X 对象选择工具集的接口;tk,它提供对 Tk 解释器内部状态的所选择部分的访问;winfo 命令,用于检索有关 Tk 管理的窗口的数据;wm,正在运行窗口管理器的界面,用于设置从标题栏文本到所有类型的几何规范和约束的多个功能。
我需要每天运行的 LAN 转换脚本的接口。所以为方便使用,让我们使用 Tcl/Tk 来构建一个小工具。我希望它能够根据主目录中的 ASCII 配置文件提供一些选择。该文件包含下表中所示的数据:
# ~/.netsetrc # 03.26.2001 bilbrey # space between name and command Home /usr/local/bin/nethome Office /usr/local/bin/netoffice Admin /usr/local/bin/netadmin
应用程序(在下一屏显示完整代码清单和图像)读取配置文件并对按钮名及其相关操作的每个非空白、非注释行进行解析。虽然通过定义三个按钮来运行显式程序,会使编写脚本变得更加容易,但这种更一般的解决方案允许我只要把单一行添加到 ~/.netsetrc 中,就可添加我想要的任何功能。
该代码的缺点是,它不容许配置文件格式有严重错误。在按钮按下时,它期望是这样的格式:单字按钮名,接着是一个空格,接着是执行的命令(如果有必要,可以有自变量)。不过,从理论上讲,与无结构的用户输入相比,配置文件更容易保持在一行上。
#!/usr/bin/wish # # netset.tcl # 03.26.2001 bilbrey set ConFile "~/.netsetrc" if [catch {open $ConFile r} Conf] { puts stderr "Open $ConFile failed" return 1 } # parse config, define buttons set Bcount 0 while {[gets $Conf Cline] >= 0} { if {1 == [string match #* $Cline]} continue if {[string length $Cline] < 4} continue set Nend [string wordend $Cline 0] incr Nend -1 set Bname [string range $Cline 0 $Nend] set Cbeg [expr $Nend + 2] set Bcomd "exec " append Bcomd [string range $Cline $Cbeg end] incr Bcount set NextBut "button$Bcount" button .$NextBut -text $Bname -command $Bcomd } if {$Bcount == 1} { puts stderr "No buttons defined" return 2 } # display buttons while {$Bcount >= 1} { set NextBut "button$Bcount" pack .$NextBut -padx 10 -pady 10 incr Bcount -1 } button .exit -text Exit -command {exit} pack .exit -padx 10 -pady 10
回页首
Expect 是 Tcl 和 Tk 语言的扩展。Expect 为使交互式程序的脚本编制自动化,提供了简单而功效强大的接口。另外,Expect 使交互式应用程序嵌入 GUI 变得容易。Expect 的开发与 Tcl/Tk 的出现是同时发生的,两者目前的版本都是 5.32。
Expect 的作者是 Don Libes,他在美国国家标准与技术学会(NIST)工作。Expect 主页驻留在 NIST 服务器上。(然而,Expect 和任何相关的商业或非商业产品显然都未经 NIST 认可。)在下面几屏中,将研究从源代码示例目录中精选出来的一些 Expect 脚本示例,并简要概述它的命令语法。
为什么要学习 Expect?引用 Don 论文中的一句话,“使用 expect,使系统管理任务自动化”(USENIX LISA 会议 1990 年 10 月)“……结果是 UNIX 系统管理员的工具箱里充斥着曾经见到过的一些最差的用户界面。只有完全重新设计才能帮助解决所有这些问题,expect 可用来处理许多这些问题。”
#!/usr/local/bin/expect -- # ftp-rfc <rfc-number> # ftp-rfc -index # retrieves an rfc (or the index) from uunet exp_version -exit 5.0 if {$argc!=1} { send_user "usage: ftp-rfc \[#] \[-index]\n" exit } set file "rfc$argv.Z" set timeout 60 spawn ftp ftp.uu.net expect "Name*:" send "anonymous\r" expect "Password:" send "[email protected]\r" expect "ftp>" send "binary\r" expect "ftp>" send "cd inet/rfc\r" expect "550*ftp>" exit "250*ftp>" send "get $file\r" expect "550*ftp>" exit "200*226*ftp>" close wait send_user "\nuncompressing file - wait...\n" exec uncompress $file
在介绍 Expect 之前,请先研究左边的示例。它只是对在标准 Expect 源代码分发版的示例目录中的版本略加修改而得到的,所有示例都在这个目录下。让我们看一下代码……
这个程序使 UUNet 档案文件中 IETF RFC(Request for Comment)文档的 FTP 检索自动化。脚本的第一行调用 Expect 外壳。注意,我已经给出了可执行文件的完整路径名。这是最安全的,因为很难知道任何给定用户的路径环境。该脚本先检查 Expect 版本,然后打印用法消息,除非给出正确的自变量数目。
接下来,设置 timeout 值,在下一行产生的 FTP 会话无法正确连接时,防止 Expect 脚本锁住系统资源。脚本的其余部分大多数是几组 expect/send 命令对。每个 expect 命令等待来自产生程序(在本例中是 ftp)的指定输出,后 send 正确响应。注意,在 cd 和 get 指令之后有两个 ftp 错误代码的陷阱。对于每种情况,错误代码 550 与第一个条件匹配,如果为真,则脚本存在。否则,执行 250 代码(表示成功),expect 进入下一条命令。
接收文档之后,脚本向 ftp 会话发出 close 命令。wait 命令挂起脚本处理,直到 ftp 终止为止。最后,脚本将给用户发送一个消息,对已下载的 RFC(或 rfc 下标)进行解压,然后隐式退出(缺省情况下)。
#!../expect -f # wrapper to make passwd(1) be non-interactive # username is passed as 1st arg, passwd as 2nd set password [lindex $argv 1] spawn passwd [lindex $argv 0] expect "password:" send "$password\r" expect "password:" send "$password\r" expect eof
Expect(作为语言,‘E’大写)有四个关键命令。第一个是 expect(命令,小写‘e’),如果找到匹配,它搜索模式并执行命令。对于每条 expect命令,可以有几个组,每个组都是由选项标志、与之匹配的模式以及要执行的命令或命令主体组成。缺省情况下,expect“侦听”SDTOUT 和 STDERR,直到找到匹配或 timeout 期满为止。
缺省情况下,使用 Tcl 的字符串匹配设施来匹配模式,它实现文件名替换,类似于 C 外壳模式匹配。-re 标志调用 regexp 匹配,-ex 表明必须是精确匹配,不带通配符或变量扩展。expect 的其它可选标志包括 -i 和 -nocase,前者表示要监控产生的进程,后者强迫在匹配之前将进程输出变为小写。对于完整的说明,在命令提示符下输入 man expect,以查看 Expect 的系统手册页面文档。
第二个重要命令是 send,它用于为由 Expect 脚本正在监控的进程生成输入。send 合并选项以发送给指定的产生的过程(-i),缓慢地发送(-s,例如,在串行通信中,为了不使缓冲区溢出)以及其它几个选项。
#!/usr/local/bin/expect # Script to enforce a 10 minute break # every half hour from typing - # Written for someone (Uwe Hollerbach) # with Carpal Tunnel Syndrome. # If you type for more than 20 minutes # straight, the script rings the bell # after every character until you take # a 10 minute break. # Author: Don Libes, NIST # Date: Feb 26, '95 spawn $env(SHELL) # set start and stop times set start [clock seconds] set stop [clock seconds] # typing and break, in seconds set typing 1200 set notyping 600 interact -nobuffer -re . { set now [clock seconds] if {$now-$stop > $notyping} { set start [clock seconds] } elseif {$now-$start > $typing} { send_user "\007" } set stop [clock seconds] }
左边是称为 carpal 的脚本,它也是来自源代码 Expect 分发版的另一个示例。
spawn 是用于创建新进程的 Expect 命令。它已经出现在我们使用过的每个示例中。在左边,它把路径拖到缺省外壳可执行文件并产生新实例。在这样做时,spawn 返回一个进程标识(在变量 spawn_id 中设置)。这可以在脚本中保存并设置,这给予了 expect 进程控制能力。
interact 是 Expect 用来打开用户与产生进程之间通信的命令。-nobuffer 标志将与模式匹配的字符直接发送给用户。-re 告诉 interact 将接下来的模式用作标准正规表达式,‘.’是与输入时每个字符匹配的模式。在交互方式中,缺省情况下,Expect 的 STDOUT 和 STDERR 流的重定向也返回给用户。
当脚本调用交互式程序时,缺省情况下,Expect 拦截所有输入和输出(STDIN、STDOUT 和 STDERR)。这允许 Expect 搜索与程序输出匹配的模式,并将输入发送到产生的进程,以模拟用户交互。另外,Expect 可以将进程的控制传递给用户(如果这样指示的话),或者根据请求控制。
这些特性不仅使 Expect 对于公共管理任务变得非常有用,而且证实了 Expect 有益于构建测试脚本,以在程序开发期间执行 I/O 验证。
最后,有一个极其有用的程序 autoexpect。它本身是一个 Expect 脚本,autoexpect 监控命令行交互式程序,生成精确复制该交互的 Expect 脚本。现在,虽然通常不需要它,但很容易拿几个 autoexpect 会话的结果,概括 expect 模式,然后将它们剪贴到期望的配置中。已经在多处提到过,学习 Expect 的最佳工具是运行 autoexpect 并使用这些结果。
回页首
Expect 仅仅是诸多 Tcl/Tk 扩展中的一个先驱者。几个是常规实用程序,而多数都是特定软件或应用程序。因为 Scriptics 仍然是所有 Tcl/Tk 的中央资源库,所以,对于进一步探索 Tcl/Tk,该站点上的 Extensions 页面是有价值的资源。
在下面几屏中,我们将简要地看一下几个主要扩展,了解它们的显著特性和吸引人的地方。
于 1993 提出的 [incr Tcl](读作 inker tickle)为 Tcl/Tk 提供面向对象的功能。[incr Tcl] 提供对象、类和名称空间等特性。这些特性使得带数据封装、组合和继承的 Tcl 来构建大项目变得更加容易。如下做到这一点:classname、objname 和 delete,都是对象命令。用 body、class 和configbody 命令创建和编辑类。其它各种命令有 code、ensemble、find、local 和 scope。
图形化的 [incr Tcl] 是 [incr Tk]。该工具扩展为 GUI,它提供了可伸缩性和数据隐藏的简易性所需的相同 OO 功能,使得分割大的编程作业更容易。[incr Tk] 提供了一些新的基类:itk::Archetype、itk::Widget 和 itk::Toplevel。这些类与一组完整的方法互相补充。
构建于 [incr Tk] 基础上,是号称 Mega-Widget 集的 [incr Widgets]。该工具允许十分容易地定义和显示复杂对象,譬如文件选择框。借用 [incr Widgets] Web 页面,只是使用下面的命令就创建了左边的图像:fileselectiondialog .fsd ; .fsd activate.
可以从 tcltk.com 的链接很容易地找到这些各种各样的扩展。
为了对这一对特别的 GUI 和图形 Tcl/Tk 扩展同样的不公平(对它们都不公平处理),先向您介绍 BLT(http://tcltk.com/blt/)。BLT 是一种 Tk 扩展,它提供了一些功能以便方便地创建多元素小部件,这对于简单的 Tk 极具挑战性。BLT 命令包括table、graph、barchart、vector、spline、busy、bgexec、drag&drop、htext、bitmap、winop、watch 和 bltdebug。
接下来是 Tix,它表示 Tk Interface eXtension。Tix 的当前版本是4.0,它提供了 43 个命令,其中大多数命令要么是 Mega-widget 要么是用于构建 Mega-widget 的一些组件,还有一些实用程序。Tix 的 Web 站点 http://tix.mne.com/ 声称“有了 Tix,可以忘记 TK 小部件的琐碎细节并全神贯注于解决您手边的问题。”当用这些命令(如 tixDirList、tixFileSelectDialog、tixPopupMenu、tixScrolledWindow 等等)迅速地创建实用的界面时,您会很容易看到这项声称的基础。
扩展的 Tcl,TclX 实际上不仅仅是另一种“扩展”。用作者的话来说,“扩展的 Tcl 面向系统编程任务和大的应用程序开发。TclX 为本机操作系统提供了附加接口以及许多新的编程结构、文本操作工具和调试能力。”可在 http://www.neosoft.com/TclX/ 上找到 TclX 的在线主页。
在过去几年中,许多 TclX 的原始特性已使它成为核心的 Tcl 发布。然而,TclX 小组很有见识,将动态装入库和包、网络编程支持和提供命令访问的过程等这些特性添加到通常称作的 expr 数学函数和更多函数中。
大多数标准 Linux 分发版都带 TclX,它作为一个包,可以在安装 Linux 时,选择性地安装它。或者,可以从源代码编译它,与 Tcl 和 Tk 连接在一起。最近 TclX 版本的非常好的特性之一是名为 tclhelp 的程序,它是 Tcl 和 Tk 帮助浏览器,可以很方便地进行参照。强烈向您推荐。
不出意料的话,对于这些流行的编程环境,除了我在本教程中所详尽讨论的一些之外,还有许多扩展。可以到 Scriptics 的 Extensions 页面,学习更多有关下面这张表中 Tcl 和 Tk 扩展所具有的一些能力: