内容


在 Perl/Tk 中使用高级窗口小部件

在 Perl 中创建丰富的用户界面

与 Perl 语言的广泛应用形成对比的是,Perl 的 GUI 工具包 Perl/Tk 的普及性差得多。 这很奇怪,因为可以证明,它是最简单的 GUI 编程工具包之一(至少在 UNIX 平台上),因此间接表明它本身 也适用于用户界面原型设计以及为含义模糊的命令行工具提供用户友好的包装器(wrapper)。

人们对 Perl/Tk 相对不太关心的原因之一可能是,感觉上它并不是特别强大,也无助于复杂的应用程序。 不过,在 CPAN 上有很多作为用户贡献的窗口小部件,提供了更为复杂的功能。在本文中,我将研究其中 的一些,并展示如何使用它们来建立更丰富的用户体验。我还将指出关于使用 Perl/Tk 进行编程的一些更 普遍可用的技术。

(当然,Perl/Tk 存在另一个问题,它的窗口小部件 外表 难看而且不能“设置主题”。不幸的是,我对 此无能为力。)

先决条件和可用性

始终都需要具备的条件包括,关于 Perl 的丰富知识,至少对 Perl/Tk 有初步体验,以及通用的 GUI 编程的 概念 —— 事件、窗口小部件、回调、表面形状管理器。您可以在本文的 参考资料 部分找 到关于 Perl/Tk 的介绍性参考资料。

您可以免费从 CPAN(Comprehensive Perl Archive Network)下载本文中用到的所有窗口小部件。其中大部分都具有 比这里所描述的多得多的选项:每个窗口小部件的 perldoc 以及在某些情况下的源代码,并提供了最终的完整 参考资料。

带标签的帧:Tk::NoteBook

Tab 是一个常见的 GUI 窗口小部件,它帮助将大量的选项分组为较小的子集,并向复杂的对话栏(举例来说)添加 结构。

Tk::NoteBook 是 Perl/Tk 的一个具有类 Tab 外观和语义的窗口小部件。图 1 中 展示了一个使用了三个标签的例子。注意,第三个标签被禁用了。

图 1. NoteBook 窗口小部件
图 1. NoteBook 窗口小部件
图 1. NoteBook 窗口小部件

这个例子由下面的代码生成:

清单 1: 使用 NoteBook 窗口小部件
use Tk;
use Tk::NoteBook;
$mw = MainWindow->new();
$mw->geometry( "400x100" );
$book = $mw->NoteBook()->pack( -fill=>'both', -expand=>1 );
$tab1 = $book->add( "Sheet 1", -label=>"Start", -createcmd=>\&getStartTime );
$tab2 = $book->add( "Sheet 2", -label=>"Continue", -raisecmd=>\&getCurrentTime );
$tab3 = $book->add( "Sheet 3", -label=>"End", -state=>'disabled' );
$tab1->Label( -textvariable=>\$starttime )->pack( expand=>1 );
$tab2->Label( -textvariable=>\$raisetime )->pack( expand=>1 );
$tab3->Button( -text=>'Quit', -command=>sub{ exit; } )->pack( expand=>1 );
MainLoop;
sub getStartTime {
  $starttime = "Started at " . localtime;
}
sub getCurrentTime {
  $raisetime = " Last raised at " . localtime;
  $book->pageconfigure( "Sheet 3", -state=>'normal' );
}

在任何 Perl/Tk 应用程序中都是一样,我们首先指定我们将要使用的模块,然后创建一个 MainWindow 。注意, Tk::NoteBook 以及 CPAN 上贡献的其他窗口小部件并不是标准 Tk 贡献的一部分,所以需要显式地指定。

我们创建一个 NoteBook 窗口小部件作为 MainWindow 的子窗口,然后添加三个标签。 add() 函数的第一个参数是一个符号名,可以在 notebook 中通过它引用生成的页:下面我们将使用它。

我将给出两条关于使用 NoteBook 窗口小部件时表面形状管理的注释。 首先,尽管标签本身是窗口小部件,但它们不需要 被包装 。它们的 表面形状管理是由封装它们的 NoteBook 来处理的。其次,如果 NoteBook 窗口小部件的父窗口的大小是可变的,那么,在 包装NoteBook 时,要 同时 指定 -fill-expand 属性,这很重要。后者确保 NoteBook 的分配矩形区将始终填充可用区域,而前者确保实际的 NoteBook 窗口小部件将扩展并填充它的分配矩形区。

