X Window 系统使用 Tcl 和 Tk 编程

作者:Matt Welsh

任何曾经通过 Xlib 接口、神秘的 X Toolkit Intrinsics 甚至 Motif 编写过 X 应用程序的人都知道,这是一种不每小时多次用头撞击桌子就无法充分体会到的体验。C 语言级别的 X 编程通常可能非常复杂,迫使程序员专注于繁琐的技术问题,而不是简单地构建界面。(当然,这种复杂性的权衡是功能和灵活性。)这就是我们使用 Tcl 和 Tk 的原因。

Tcl(工具命令语言)是一种解释型脚本语言,与 C Shell 或 Perl 类似。它提供了您在这种语言中期望的所有基本功能:变量、过程、循环、文件 I/O 等等;没有什么太花哨或奇怪的东西。如果您曾经编写过 shell 脚本,您会发现 Tcl 很容易上手。

Tcl 的特别之处在于它可以嵌入到其他应用程序中。也就是说,Tcl 解释器是一个 C 例程库,您可以从自己的程序中调用它。例如;假设您正在编写一个调试器,其性质类似于 gdb。您需要例程来允许用户在提示符下输入命令(例如“step 10”或“breakpoint foot c:23”)。您还希望允许用户通过编写新的命令过程或修改状态变量来自定义调试环境。处理用户界面的一个好方法是使用 Tcl。您可以将 Tcl 解释器链接到您的应用程序,并且该语言的所有功能都将可用。用户在调试器提示符下输入的命令将由 Tcl 解释器执行,后者可以调用您编写的 C 函数。如果用户需要自定义应用程序的某些方面,他们可以编写 Tcl 脚本来实现新命令等等。

Tcl 本身可能不是很令人兴奋,但与 Tk 结合使用肯定会令人兴奋。Tk 是 Tcl 的一组扩展,它实现了用于编写 X Window 系统应用程序的命令。这些命令允许您创建按钮、滚动条、文本输入小部件、菜单等等;使您能够将 X 应用程序编写为简单的 Tcl 脚本。使用 Tcl 和 Tk,几乎不需要学习用于 X 编程的 C 库接口;Tcl 和 Tk 提供了对许多 X Window 系统功能的访问。此外,您可以将 Tcl 和 Tk 嵌入到您自己的 C 语言应用程序中,以极大地增强该系统的功能。

本文是关于 Tcl 和 Tk 系列文章的第一篇。在本文中,我将介绍如何将简单的 X 程序编写为 Tcl/Tk 脚本。下个月,我将介绍如何结合使用 Tcl/Tk 解释器和用 C 或 Perl 编写的程序。

Tcl 语言本身的语法相对简单明了,对于任何编写过 shell 或 Perl 脚本的人来说都是显而易见的。因此,我不会花太多时间描述 Tcl 语言本身的语法;我将专注于 Tk 工具包的 X 特定功能。有关 Tcl 的其他信息来源,请参阅边栏“获取 Tcl 和 Tk”。

基本的 Tcl/Tk 脚本

让我们直接深入一个简单的 Tcl/Tk 程序。Tcl/Tk 解释器名为 wish(“窗口 shell”)。假设您的系统上已安装 Tcl/Tk 并且 X 正在运行,您应该能够执行 wish。您将看到一个空白的矩形窗口和一个 wish 提示符。

您可以在此提示符下键入 Tcl/Tk 命令,结果将显示在 wish 窗口中。例如,如果您键入:button .b -text “Hello, world!” -command { exit } pack .b

wish 窗口应缩小为一个包含字符串“Hello, world!”的按钮。(请参阅图 1,下一页)按下此按钮将导致 wish 进程退出。

我们刚刚做了什么?每个 Tcl 命令都以命令名称开头,后跟任何参数。button 是一个 Tk 命令,用于创建按钮。在本例中,我们希望按钮包含文本“Hello, world!”,并且我们希望在按下按钮时执行 Tcl exit 命令。button 的第一个参数“.b”是我们希望赋予按钮小部件的名称。

