PracTCL 编程技巧

作者 Stephen Uhler

对于 Tcl 语言的新手来说,这个专栏的名称可能会让人困惑:Tcl 的发音为 “tickle”。本专栏主要面向那些已经了解 Tcl 和 Tk 并希望学习如何提高编程技能的人。对于想要了解 Tcl 和 Tk 的读者,John K. Ousterhout 的著作 Tcl 和 Tk 工具包 已经做了充分的介绍,并且 Linux Journal 也在 1994 年 12 月刊上发表了一篇介绍性文章。

用户界面在 Tk 中通过两个步骤创建。首先,用户界面元素(称为控件),例如按钮或滚动条,使用相应的控件命令创建。接下来,它们通过几何管理器在显示器上排列。

Tk 提供了几种不同的几何管理器供选择。“packer” 和 “placer” 是通用的几何管理器,而 “text”(在 tk4.0 中)和 “canvas” 控件也可以充当几何管理器,通过在自身内部定位其他控件。其他几何管理器在许多扩展包中也可用,例如 George Howlett 的 BLT 扩展中的 “blt_table”。

传统上,“packer” 被认为是 Tk 应用程序中的主要几何管理器,因为它支持强大的基于约束的布局,而 “placer” 则保留给尚未掌握 packer 复杂性的初学者。John Ousterhout 在 Tcl 和 Tk 工具包 中花费了 15 页描述 “packer”,但只用了一个段落介绍 “place”,暗示 “placer 仅用于少数特殊目的”。事实上,“placer” 对于高级用户来说是一个必不可少的工具,因为它提供了对控件定位的精确控制。我将演示 “placer” 的两个示例用法,以揭示其真正的威力。

如何在 Tk 中成为窗格

对于第一个示例,我们将完全使用 TCL 编写一个特殊的几何管理器,使用 “placer” 来构建一个类似 Motif 的 “pane” 控件。“pane” 控件将窗口分为两个半区或窗格,并提供一个句柄,用户可以使用该句柄动态更改每个半区的相对大小。

frame .top
frame .bottom
frame .handle -bd 2 -relief raised -bg red \
    -cursor sb_v_double_arrow

我们将从创建两个框架 .top.bottom,以及一个调整大小的 .handle 来更改 .top.bottom 的相对大小开始。在本例中,我们将把我们的 “pane” 控件放在顶层 . 中,但它几乎可以在任何地方使用。

place .top -relwidth 1 -rely 0 -height -1 \
    -anchor nw
place .bottom -relwidth 1 -rely 1 -height -1 \
    -anchor sw
place .handle -relx 0.9 -width 10 -height 10 \
    -anchor e
. configure -bg black

这 3 个控件通过 “placer” 分两步排列。首先,我们指定 place 的选项,这些选项不会更改。.top.bottom 都将跨越窗口的整个宽度 (-relwidth 1),其中 .top 锚定到顶部 (-rely 0 -anchor nw),.bottom 锚定到底部 (-rely 1 -anchor sw)。选项 -height -1(Tk4.0 的新功能)将 .top.bottom 的高度减少 1 像素,这会在窗口之间留下一个“间隙”,因此根窗口将显示为 2 个窗格之间的黑色 (.<\!s>configure -bg black) 线条。最后,我们将把 .handle 放置在靠近右边缘的位置。

bind . <Configure> {
    set H [winfo height .].0
    set Y0 [winfo rooty .]
}

为了计算 .top.bottom 的相对位置,我们需要知道根窗口的位置 (Y0) 和大小 (H),我们将通过将计算绑定到 <Configure> 事件来在任何可能更改时计算它们。由于高度 (H) 将用作浮点数,我们将在其后添加 .0

bind .handle <B1-Motion> {
    adjust [expr (%Y-$Y0)/$H]
}

当用户通过用鼠标拖动句柄来移动它时,我们将计算鼠标在根窗口中向下移动的距离的比例,并调用 adjust 来相应地移动窗口。我们需要使用 %Y,即鼠标在“根”坐标中的位置,因为 %y 是相对于 handle 而不是根窗口 . 的。

proc adjust {fract} {
    place .top -relheight $fract
    place .handle -rely $fract
    place .bottom -relheight [expr 1.0 - $fract]
}

过程 adjust 接受一个介于 0 和 1 之间的分数,更改 topbottom 窗口的高度,并更新句柄的位置。只需要更新可能已更改的 place 选项。这就是全部。