标签接受多种属性。我们在这里作为例子来说明的是 -createcmd-raisecmd-state 。前两个可以 用来注册回调,分别在标签第一次被创建及重新激活时被调用;最后一个属性可以取的值为 normaldisabled 。我们还在第 二个标签上使用了 -raisecmd 回调,目的是当第二个标签第一次 被重新激活时将第三个标签的状态由禁用切换为活动。我们在封装标签的 NoteBook 窗口小部件中使用 pageconfigure() 函数来完成这一任务,传入 的第一个参数为被引用的标签的符号名。

多用途图形显示:Tk::ProgressBar

Tk::ProgressBar 是一个显示数据值的图形化描述的窗口小部件。 进度条通常是在下载大文件或执行类似的长时间运行的任务时为用户提供反馈。Perl/Tk 中相应 的窗口小部件具备一些特殊的特性,使得它在其他用途方面也具有吸引力。

图 2. ProgressBar 窗口小部件
图 2. ProgressBar 窗口小部件

这个例子包括两个 ProgressBar 窗口小部件,它们 连结到一个标准的 Scale 。滑块的移动调用名为 fct 的回调来改变显示的颜色条的长度,那个回调会调用 ProgressBarvalue 函数来设置新的长度。值为 100 对应的是可见的颜色条 的全长。可以分别使用 -to-from属性来改变这个值以及对应于零长度颜色条的值。

清单 2举例说明了定制颜色条外观的一些方法。底部的 ProgressBar 分为十块,每两块之间有一个象素的间隔。这些是 默认的值;可以通过显式地指定 blocksgap 属性来进行设置。通常需要对 -padx-pady-length-borderwidth 进行一些试验。

-colors 属性接受对包含有位置-颜色对的一个数组的引用。(注意, 位置必须以升序排序!)最后指定的那个颜色将用来显示颜色条,直到到达颜色将发生改变的下一个位置点。 所以,定义 @colors = ( 0, "red", 50, "green" ); 会生成一个左边为红色 右边为绿色的颜色条。在这里,我给出了 100 个离散的值,为顶部的 ProgressBar 生成了彩虹的颜色。注意,当窗口大小发生变化时,顶部的 ProgressBar 会改变它的长度。

清单 2: 使用 ProgressBar 窗口小部件
use Tk;
use Tk::ProgressBar;
@colors = (  0, '#ff002a',  1, '#ff0014',  2, '#ff000a',  3, '#ff0500',  4, '#ff1000',
	     5, '#ff1b00',  6, '#ff3000',  7, '#ff3b00',  8, '#ff4600',  9, '#ff5100',
	    10, '#ff6100', 11, '#ff7600', 12, '#ff8100', 13, '#ff8c00', 14, '#ff9700',
	    15, '#ffa100', 16, '#ffbc00', 17, '#ffc700', 18, '#ffd200', 19, '#ffdd00',
	    20, '#ffe700', 21, '#fffd00', 22, '#f0ff00', 23, '#e5ff00', 24, '#dbff00',
	    25, '#d0ff00', 26, '#baff00', 27, '#afff00', 28, '#9fff00', 29, '#95ff00',
	    30, '#8aff00', 31, '#74ff00', 32, '#6aff00', 33, '#5fff00', 34, '#54ff00',
	    35, '#44ff00', 36, '#2eff00', 37, '#24ff00', 38, '#19ff00', 39, '#0eff00',
	    40, '#03ff00', 41, '#00ff17', 42, '#00ff21', 43, '#00ff2c', 44, '#00ff37',
	    45, '#00ff42', 46, '#00ff57', 47, '#00ff67', 48, '#00ff72', 49, '#00ff7d',
	    50, '#00ff87', 51, '#00ff9d', 52, '#00ffa8', 53, '#00ffb8', 54, '#00ffc3',
	    55, '#00ffcd', 56, '#00ffe3', 57, '#00ffee', 58, '#00fff8', 59, '#00faff',
	    60, '#00eaff', 61, '#00d4ff', 62, '#00c9ff', 63, '#00bfff', 64, '#00b4ff',
	    65, '#00a9ff', 66, '#008eff', 67, '#0083ff', 68, '#0079ff', 69, '#006eff',
	    70, '#0063ff', 71, '#004eff', 72, '#003eff', 73, '#0033ff', 74, '#0028ff',
	    75, '#001dff', 76, '#0008ff', 77, '#0200ff', 78, '#1200ff', 79, '#1d00ff',
	    80, '#2800ff', 81, '#3d00ff', 82, '#4800ff', 83, '#5300ff', 84, '#5d00ff',
	    85, '#6e00ff', 86, '#8300ff', 87, '#8e00ff', 88, '#9900ff', 89, '#a300ff',
	    90, '#ae00ff', 91, '#c900ff', 92, '#d400ff', 93, '#df00ff', 94, '#e900ff',
	    95, '#f400ff', 96, '#ff00f3', 97, '#ff00e3', 98, '#ff00d9', 99, '#ff00ce' );