X Window System Programming with Tcl and Tk

图 1

(小部件只是一个图形对象,例如按钮、滚动条等,它具有某些视觉和功能属性。正如我们将看到的,Tk 支持多种类型的小部件。)稍后我们可以使用此名称引用该按钮。

pack 命令是一个几何管理器;它控制小部件在 wish 窗口中的放置方式。pack 是一个简单的几何管理器,它将小部件一个接一个地“打包”(因此得名)在一起。在通过 pack 给出位置之前,小部件不会变为可见。

在本例中,我们希望将按钮小部件打包到 wish 窗口中。pack 可以接受多个参数来指定小部件相对于其他小部件的位置。但是,这里我们只有一个小部件,因此默认行为是可以接受的。

您可以编写脚本并通过 wish 执行,而不是向 wish 进程键入命令。这是一个简单的程序,它将提示您输入文件名,然后启动一个 xterm 运行 vi 来编辑该文件。

#!/usr/local/bin/wish -f
label .1 -text "Filename:"
entry .e -relief sunken -width 30 -textvariable\
  fname
pack .1 -side left
pack .e -side left -padx
lm -pady lm
bind .e <Return> {
exec xterm -e vi $fname

如果您将以上内容保存在名为 edit.tcl 的文件中,然后运行它,您应该会看到如图 2 所示的窗口。(当然,这假设您的 /usr/local/bin 中安装了 wish。如果不是,请编辑脚本第一行上的路径名。)与 shell 脚本一样,您需要使文件可执行才能运行它。

让我们逐步了解这个脚本。第一行是一个 label 命令,它(正如您可能猜到的)创建一个标签小部件。标签仅包含静态文本字符串。我们将此小部件命名为 .1(稍后会详细介绍小部件命名约定),并赋予其文本值“Filename:”。

第二行创建一个名为 .e 的 entry 小部件。entry 小部件类似于标签,只不过它允许用户编辑文本。-relief sunken 选项指示小部件应显示为好像在窗口中“凹陷”,如图 2 所示。-width 选项设置 entry 小部件的宽度(以字符为单位),-textvariable 选项指示 entry 文本的值应存储在变量 fname 中。

接下来的两行将标签小部件,然后是 entry 小部件,打包到 wish 窗口中。在这两种情况下,我们都指定 -side left,这表示小部件应打包到窗口的左侧,一个接一个。在打包 entry 小部件时,我们使用 -padx 和 -pady 选项在小部件的侧面留出一点“填充 H”(此处为 1 毫米)。Tcl pack 手册页详细描述了这些选项。

我们脚本的最后三行使用 bind 命令为我们的 entry 小部件创建一个事件绑定。绑定允许您在小部件中发生特定事件时执行一系列命令;例如,鼠标按钮单击或按键。在本例中,我们希望在 entry 小部件中按下 RETURN 键时执行命令。

exec xterm -e vi $fname

将启动一个 xterm,在该终端上运行 vi 编辑用户输入的文件名。请注意 fname 变量的使用,

X Window System Programming with Tcl and Tk

图 2

我们在脚本的第二行中将其与 entry 小部件关联。变量名称以美元符号为前缀,以引用 fname 本身的值。这类似于 shell 脚本中使用的变量语法。

使用大括号

关于 Tcl 语法的一个简短说明:大括号 ( { ... } ) 用于将一组 Tcl 命令组合在一起作为“子脚本”。大括号中包含的命令将传递给 Td 解释器,而无需执行变量替换。这是一个需要理解的重要概念。如果没有大括号,Tcl 解释器会尝试在解释脚本中的 bind 命令时替换变量 fname 的值。也就是说,当首次执行 bind 命令时,变量 fname 没有值。如果没有大括号,Tcl 解释器会抱怨 fname 是一个未知变量。但是,使用大括号,我们将 $fname 的解释延迟到实际执行事件绑定时;在本例中,即在 entry 小部件中按下 Return 键时。

Tk 是 Tcl 的一组扩展,它实现了命令...使您能够将 X 应用程序编写为简单的 Tcl 脚本。

请注意,Tcl 在换行符方面有几个奇怪的规则。Tcl 期望每个命令都由一行组成;行尾表示命令的结尾,除非该行以反斜杠结尾,这与 shell 脚本中相同。但是,如果一行以左大括号结尾,则 Tcl 理解您正在开始一个子脚本,该子脚本将包含在大括号中,并继续读取脚本直到右大括号。因此,您不能说

bind .e <Return>
{
exec xterm -e vi $fname
{

Tcl 会认为 bind 命令在第一行之后结束,并抱怨它需要一个脚本来执行事件绑定。因此,当使用大括号封装子脚本时,请确保左大括号位于开始脚本的行尾。

命名小部件

在 Tk 中,小部件以分层方式命名。最顶层的“shell H”小部件(即主 wish 窗口)名为“.”(点)。作为 . 的直接子级的​​所有小部件都以 . 开头命名,例如 .b,.entry,.leftscroll 等等。小部件名称可以是任何以点开头的字母数字字符串;您在使用 button 和 label 等命令创建小部件时选择小部件名称。进一步的子小部件的名称类似于 .foo.bar.bar,其中每个级别都用点分隔。

创建菜单栏列表

例如,您可能有一个名为 .mbar 的菜单栏小部件。当然,它是主窗口 . 的子级。菜单栏中包含的菜单按钮可能命名为 .mbar.file,.mbar.options, 等等。也就是说,菜单栏是主应用程序窗口的子级,而各个菜单按钮是菜单栏的子级。将小部件安排到层次结构中允许您出于逻辑和视觉目的将它们分组在一起。稍后我将更详细地介绍这一点。

一个实际的应用程序

为了演示 Tcl/Tk 的强大功能,我将展示一个完全以 Tcl/Tk 脚本编写的实际应用程序。在介绍的过程中,我将描述所使用的语法和可用的功能。您可以使用 Tcl/Tk 手册页来填补空白。

该程序是一个简单的绘图应用程序,利用了 Tk canvas 小部件。canvas 是一个简单的图形显示小部件,它将显示各种类型的对象:矩形、线条、文本、椭圆等等。我们要做的是将 canvas 小部件与 Tk 的用户界面功能结合起来,以允许用户使用鼠标绘制对象。

X Window System Programming with Tcl and Tk

图 3

图 3 演示了我们的应用程序在使用时的外观。“颜色”菜单已拉下,以便您可以看到各种可用的选择。

整个脚本 draw.tcl 在此处给出。请注意,它不到 200 行。对于这样一个涉及菜单、颜色、鼠标输入等的复杂 X 应用程序来说,这非常短。如果您不想输入整个脚本,可以从 sunsite.unc.edu 通过 ftp 获取代码,目录为 /pub/Linux/docs/LJ

创建菜单栏

乍一看,这个脚本可能看起来很复杂。有一些粗糙的地方,但这里介绍的总体概念非常简单。让我们仔细看看这个程序,但让我们从脚本的中间附近开始,我们将在那里创建 frame 小部件

frame .mbar -relief groove -bd
pack .mbar -side top -expand yes -fill x

frame 命令创建一个 frame 小部件,用于将小部件分组在一起。frame 本身通常是不可见的,除非您指定应在其周围绘制边框。

在这里,我们创建一个名为 .mbar 的 frame,并指定它应使用 groove relief 类型。小部件的“relief”指示应在小部件周围显示哪种 3D 边框。(所有小部件类型都支持许多选项,例如 -reliefbd-foreground-background。)-relief 的有效类型为

  • raised:使小部件看起来在显示器上凸起。

  • sunken:使小部件看起来沉入显示器中。

  • ridge:在小部件边框周围绘制凸起的脊。

  • groove:在小部件边框周围绘制凹陷的凹槽。

  • flat:无 relief;看起来像平面的。

-ted 选项指定用于小部件的边框宽度(在本例中为凹槽的宽度)。在这里,我们将边框宽度设置为 3 像素。

接下来,我们将 .mbar 打包到 wish 窗口中。(默认情况下,小部件被打包到它们的直接父级中。在本例中,.mbar 的父级是顶层窗口 .)。pack 的 -side 参数指示我们应将 .mbar 打包到父级的哪一侧。-expand yes 选项指示应为小部件提供其周围的所有额外空间。因为我们将小部件打包到窗口的顶部边缘,所以 -expand 选项为小部件提供了其左侧和右侧的任何额外水平空间。-fill x 命令使小部件增长直到它填充此空间。在不使用 -fill 的情况下使用 -expand yes 会为小部件提供额外的水平空间,但小部件不会增长到填充该空间。(如果您对它的工作原理感兴趣,请以各种形式尝试 pack 命令。另请参阅 pack 手册页或 Ousterhout 的书以获取更多详细信息。)

创建菜单

创建并打包菜单栏后,我们使用 menubutton 命令创建三个菜单项

menubutton .mbar.file -text "File" -underline 0 \
        -menu .mbar.file.menu
menubutton .mbar.obj -text "Object" -underline 0 \
        -menu .mbar.obj.menu
menubutton .mbar.color -text "Color" -underline 0 \
        -menu .mbar.color.menu
pack .mbar.file .mbar.obj .mbar.color -side left

menubutton 小部件只是一个按钮,按下该按钮时,会在其下方显示一个菜单。首先,请注意菜单按钮是 .mbar 小部件的子小部件。-text 选项指定要在 menubutton 中显示的文本,-underline 选项指定 menubutton 文本中要为键盘快捷键加下划线的字母的索引。在每种情况下,我们都为字符串的第一个字母加下划线(参见:图 3)。

-menu 选项指定应在按下按钮时显示的菜单小部件的名称。我们尚未创建这些小部件(.mbar.file.menu 等),但会在此之后立即创建。

填充菜单

首先,我们创建“文件”菜单

menu .mbar.file.menu
        .mbar.file.menu add command -label \
                "Save PostScript..." -command { get_ps
        .mbar.file.menu add command -label "Quit"
                -command { exit }

menu 命令创建一个具有给定名称的菜单小部件。(请注意,菜单 .mbar.file.menu 是 menubutton .mbar.file 的子小部件)。然后,我们使用 add menu 小部件命令向该小部件添加菜单项。

请注意,小部件名称本身就是命令。通常,如果我们想对特定小部件执行函数,我们会将其作为命令调用并使用各种小部件子命令。例如,要修改小部件的外观,我们可以使用小部件子命令 configure

<widget name> configure [ <options> ... ]

例如,

.mbar configure -background blue

会将 .mbar 小部件背景颜色更改为蓝色。Tcl/Tk 附带的小部件手册页描述了每种小部件类型可用的子命令。

在上面的示例中,我们使用 menu 子命令 add 向菜单添加条目。语法为

<widget name> add <entry type> [ <options> ... ]

其中 <entry type> 是以下之一

command-:类似于 button 小部件,在选择时调用 Tcl 命令。radictutton- 一组 radicLutton 条目控制命名变量的值。组中的一个单选按钮(即,只有一个与给定变量关联的单选按钮)可以在任何给定时间激活。

checkbutton:类似于 radicLutton。将变量的值切换为 0(关闭)或 1(打开)。但是,与单选按钮不同,复选按钮彼此之间不是互斥的。

cascade:允许您在当前菜单中“级联”子菜单。

separator:一个非功能性菜单分隔符,用于在视觉上分隔菜单项。

menu 手册页更详细地描述了这些内容。“文件”菜单中,我们创建了两个命令条目。选择每个条目时,都会执行 -command 参数给出的命令。正如您可能猜到的,-label 参数为菜单项分配一个文本标签。

“对象”菜单在性质上与“文件”菜单类似,只不过它使用 radiobutton 条目。这些条目链接到变量 object_type。当我们从菜单中选择“椭圆”选项时,object_type 设置为 oval。同样,当选择“矩形”时,变量设置为 rect。因为这些是单选按钮,所以一次只能选择一个。稍后,我们将了解 object_type 变量如何影响 canvas 小部件内的对象绘制。

“颜色”菜单类似于“对象”菜单:我们有四个链接到变量 thecolor 的单选按钮条目。我们将 -background 选项与这些条目一起使用,以直观地描绘所选颜色。

创建菜单后,我们使用 tk_menubar 命令告诉 Tk 这是我们应用程序的主菜单栏。这启用了菜单栏的键盘快捷键。如果您按下 Alt 键以及菜单标题中带下划线的字母之一,则该菜单将被激活。例如,按 Alt-F 将激活“文件”菜单。menubutton 命令的 -underline 参数控制哪个字母激活哪个菜单。按 F10 激活最左侧的菜单,您可以使用箭头键四处移动。

focus 命令用于使应用程序窗口中的所有键盘事件都由菜单栏接收。否则,鼠标指针必须位于菜单栏内才能接收键盘快捷键事件。这是关于 X 编程的另一个细微之处,您此时不应担心。

定义过程

“文件”菜单上的第一个菜单项“保存 PostScript...”执行命令 get_ps,这是一个我们在脚本前面定义的过程。proc 命令用于定义过程;语法为

proc <procedure name> <arguments> <body>

其中 <procedure name> 当然是要定义的过程的名称,<arguments> 是过程参数的括号列表,<body> 是调用过程时要执行的脚本。

看一下 get_ps 过程。它使用 canvas 小部件 postscript 子命令,该子命令将 canvas 小部件的 PostScript 图像保存到文件中。这是一个非常方便的功能,我们可以使用它来“保存”我们的绘图(可能用于打印或使用 Ghostview 查看)。

创建对话框

get_ps 命令显示一个对话框,要求输入要保存的文件名,以及两个按钮:“确定”和“取消”(参见图 4,第 28 页)。

X Window System Programming with Tcl and Tk

图 4

对话框(这是一个单独的窗口)实际上是一个 toplevel 小部件。我们使用 -class 选项来 toplevel 以更改窗口类;这与新窗口的 X 资源数据库设置有关(这里不是重要的细节)。我们将新的 toplevel 小部件命名为 .ask,并使用 wm(窗口管理器)命令设置窗口的标题。

在 toplevel 小部件中,我们创建两个 frame,.ask.top.ask.bottom。这些将用于将小部件分组在一起。我们希望在顶部 frame 中有一个标签和一个文本输入小部件,在底部 frame 中有两个按钮。(这仅用于视觉效果:使用 frame 是在使用 pack 时将小部件分组在一起的非常好的方法)。因此,我们使用 frame 命令创建 frame 并将它们打包到 toplevel 任务中。这里没有什么新鲜事。

在顶部 frame 中,我们创建一个标签和一个 entry。这等效于我们的第一个示例脚本 edit.tcl。请注意,标签 (.ask.top.l) 和 entry (.ask.top.e) 是 frame 小部件的子级,而 frame 小部件又是 .ask 的子级。此外,我们将 entry 小部件中的 Return 键绑定到执行 canvas 小部件的 postscript 子命令 .c(我们稍后将在脚本中创建)并销毁 .ask 小部件。这具有“弹出”对话框的效果。

在较低的 frame 中,我们创建两个 button 小部件并将类似的命令绑定到它们。这应该是自明的。最后,我们使用 grab 命令。这会将鼠标和键盘事件限制在对话框窗口中。否则,当“保存 PostScnpt”对话框处于活动状态时,您将能够在主应用程序窗口中继续绘图;我们当然不希望那样。

Canvas 小部件和事件绑定

处理完菜单后,我们准备处理 canvas 小部件,它将用于绘图。首先,我们创建小部件并将其打包到应用程序窗口中。接下来,我们在 canvas 中创建两个事件绑定:一个用于 <ButtonPress-1>(在按下鼠标按钮 1 时执行),另一个用于 <B1-Motion>(在按下鼠标按钮 1 时移动鼠标时执行)。

与 bind 一起使用的事件名称是标准的 X11 事件说明符。这些在任何关于 X11 编程的书籍(以及好的 X 用户指南)中都有描述。这里要枚举的 X 事件类型太多了;有关事件名称列表,请参阅头文件 /usr/include/X11/X.h

当在 canvas 小部件中按下按钮 1 时,我们希望开始绘制由 object_type 变量指定的类型对象,颜色为 thecolor。首先,我们将全局变量 orig_xorig_y 设置为鼠标单击的原始位置;这定义了要绘制对象的左上角。正如注释所说,伪变量 sxsy 引用事件的 %x%y 坐标。

接下来,我们使用 canvas create 子命令创建对象。语法为

<canvas name> create `(type> <xl> <yl> <x2>
<y2&gt: \
[ <options> ... ]

这将创建一个类型为 <type> 的对象,左上角位于 <xl> ,<yl>,右下角位于 <x2>, <y27gt;。有效的对象类型为 arcbitmaplineovalpolygonrectangletextwindow。Tk canvas 手册页描述了所有这些类型。

canvas create 子命令返回刚创建对象的唯一标识符(只是一个整数)。方括号 ( [ . . . ]) 中包含的 Tcl/Tk 命令用于运行子脚本,其返回值将替换到其位置。我们将 create 子命令的返回值分配给变量 theitem。稍后,我们将使用此值在拖动鼠标时调整对象大小。

<B1-Motion> 的绑定非常相似。首先,我们删除变量 theitem 给出的标识符的项目,然后使用由鼠标当前位置定义的新的右下角重新创建该项目。原始左上角

已保存在变量 orig_xorig_y 中,我们在此处重新使用它们。我们将新的对象标识符保存回 theitem 变量中。我们基本上所做的是删除了当前对象,并根据拖动鼠标期间的鼠标位置以新尺寸重新创建了它。这样做的视觉效果是在我们拖动鼠标时调整项目的大小。

我们脚本的最后几行调用“对象”和“颜色”菜单中的第一个条目。这启用了椭圆对象类型和红色颜色,就像我们使用鼠标选择了这些菜单项一样。如果我们不这样做,则在应用程序启动时将不会选择任何对象类型或颜色。当然,我们可以手动设置变量 object_type 和 thecolor;但是,菜单中的单选按钮条目不会突出显示以与这些变量设置相对应。使用菜单项 invoke 子命令可以一次解决这两个问题。

您已经拥有了!一个完整的 X 绘图应用程序,包含颜色、菜单和 PostScript 功能,全部都在几百行解释型 Tcl/Tk 脚本中。

结合手册页和本文中的信息,您应该可以自行探索 Tcl 和 Tk 了。

如您所见,Tcl/Tk 编程很容易;它是编写简单 X 应用程序或为您喜欢的实用程序添加 X 前端的理想方式。还有一个完整的文本编辑小部件,它将允许您与其他基于文本的应用程序进行交互。并且 Tcl/Tk 非常可定制;从键盘和鼠标小部件绑定到字体和突出显示颜色,一切都可以修改。

但是,以 Tcl/Tk 脚本编写整个应用程序可能不适合您的需求。在下个月的文章中,我将介绍如何使用 Tcl/Tk 解释器 wish 作为来自 C 或 Perl 程序的 X 界面请求的“服务器”。(您甚至可以使用来自 C 的较低级别的 Xlib 函数调用直接绘制到 Tk 窗口。)这将允许您编写复杂的基于 X 的程序,而无需涉足 Xt Intrinsics 或 Motif。

祝您编程愉快。

Matt Welsh (mdw@sunsite.unc.edu) 是一位作家和代码苦工,与 Linux 文档项目和 Debian 开发团队合作。作者欢迎提出问题和意见。

加载 Disqus 评论