内容


用 Perl/Tk 实现数据可视化

使用 Perl 的标准 GUI 工具箱构建自定义绘图工具

Comments

在直观显示数据时,人类的眼睛在识别复杂的行为、运动轨迹和图案时表现得出奇地好。如果一个数据集的数据超过 12个点,那么用图形表示就会有所帮助;如果数据集里的数据超过了数千个点,那么使用图形表示就是必需的了。

对于简单的 x-y 平面图,gnuplot 经常是第一选择。对更复杂的问题来说,您可以使用 xmgrace 或者其他的绘制工具。但是大多数简单的曲线绘图仪在绘制二维数据方面或者在复杂的图形中进行极为精细的控制方面显得很不足。复杂图形的例子包括专业化的条形图和须状图,带有完整误差的时间序列条形图,颜色编码和密度图,以及许多其他可能的图。

这就是 Perl/Tk 的闪光之处。前提是,您已经使用 Perl 来进行数据操纵和提取。Perl/Tk 提供了与 Perl 绑定的 Tk GUI 工具集,并且它可能是最容易使用的部件之一。

在本文中,不会关注 Perl/Tk 的实际用户界面部分(例如,复选框和下拉菜单),只探讨它的绘图能力。

作为一个 GUI 工具箱,Perl/Tk 提供一个其他 Perl 图形扩展包(例如,极好的 GD 包)中没有的附加工具:即,动画和交互式数据探索的能力。在接下来的示例中我将向您展示这些应用。

Perl/Tk 程序剖析

为了介绍 Perl/Tk 编程的基本概念,先让我们考虑大家熟知的“Hello, world”程序,在 Perl/Tk 中,该程序类似如下:

清单 1. Perl/Tk 风格的“Hello, world”程序
#!/usr/bin/perl -w
use Tk;
$mw = MainWindow->new;
$mw->Label( -text => "Hello, World!" )->pack;
$mw->Button( -text => "Exit", -command => sub { exit } )->pack;
MainLoop;

这个程序首先打开一个新窗口,然后显示文本和一个“Exit”按钮。单击这个按钮,终止程序。(曾经使用过的GUI工具箱有比这个程序更容易的吗?)

语句 use Tk; 声明在程序中要引用 Tk 模块。整个 Tk 工具集汲取了大多数 Perl 面向对象的特性。您经常可以看到使用->符号基于对象对方法进行调用。

每个 Perl/Tk 应用程序必须明确地对应一个主窗口。它是一个“项层”部件,意思是它必须在一个单独的窗口中打开。我们已经使用其构造函数 new 来说明了这一点。

下一步,我们将在这个主窗口中创建标签和按钮对象,作为主窗口的子部件。每个部件(除了顶层部件外,比如主窗口)必须是某个确切父部件的子部件。

我们为标签和按钮部件指定一些参数,然后调用 pack ,该语句是 Perl/Tk 的几何管理器之一。几何管理器的职责是在父窗口中将子部件有规则地排列起来。除了语句 pack 外,还有语句 gridplace ,它们除了布局部件外,更可以对部件进行出色的纹理控制。

在 Perl/Tk 中,部件的参数用它们的名字来指定(和管理器工具语句 pack 的参数一样),通常开头带有一个短横。注意那些围绕参数名的引号不是必需的(因此通常被省略掉),因为操作符=>将它左边的裸单词(barewords)看作是被引用的字符串。参数 -command 用来定义回调,如果部件接收到一个用户事件,那么就会触发对将被调用函数的引用(例如,在当前例子中,用户点击按钮)。由于这个回调程序体很小,所以我们将整个函数内嵌为一个匿名子例程来完成。下面您将看到独立子例程被注册为回调程序的示例。

现在我们的部件已经设置完成,并且程序准备接收用户事件。程序的最后一行调用 MainLoop 语句,这个语句的作用是开始监听用户事件,并且把它们分派给合适的回调。所有的事件都已被对应的能够产生事件的部件注册。