$mw = MainWindow->new();
$mw->geometry( '250x150' );
$mw->resizable( 1, 0 );
$bar1 = $mw->ProgressBar( -borderwidth=>2, -blocks=>100, -gap=>0,
			  -troughcolor=>'white',-colors=>\@colors,
			  -length=>106 )->pack( -padx=>5, -pady=>5, -fill=>'x' );
$slide = $mw->Scale( -orient=>'horizontal', -length=>150,
		     -showvalue=>0, -tickinterval=>20 )->pack;
$bar2 = $mw->ProgressBar( -padx=>2, -pady=>2, -borderwidth=>2,
			  -troughcolor=>'#BFEFFF', -colors=>[ 0, '#104E8B' ],
			  -length=>106 )->pack;
$mw->Button( -text=>'Quit', -command=>sub{ exit } )->pack( -padx=>15, -pady=>15,
							   -anchor=>'se' );
$slide->configure( -command=>[ \&fct, $slide, $bar1, $bar2 ] );
MainLoop;
sub fct {
  my ( $slide, $bar1, $bar2 ) = @_;
  my $val = $slide->get();
  $bar1->value( 100 - $val );
  $bar2->value( $val );
}

我们没有在它的构造函数中指定 Scale 窗口小部件的回调,而是稍后使用 configure 来指定。原因在于,我们需要以对两个 ProgressBar 的 引用作为回调的参数,而当 Scale 构造函数被调用时,它们还没有被定义。 不可能改变窗口小部件构造函数的调用次序,因为这将改变窗口小部件包装的次序。

此代码还举例说明了指定回调的参数的一种方法:作为一个匿名列表,具有到作为第一个 参数的回调的引用,随后是所需的参数。如果回调不需要参数,则我们不需要创建那个匿名列表,因为 Perl 按单元素数组来 处理列表上下文中计算得到的数量值。(要获得关于 Perl 中的列表的更多信息,请参阅 参考资料 中列出的 Learning Perl, 2nd edition。)下面我们将用到向回调发送参数的一种不同的方法;查看 侧栏中关于两种方法之间区别的讨论。

可以为颜色条中任何一段自由地选择任意的颜色值,这就使得 ProgressBar 可以 有趣地用作常规的显示窗口小部件:例如,可以通过颜色来指明显示的值是不是处在“正常”的参数范围之内。不幸的 是,窗口小部件不支持 -orient 属性; ProgressBar 的 方向只能是水平的。

简化的数据入口:Tk::DateEntry 和 Tk::PathEntry

通过显示合法的输入并允许用户自其中进行选择, Tk::DateEntryTk::PathEntry 窗口小部件简化了结构化数据的输入(分别是日期和文件路径)。

DateEntry 窗口小部件上有一个文本输入域以及一个紧挨着的按钮。 点击那个按钮,会在下拉菜单中显示一个日历,然后使用鼠标选择日期,相应的字符串就会输入到本文 输入域中。

图 3. DateEntry 窗口小部件
图 3. DateEntry 窗口小部件

清单 3 展示了与此例相关的代码。选用 Convert 来计算从 Unix 诞生到现在的秒数,并将它们显示在本文输入域上面的 Label 窗口小部件中。

