探索 Tcl/Tk 的基础构造,包括用户输入、输出、变量、条件评估、简单函数和基础事件驱动编程。
我写这篇文章的初衷源于我想更深入地利用基于 Tcl 的 Expect。这让我写下了以下两篇文章:通过编写一个简单的游戏学习 Tcl 和 通过编写一个简单的游戏学习 Expect。
我进行了一些 Ansible 自动化工作,逐渐积累了一些本地脚本。有些脚本我频繁使用,以至于以下循环操作变得有些烦人:
- 打开终端
- 使用
cd
命令跳转至合适的目录 - 输入一条带有若干选项的长命令启动所需的自动化流程
我日常使用的是 macOS。实际上我更希望有一个菜单项或者一个图标,能够弹出一个简单的界面接受参数并执行我需要的操作,这就像在 Linux 的 KDE 中一样。
经典的 Tcl 类书籍都包含了关于流行的 Tk 扩展的文档。既然我已经深入研究了这个主题,我尝试着对其(即 wish
)进行编程。
虽然我并非一名 GUI 或者前端开发者,但我发现 Tcl/Tk 脚本编写的方式相当直接易懂。我很高兴能重新审视这个 UNIX 历史的古老且稳定的部分,这种技术在现代平台上依然有用且可用。
安装 Tcl/Tk
对于 Linux 系统,你可以按照下面的方式安装:
$ sudo dnf install tcl
$ which wish
/bin/wish
而在 macOS 上,你可以通过 Homebrew 来安装最新版的 Tcl/Tk:
$ brew install tcl-tk
$ which wish
/usr/local/bin/wish
编程理念
许多编写游戏的教程都会介绍到典型的编程语言结构,如循环、条件判断、变量、函数和过程等等。
在此篇文章中,我想要介绍的是 事件驱动编程。当你的程序使用事件驱动编程,它会进入一个特殊的内置循环,等待特定的事件发生。当这个特定的事件发生时,相应的代码就会被触发,产生预期的结果。
这些事件可以包括键盘输入、鼠标移动、点击按钮、定时器触发,甚至是任何你的电脑硬件能够识别的事件(可能来自特殊的设备)。你的程序中的代码决定了用户看到了什么,以及程序需要监听什么输入,当这些输入被接收后程序会怎么做,然后进入事件循环等待输入。
这篇文章的理念并没有脱离我之前的 Tcl 文章太远。这里最大的不同在于用 GUI 设置和用于处理用户输入的事件循环替代了循环结构。其他的不同则是 GUI 开发需要采取的各种方式来制作一个可用的用户界面。在采用 Tk GUI 开发的时候,你需要了解两个基础的概念:部件widget和几何管理器geometry manager。
部件是构成可视化元素的 UI 元素,通过这些元素用户可以与程序进行交互。这其中包括了按钮、文本区域、标签和文本输入框。部件还包括了一些选项选择,如菜单、复选框、单选按钮等。最后,部件也包括了其他的可视化元素,如边框和线性分隔符。
几何管理器在放置部件在显示窗口中的位置上扮演着至关重要的角色。有一些不同的几何管理器可以供你使用。在这篇文章中,我主要使用了 grid
几何来让部件在整齐的行中进行布局。我会在这篇文章的结尾地方解释一些几何管理器的不同之处。
用 wish 进行猜数字游戏
这个示例游戏代码与我其他文章中的示例有所不同,我将它分解为若干部分以方便解释。
首先创建一个基本的可执行脚本 numgame.wish
:
$ touch numgame.wish
$ chmod 755 numgame.wish
使用你喜欢的文本编辑器打开此文件,输入下列代码的第一部分:
#!/usr/bin/env wish
set LOW 1
set HIGH 100
set STATUS ""
set GUESS ""
set num [expr round(rand()*100)]
第一行定义了该脚本将通过 wish
执行。接下来,创建了几个全局变量。这里我使用全部大写字母定义全局变量,这些变量将绑定到跟踪这些值的窗口小部件(LOW
、HIGH
等等)。
全局变量 num
是游戏玩家要猜测的随机数值,这个值是通过 Tcl 的命令执行得到并保存到变量中的:
proc Validate {var} {
if { [string is integer $var] } {
return 1
}
return 0
}
这是一个验证用户输入的特殊函数,它只接受整数并拒绝其他所有类型的输入:
proc check_guess {guess num} {
global STATUS LOW HIGH GUESS
if { $guess < $LOW } {
set STATUS "What?"
} elseif { $guess > $HIGH } {
set STATUS "Huh?"
} elseif { $guess < $num } {
set STATUS "Too low!"
set LOW $guess
} elseif { $guess > $num } {
set STATUS "Too high!"
set HIGH $guess
} else {
set LOW $guess
set HIGH $guess
set STATUS "That's Right!"
destroy .guess .entry
bind all <Return> {.quit invoke}
}
set GUESS ""
}
这是主要的猜数逻辑循环。global
语句让我们能够修改在文件开头创建的全局变量(关于此主题后面将会有更多解释)。这个条件判断寻找入力范围在 1 至 100 之外以及已经被用户猜过的值。有效的猜测和随机值进行比较。LOW
和 HIGH
的猜测会被追踪,作为 UI 中的全局变量进行报告。在每一步,全局 STATUS
变量都会被更新,这个状态信息会自动在 UI 中显示。
对于正确的猜测,destroy
语句会移除 “Guess” 按钮以及输入窗口,并重新绑定回车键,以激活 “Quit” 按钮。
最后的语句 set GUESS ""
用于在下一个猜测之前清空输入窗口。
label .inst -text "Enter a number between: "
label .low -textvariable LOW
label .dash -text "-"
label .high -textvariable HIGH
label .status -text "Status:"
label .result -textvariable STATUS
button .guess -text "Guess" -command { check_guess $GUESS $num }
entry .entry -width 3 -relief sunken -bd 2 -textvariable GUESS -validate all \
-validatecommand { Validate %P }
focus .entry
button .quit -text "Quit" -command { exit }
bind all <Return> {.guess invoke}
这是设置用户界面的部分。前六个标签语句在你的 UI 上创建了不同的文本展示元素,-textvariable
选项监控给定的变量,并自动更新标签的值,这展示了全局变量 LOW
、HIGH
、STATUS
的绑定。
button
行创建了 “Guess” 和 “Quit” 按钮, -command
选项设定了当按钮被按下时要执行的操作。按下 “Guess” 按钮执行了上面的 check_guess
函数以检查用户输入的值。
entry
部件更有趣。它创建了一个三字符宽的输入框,并将输入绑定到 GUESS
全局变量。它还通过 -validatecommand
选项设置了验证,阻止输入部件接收除数字以外的任何内容。
focus
命令是用户界面的一项改进,使程序启动时输入部件处于激活状态。没有此命令,你需要先点击输入部件才可以输入。
bind
命令允许你在按下回车键时自动点击 “Guess” 按钮。如果你记得 check_guess
中的内容,猜测正确之后会重新绑定回车键到 “Quit” 按钮。
最后,这部分设定了图形用户界面的布局:
grid .inst
grid .low .dash .high
grid .status .result
grid .guess .entry
grid .quit
grid
几何管理器被逐步调用,以逐渐构建出预期的用户体验。它主要设置了五行部件。前三行是显示不同值的标签,第四行是 “Guess” 按钮和 entry
部件,最后是 “Quit” 按钮。
程序到此已经初始化完毕,wish
shell 进入事件循环,等待用户输入整数并按下按钮。基于其在被监视的全局变量中找到的变化,它会更新标签。
注意,输入光标开始就在输入框中,而且按下回车键将调用适当且可用的按钮。
这只是一个初级的例子,Tcl/Tk 有许多可以让间隔、字体、颜色和其他用户界面方面更具有吸引力的选项,这超出了本文中简单 UI 的示例。
运行这个应用,你可能会注意到这些部件看起来并不很精致或现代。这是因为我正在使用原始的经典部件集,它们让人回忆起 X Windows Motif 的时代。不过,还有一些默认的部件扩展,被称为主题部件,它们可以让你的应用程序有更现代、更精致的外观和感觉。
启动游戏!
保存文件之后,在终端中运行它:
$ ./numgame.wish
在这种情况下,我无法给出控制台的输出,因此这里有一个动画 GIF 来展示如何玩这个游戏:
用 Wish 编写的猜数游戏
用 Wish 编写的猜数游戏
进一步了解 Tcl
Tcl 支持命名空间的概念,所以在这里使用的变量并不必须是全局的。你可以把绑定的部件变量组织进不同的命名空间。对于像这样的简单程序,可能并不太需要这么做。但对于更大规模的项目,你可能会考虑这种方法。
proc check_guess
函数体内有一行 global
代码我之前没有解释。在 Tcl 中,所有变量都按值传递,函数体内引用的变量的范围是局部的。在这个情况下,我希望修改的是全局变量,而不是局部范围的版本。Tcl
提供了许多方法来引用变量,在执行堆栈的更高级别执行代码。在一些情况下,像这样的简单引用可能带来一些复杂性和错误,但是调用堆栈的操作非常有力,允许
Tcl 实现那些在其他语言中实现起来可能较为复杂的新的条件和循环结构。
最后,在这篇文章中,我没有提到几何管理器,它们用于以特定的顺序展示部件。只有被某种几何管理器管理的部件才能显示在屏幕上。grid 管理器相当简洁,它按照从左到右的方式放置部件。我使用了五个 grid 定义来创建了五行。另外还有两个几何管理器:place 和
pack。pack 管理器将部件围绕窗口边缘排列,而 place 管理器允许固定部件的位置。除这些几何管理器外,还有一些特殊的部件,如 canvas
,text
和 panedwindow
,它们可以容纳并管理其他部件。你可以在经典的 Tcl/Tk 参考指南,以及 Tk 命令 文档页上找到这些部件的全面描述。
继续学习编程
Tcl 和 Tk 提供了一个简单有效的方法来构建图形用户界面和事件驱动应用程序。这个简单的猜数游戏只是你能用这些工具做到的事情的起点。通过继续学习和探索 Tcl 和 Tk,你可以打开构建强大且用户友好的应用程序的无数可能性。继续尝试,继续学习,看看你新习得的 Tcl 和 Tk 技能能带你到哪里。