整个程序就是这样!注意初学者通常犯的错误是忘了调用 pack 或者调用 MainLoop 。如果程序运行时应用程序窗口不能正常显示,原因可能就是由于其中之一或者两者都被遗漏掉所造成的。另外要记住的一点是新窗口的实际大小和位置是由窗口管理器控制的(比如Gnome,KDE,IceWM),而不是由程序本身控制。

在画布上创建图形

如果我们要输出图形,Tk 提供了画布部件。一旦创建了画布部件实例,其上就可以创建一些标准的图形对象(例如,直线、圆形、长方形等)。以下是示例程序:

清单 2. 创建了画布部件实例并在其上放置对象
#!/usr/bin/perl -w
use Tk;
my ( $size, $step ) = ( 200, 10 );
# Create MainWindow and configure:
my $mw = MainWindow->new;
$mw->configure( -width=>$size, -height=>$size );
$mw->resizable( 0, 0 ); # not resizable in any direction
# Create and configure the canvas:
my $canvas = $mw->Canvas( -cursor=>"crosshair", -background=>"white",
              -width=>$size, -height=>$size )->pack;
# Place objects on canvas:
$canvas->createRectangle( $step, $step, $size-$step, $size-$step, -fill=>"red" );
for( my $i=$step; $i<$size-$step; $i+=$step ) {
  my $val = 255*$i/$size;
  my $color = sprintf( "#%02x%02x%02x", $val, $val, $val );
  $canvas->createRectangle( $i, $i, $i+$step, $i+$step, -fill=>$color );
}
MainLoop;

在这个程序中,和前一程序一样,首先创建一个主窗口,并且设置窗口的宽和高为 $size 像素。 resizable( $in_x_direction, $in_y_direction ) 方法用于固定顶层窗口的尺寸。该方法带有两个布尔变量,用于决定部件在 x方向或者 y方向是否可以调节。这里我们完全禁止调节尺寸。

下一步是创建画布部件并使之充满整个主窗口。我们已经将画布清理干净(将画布的背景色设置为“白色”),并且当鼠标移至画布时,光标变为交叉(在 X11 中有 78种标准的鼠标光标形状,其名称可以在头文件 cursorfont.h 中找到,典型安装时,这个头文件可以在/usr/X11/include/X11/ 目录中找到。)

注意在末尾要调用 pack ,它使得画布对象的实例在主窗口中显示出来。

在注释语句 "# Place objects on canvas:" 之后,我们准备在画布上绘制内容了。我们已经创建了一个大的红色的长方形,并且有一排较小的灰色长方形斜穿过它。注意画布对象使用了标准的“图形坐标系统”,x 轴指向右边,y 轴指向下面,所以坐标系统的原点(两轴交接的地方)位于窗口的左上角。

图 1. 放置在画布上的长方形对象
Objects placed on canvas

这段代码演示了在 Perl/Tk 中指定颜色的两种办法。一种是使用在文件 rgb.txt(通常这个文件可以在目录 /usr/X11/lib/X11/ 下找到)中预定义的颜色名称,比如“red”或者“PapayaWhip”。另一种方法是指定各个 RGB(red/green/blue)值,这个值以“#”打头,后面跟着三个两位的十六进制数的字符串。注意如果某个十六进制数只有个位数,那么在其左边必须补上一个零。如果 RGB 三个值相等(正如本例所示),显示的颜色就是灰色。

长方形(其他形状也一样)有两种颜色,一种是填充色,另一个是边框色。由于我们没有指定后者,所以默认为黑色。要去除这些边框,设置 -outline 属性为与填充色相同即可;要加宽边框,使用 -width 属性来指定宽度(用像素表示)。

最后,要注意图形元素(像长方形、直线、圆形)都不是部件!诸如 createRectangle 这样的函数都不会返回一个对象,然而每个图形元素都有一个 ID(确切地说,是一个数字)来标识。要移动、修改或者删除图形元素,该 ID将作为参数传递给包含画布对象的各个成员函数。例如,为了删除红色正方形,我们使用 $canvas->delete( $id ) ,其中的 $id 就是第一次调用函数 createRectangle 的返回值。