清单 3: 使用 DateEntry 窗口小部件
use Tk;
use Tk::DateEntry;
use Time::Local;
%idx_for_mon = ( JAN=>1, FEB=>2, MAR=>3, APR=> 4, MAY=> 5, JUN=> 6,
		 JUL=>7, AUG=>8, SEP=>9, OCT=>10, NOV=>11, DEC=>12 );
$input = '01-APR-2004'; # Initial value for display
$mw = MainWindow->new();
$mw->geometry( '200x80' );
$mw->resizable( 0, 0 );
$label = $mw->Label( -text=>'' )->pack;
$entry = $mw->DateEntry( -textvariable=>\$input, -width=>11,
			 -parsecmd=>\&parse, -formatcmd=>\&format )->pack;
$mw->Button( -text=>'Quit', -command=>sub{ exit } )->pack( -side=>'right' );
$mw->Button( -text=>'Convert',
	     -command=>sub{ convert( $input, $label ) } )->pack( -side=>'left' );
MainLoop;
# called on dropdown with content of \$textvariable, must return ( $yr, $mon, $day )
sub parse {
  my ( $day, $mon, $yr ) = split '-', $_[0];
  return ( $yr, $idx_for_mon{$mon}, $day );
}
# called on user selection with ($yr, $mon, $day), must return formatted string
sub format {
  my ( $yr, $mon, $day ) = @_;
  my %mon_for_idx = reverse %idx_for_mon;
  return sprintf( "%02d-%s-%2d", $day, $mon_for_idx{ $mon }, $yr );
}
# perform the conversion to epoch seconds when the corresponding button is pressed
sub convert {
  my ( $input, $label ) = @_;
  my ( $yr, $mon, $day ) = parse( $input );
  my $output = "Epoch seconds: " . timelocal( 0, 0, 0, $day, $mon-1, $yr-1900 );
  $label->configure( -text => $output );
}

由于以字符串形式来描述同一日期的方式很多,所以日期的输入是困难的。 DateEntry 提供了三种标准的日期格式(MM/DD/YYYY、 YYYY/MM/DD 和 DD/MM/YYYY),可以通过 -dateformat 选项来选择使用它们。如果希望使用不同的日期格式,那么程序员就不得不使用 -parsecmd-formatcmd 回调来显式地给出转换例程。在上面的例子中,我们使用了一个定制的日期格式,以三个字母的 缩写的形式表示月份。当将输入的字符串解析为数字时,我们使用的是哈希表(hash) %idx_for_mon,它保持了每个月份(1..12)给定缩写的数字 索引。当用户从下拉菜单中选择了一个日期后,它必须被格式化为相应的字符串,需要 进行反向查找,也就是根据索引查找缩写。我们在 format 中 使用 reverse 命令动态地构建了这样一个数据结构。 由于这个命令需要以一个数组作为参数,所以将原来的哈希表拆解为一个数组,拆解的方式是, 在数组中,每个值跟在它的键的后面。然后这个数组被翻转(所以现在先前的键跟在值的后面) 并重新转换为一个哈希表。这个技巧在这里是可以成功的,因为键和值都唯一(此外,要获得 关于此内容的更多资料,请参考在 参考资料 中列出的 Learning Perl)。

convert 以包含有输入字符串的变量以及对 Label 窗口小部件的引用作为参数,以改变 Label 所显示的值。在这里,我们没有将 对回调的引用和匿名数组中的参数值传递给 -command 属性;取而代之的是,我们在一个 匿名子例程中直接去调用回调函数(一个 closure)。原因与变量的范围有关;参阅 侧栏中的完整说明。

最后, Tk::PathEntry 窗口小部件是非常简单的:它具有一个输入路径名的文本输入域 —— 但是用到了一个技巧!它的方式类似于 tcsh 或 Emacs 的微缓存,按下 Tab 将尽可能完全地补充完整输入域的内容,而且,如果不能明确地补充完整当前的内容, 则它将弹出一个列表框,列出所有可能的选择。奇怪的是,列表框的颜色不能被改变 —— 除非有人愿意去编辑底层 Perl 模块的代码。