proc stuff {root file} {
    text $root.text -yscrollcommand \
        "$root.scroll set"
    scrollbar $root.scroll -command \
        "$root.text yview"
    pack $root.scroll -side right -fill y
    pack $root.text  -fill both -expand 1
    $root.text insert 0.0 [exec cat $file]
}

为了测试它,我们需要在顶部和底部半区中放入一些东西。我们将创建一个过程 stuff,它在带有滚动条的文本控件中显示文件的内容。

adjust .5
stuff .bottom $env(HOME)/.login
stuff .top $env(HOME)/.cshrc

现在填充每个窗格,adjust 这两个半区,然后就可以开始了。

真能拖拽

对于我们的第二个示例,我们将使用 placer 允许用户在窗口中交互式地“拖放”控件。当用户通过用鼠标单击来选择控件时,我们将把它抬起,使其悬停在窗口上方,并在我们拖动它时投下阴影。释放鼠标会将控件放在其新位置。

label .label -text "drag me"\
  -borderwidth 3 -relief raised
frame .shadow -bg black
lower .shadow .label
place .label -x 50 -y 50
set hover 5

我们将从创建一个要拖动的控件 .label 及其阴影 .shadow 开始。我们将使用 lower 来确保阴影始终“低于”控件,并且我们将从任意位置开始 .label,即距 . 左上角 50 像素。变量 hover 控制着我们在拖动控件时将其抬高到窗口上方的高度。

bind .label <1> {
    array set data [place info .label]
    place .label -x [expr $data(-x) - $hover]\
                 -y [expr $data(-y) - $hover]
    place .shadow -in .label -x $hover -y $hover \
                  -relx 0 -rely 0 -relwidth 1 \
                  -relheight 1 -width -2 -height -2 \
                  -bordermode outside
    set Rootx [expr %X - [winfo x %W]]
    set Rooty [expr %Y - [winfo y %W]]
}

当我们第一次单击 .label 时,我们需要将其抬起,添加其阴影,并计算其相对于根窗口的位置,以便我们可以弄清楚如何移动它。array set 命令(tcl7.4 中的新命令)接受名称-值对并从中创建一个数组。幸运的是,place info 命令恰好以名称-值对的形式报告当前的 place 选项,从而允许我们使用数组访问来访问和修改单个 place 选项。第一个 place 命令只是在我们第一次按下鼠标时将控件向上和向左移动 $hover 像素。我认为第二个 place 命令(它定位阴影)使用了所有可用的 place 选项!

-in 选项,更准确地描述为“相对于”,使 .shadow 中指定的所有位置都相对于 .label,而不是 .,后者是默认值。-x-y 选项,当添加到 -relx-rely 时,将阴影定位在我们 $hover 它之前 .label 的位置。-relwidth-relheight 选项使 .shadow.label 的大小相同,然后 -width-height 选项使阴影稍微小一些,这样它看起来会更远。最后,-bordermode 选项指示 placer 在计算 -relwidth-relheight 的大小时包括 .label 的边框。

最后,我们计算鼠标光标的位置(以像素为单位),相对于根窗口 (Rootx, Rooty) 的左上角,这样就可以更容易地弄清楚如何用鼠标跟踪 .label

bind .label <B1-Motion> {
    place .label -x [expr %X - $Rootx] \
                 -y [expr %Y - $Rooty]
}

当鼠标移动时,我们重新定位控件以跟随。因为我们“放置”了相对于控件的阴影(使用 -in 选项),所以它会自行跟随。

bind .label <ButtonRelease-1> {
    array set data [place info .label]
    place .label -x [expr $data(-x) + $hover] \
                 -y [expr $data(-y) + $hover]
    place forget .shadow
}

当我们释放鼠标按钮时,与之前相同的 array set 技巧用于将控件“放回”窗口,然后移除阴影。

正如您希望从这两个简单的示例中看到的那样,“placer” 可以成为在 Tk 中动态放置控件的强大工具。

Stephen Uhler 是 Sun Microsystems 实验室的研究员,他在那里与 John Ousterhout 一起改进 Tcl 和 Tk。Stephen 是 MGR 窗口系统和 PhoneStation(一个基于 TCL 的个人电话环境)的作者。您可以通过电子邮件 Stephen.Uhler@Eng.Sun.COM 与他联系。

加载 Disqus 评论