与用户交互

从某种意义上来讲, GUI工具箱(例如 Tk)的整个出发点就是为了实现与用户交互。换句话说,工具箱应该使得应用程序响应各种事件变得很容易,例如鼠标点击或者键盘输入。

在 Perl/Tk 中,大多数部件都有 -command 属性,正如上面所提及的,该属性允许部件注册一个子例程(“回调”),如果这个部件被用户激活,那么该子例程将被调用。我们已经在清单1中看到了一个这样的例子,其中回调是一个匿名子例程: -command => sub { exit } 。如果不采用这种方法的话,我们要想注册一个子例程(用 sub method_name{ ... } 来定义),那么必须注册一个到这个子例程的引用,类似于: -command=>\&method_name

画布部件则不同,它不带-command属性。为了使画布能够响应用户的交互,我们需要使用 Tk::bind 函数显式地将回调绑定到事件。(注意显式命名空间 Tk:: 在这里是必需的,因为画布部件定义了自己的 bind 函数是隐含继承的)。

函数 bind 带有两个变量:第一,要响应的事件序列;第二,回调和它的参数。事件序列是放在尖括号中的字符串,例如, <Motion> 或者 <Shift-Button-3> 。(Tk响应的事件是类似于 X11窗口系统定义的事件集,但是不完全相同。请查阅 Tk::bind 的参考资料来获得事件和修饰符的完整列表。)

函数 bind 的第二个变量是被调用的回调。我们已经看到如何使用匿名的子例程或者对指定子例程的引用。当回调需要参数时,我们将这个回调指定为匿名列表,首先是子例程引用,然后是后续列表项参数: bind( "<Motion>", [ \&method_name, parameter1, parameter2 ] ) 。注意方括号(不是圆括号)中需要组成一个匿名列表引用。

使用 bind 指派的回调的第一个变量总是一个对产生事件的部件的引用。然后才是用户定义的参数。这是一个非常容易忘记的细节!

作为最后的难点, Ev() 工具允许检索和使用调用回调的事件细节。例如,事件发生的位置坐标(参照 $canvas 原点)可以通过 Ev('x')Ev('y') 获得。

所以如果我们增加以下一行:

$canvas->Tk::bind( "<Button-1>", [ sub { print "$_[1] $_[2]\n"; }, Ev('x'), Ev('y') ] );

放到我们第二个示例程序中的 MainLoop 调用之前,那么当我们在画布窗口中单击鼠标左键时,画布坐标将显示在屏幕上。(注意,在参数列表中,我们省略了第一个条目 $_[0] 。这是上面提到的对调用部件的引用,在本例中,就是 $canvas 。)

一个图形例子

作为展示绘制图形和数据可视化能力的重要例子,设想我们面临以下带有两个变量 x和 y的方程:

f( x, y) = cos(2 a) cos(4 b) + cos(5 a) cos(3 b) + cos(7 a) cos(1 b)

其中 ( pi = 3.1415...,sqrt() 表示平方根):

a= pi ( x- sqrt(3.0) y ) b= pi ( 3 x+ sqrt(3.0) y )

仅从研究方程本身很难得到该函数行为的任何想法。此外,这个行为十分复杂,以致即使绘制大量的一维图形(也就是说固定 y 为某些值,仅仅绘制 x 函数的值)也不能揭示该方程的底层结构。然而,该函数的简单二维密度绘图马上给我们一种清晰的感觉,那就是在原公式中包含的简单而漂亮的对称结构。

图 2. 二维密度图
Two-dimensional density plot
Two-dimensional density plot

您可以在本文后面的 参考资料 中下载这个程序,并完成绘图。根据前面的讨论,这个程序应该容易理解,虽然在这里一步一步地讲解这个程序会过于冗长。注意,这个程序关于处理实际绘图和用户交互的部分十分简短。程序的绝大部分花费在创建和设置各种GUI部件,以及完成从函数的坐标系统到屏幕坐标系统的转换上。这是所有绘图程序的典型特征(并且如果我们允许用户输入要绘制的函数,程序将进一步扩大,这是由于代码必须完成必需的输入验证。)