图 4. PathEntry 窗口小部件
图 4. PathEntry 窗口小部件
图 4. PathEntry 窗口小部件
清单 4: 使用 PathEntry 窗口小部件
use Tk;
use Tk::PathEntry;
use Cwd;
$path = cwd();
$mw = MainWindow->new();
$mw->geometry( '300x80' );
$mw->resizable( 0, 0 );
$mw->PathEntry( -textvariable=>\$path )->pack;
$mw->Label( -textvariable=>\$path, -foreground=>'blue' )->pack;
$mw->Button( -text=>'Quit', -command=>sub{ exit } )->pack;
MainLoop;

PathEntry 如此吸引人的原因是,它只是一个窗口小部件,而不是 一个对话框。它可以(实际上是必须)在程序中与其他窗口小部件组合使用。因此,它提供了一个 向应用程序添加文件选择能力的非常轻量级的方法。

结束语

Perl/Tk GUI 工具包的更“高级”的窗口小部件让开发者可以使用 Perl 创建更丰富的、更强大的 用户界面,这些只是其中的一部分。这里所讨论的所有窗口小部件都可以作为用户贡献从 CPAN 免费下载。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • Perl/Tk FAQ 由 Cameron Laird 维护, 如果现在您的系统中还没有安装 Perl/Tk,那么通过它可以知道应该 到哪里去下载以及如何安装它
  • 还有一个 comp.lang.perl.tk 新闻组和一个 ptk 邮件列表。 另外查看 Perl/Tk Wiki
  • The User Interface section on CPAN 中包括了本文中讨论的高级模块。
  • Steve Lidie 为 The Perl Journal 的前三期撰写了三篇关于 Perl/Tk 的文章,并将第一篇,即 “ Perl and the Tk Extension”,重新发布在了他的 Web 站点上 —— 他还在那里保持着 重要的 Perl/Tk 链接页面。
  • Steve Lidie 还撰写了介绍性的“ Getting Started with Perl/Tk”( Perlmonth,1999 年 4 月);此文也重新发表于他的站点上。 Steve 的“ A Drag-and-Drop Primer for Perl/Tk” ( perl.com,2001 年 12 月)和“ Creating Custom Widgets”( perl.com,2002 年 1 月)中涉及了更高级的话题。
  • Cameron Laird 的“ 通过使用 Perl/Tk 把 GUI 加入服务器编程 ” ( developerWorks,2002 年 11 月)深入研究了使用 Perl/Tk 构建用户友好界面的方便性。
  • 如果您对 Perl 非常熟悉,并且是刚接触 Perl/Tk,您可能还会喜欢 perl.com 的幻灯片介绍 Perl/Tk Tutorialperl.com,1999 年 10 月)。
  • Philipp 以前为 developerWorks 撰写了“ 用 Perl/Tk 实现数据可视化”( developerWorks,2003 年 8 月)一文,其中展示了 如何使用 Perl/Tk 构建定制的绘图和图形工具。
  • 标准的 Perl 发行版本附带了对 Perl/Tk 的概述:在命令行中输入 perldoc Tk。还有一个对 Perl/Tk 中回调的简单独立介绍。 参见 perldoc Tk::callbacks
  • Stephen Lidie 和 Nancy Walsh 的 Mastering Perl/Tk(O'Reilly & Associates,2002)适合新老程序员去阅读。
  • Randal L. Schwartz 和 Tom Christiansen 的 Learning Perl, 2nd edition(O'Reilly & Associates,1997)是 Philipp 最喜欢的参考资料之一。 这本书现在已经出了 第三版(2001)。
  • developerWorks Linux 专区 可以找到 更多为 Linux 开发者准备的参考资料。
  • 在 Developer Bookstore Linux 区中定购 打折出售的 Linux 书籍
  • 从 developerWorks 的 Speed-start your Linux app 专区下载可以运行于 Linux 之上的经过挑选的 developerWorks Subscription 产品免费测试版本,包括 WebSphere Studio Site Developer、WebSphere SDK for Web services、WebSphere Application Server、DB2 Universal Database Personal Developers Edition、Tivoli Access Manager 和 Lotus Domino Server。要更快速地开始上手,请参阅针对各个产品的 how-to 文章和技术支持。
static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=23098
ArticleTitle=在 Perl/Tk 中使用高级窗口小部件
publish-date=08232004