保存工作

在屏幕上看到数据是一回事,将它保存到磁盘中(或者打印出来)又完全是另一回事。Perl/Tk 已经显示了很强的绘图能力和灵活性,但是不可思议的是它竟然没有一个标准的模块用于将图形存储到位图文件中。

有一个工具用于将画布内容存储到 PostScript 文件中,这就是通过调用 $canvas->postscript( -file=>"file_name.ps" ) 。这将只能捕获在画布中实际显示的内容。因此,要注意的是一定要确保画布对象已经全部渲染到屏幕上;否则输出文件将是空的。函数 update() 用于(在任何部件上)强制渲染并且等待所有进行的事件全部完成。

另一种可能是将每一屏幕像素的 RGB 值输出为由三部分组成的字节,并直接写入任何文件句柄。一些图形程序可以处理用这种方法生成的 rgb 文件。可能最为强大的是来自 ImageMagick 软件包的转换实用程序,它能够将 rgb 文件转换成任何常用的图形文件格式。

最后,从显示窗口获取图形文件的最简易方法是直接拷屏。来自 ImageMagick 软件包的导入工具十分灵活方便,并且可以生成多种文件格式。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • 要创建图 2 所示的密度图,请下载 hexa-plot.pl
  • 由Stephen O. Lidie 和 Nancy Walsh 合著的书 Mastering Perl/Tk (O'Reilly & Associates, 2002) 提供了通俗易懂的介绍。它是第 2 版,对 Nancy Walsh 所著的 Learning Perl/Tk(O'Reilly & Associates, 1999) 进行了大量扩充。 然而该书组织得不是很好,而且没有覆盖标准发行版的所有组件。
  • 也许最能够及时更新并且全面的 Perl/Tk 文档是手册页了。输入 man Tk (或者 perldoc Tk ),就可以看到简单的和复杂的部件的概述和详细列表。
  • 在本文中,作者没有提到的一个问题是颜色选择。原因是 Perl/Tk 仅支持 RGB 颜色规范(和符号名称)。对于大多数人来说,RGB 值很不直观,基于 HSV (Hue/Saturation/Value=Brightness)色彩模型的颜色选择要更加容易一些。然而,在不同颜色空间之间进行转换却是十分复杂的。这方面的例子可以参阅 Color FAQ或者 Colour Space Conversions FAQ。简单但是很有用的概述可以参阅 Introduction to Color
  • 有关如何从命令行使用ImageMagick的概述请参阅 " 通过命令行处理图形" ( developerWorks,2003年7月)。
  • ImageMagick 主页是下载ImageMagick软件的地方。
  • 如果您需要操纵 TIFF 图像,学习如何通过调用 libtiff 来使用 TIFF 的 ANSI C 实现,可以参阅 developerWorks的两部分系列文章。 用 libtiff 进行图形编程,第 1 部分 讨论了TIFF的不足之处,并指导您使用 libtiff 库来处理黑白图像。 用 libtiff 进行图形编程,第 2 部分 进一步讲解如何使用 libtiff 库处理灰度和彩色图像。
  • 字符和图形的 Perl 用户界面 列在CPAN上。
  • 在 CPAN上您可以找到更多的 GD.pm 信息,GD 图形库上的 Perl 界面。
  • SFGraph 是 IBM alphaWorks 框架,用于压缩和查看高分辨率的大图像。SFGraph 的编码器使用 wavelet 技术来压缩大图像,这些图像可以是 PGM、PPM、PNM、JPEG、GIF,以及原始 RGB 颜色和灰度级别格式。
  • developerWorks 的 Linux 专区可以找到更多的 linux 开发者资源
  • developerWorks 的开放源代码专区,可以浏览更多的 开放源代码资源

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=20686
ArticleTitle=用 Perl/Tk 实现数据可视化
publish-date=10012003