不可否认,现在是图形用户界面(GUI)的时代。在可预见的将来,图形界面仍将是与计算机交互的首选方式。
命令行在未来10年内不会消失,它肯定有立足之地。然而,即使是以前的黑客(宁愿用cp –R也不愿使用拖放界面)在适当的时候也乐意使用GUI。
然而,图形编程有很多困难。第一个困难当然是设计有意义的程序“前端”。在界面设计中,一张图片不一定总是胜过千言万语。本书不能解决这些问题,这里要讨论的并非人体工程学、美学或心理学。
第二个很明显的问题是,图形编程更复杂,对于所有可能显示在屏幕上并使用鼠标或键盘进行操纵的控件,都必须考虑其大小、形状、位置和行为。
第三个难点是,不同的计算文化对窗口系统是什么、应如何实现有不同的看法。这些系统之间的差异必须经过体验才能全面理解,很多程序员试图创建跨平台工具,最终却发现GUI之间的不匹配是最难处理的。
本章不能帮助解决这些问题,最多只概述一些流行的GUI系统(与Ruby相关的),提供一些提示和现象。
本章很大一部分内容是关于Tk、GTK+、FOX和Qt的。不管读者的背景如何,都很可能会问:“为什么这里没有包含XXX(你最喜欢的GUI)?”
这可能有几种原因。一个原因是篇幅限制,因为本书并不是专门介绍图形界面的。另一个原因是你最喜欢的系统没有成熟的Ruby绑定(鼓励读者自己创建)。最后一个原因是,并非所有GUI系统都是生而平等的。本章介绍的是最重要、最成熟的系统,其他的只顺便提及。
12.1 Ruby/Tk
Tk最早出现于1988年——如果算上预发行的版本的话。长期以来它都与Tcl一起使用,但近年来它也用于几种其他语言,包括Perl和Ruby。
如果说Ruby有本征GUI,那就是Tk。它在编写本书时仍被广泛使用,有些Ruby下载版还包含Tk。
上面提到Perl并非毫无干系。Ruby和Perl的Tk绑定非常类似,几乎所有Perl/Tk参考资料也适用于Ruby/Tk。
12.1.1 概述
在2001年,Tk可能是Ruby最常用的GUI。它是Ruby最早使用的GUI,长期以来一直是标准的Ruby的一部分。虽然现在不如以前流行,但仍被广泛使用。
有人说Tk过时了,喜欢面向对象的简单界面的人可能对它感到失望,但它具有著名、可移植和稳定等优点。
所有Ruby/Tk应用程序都必须使用require来加载tk扩展,然后使用容器和填充容器的控件逐步创建应用程序的界面,最后,调用Tk.mainloop,该方法捕获所有事件(如鼠标移动和单击按钮)并做出相应的响应。
require "tk"
# Setting up the app...
Tk.mainloop
与大多数窗口系统一样,Tk的图形控件称为窗口部件(widget),这些窗口部件通常使用容器进行组合。顶级容器称为根(root);并非一定要显式地指定根,但这样做是一个不错的主意。
每个窗口部件类都根据它在Tk中的名称来命名(在名称前加上Tk),因此窗口部件Frame对应的类为TkFrame。
使用new方法实例化窗口部件,第一个参数指定窗口部件将要放入的容器,如果省略了,将放入到根容器中。
有两种方式可指定用于实例化窗口部件的选项。第一个方式(与Perl类似)是传递包含属性和值的散列(注意,在Ruby语法中,如果散列是最后一个或唯一的参数,可省略大括号)。
my_widget = TkSomewidget.new( "borderwidth" => 2, "height" => 40 ,
"justify" => "center" )
另一种方式是将一个代码块传递给构造函数,该代码块将被instance_eval执行。在这个代码块中,可调用方法来设置窗口部件的属性(使用与属性同名的方法)。别忘了,代码块将在对象而不是调用者的上下文中执行。这就意味着在代码块中不能引用调用者的实例变量。
my_widget = TkSomewidget.new do
borderwidth 2
height 40
justify "center"
end
Tk中有三个几何体管理器,它们都用于显示在屏幕上的窗口部件的大小和位置。第一个(也是最常用的)管理器是pack,另外两个是grid和place。grid管理器很复杂,但也容易出错,place管理器是最简单的,因为它使用绝对值来指定窗口部件的位置。在本章的示例中,将只使用pack。
12.1.2 一个简单的窗口应用程序
这里将演示最简单的应用程序——一个显示当前日期的简单日历应用程序。
首先创建一个root容器,并将一个Label窗口部件放入到该容器中。
require "tk"
root = TkRoot.new() { title "Today's Date" }
str = Time.now.strftime("Today is \n%B %d, %Y")
lab = TkLabel.new(root) do
text str
pack("padx" => 15, "pady" => 10,
"side" => "top")
end
Tk.mainloop
在上面的代码中,创建了根容器,设置了日期字符串,并创建了一个label。创建label时,将其文本设置为str的值,然后调用pack来排列这些内容。这里让pack在水平方向使用15像素的边距、在垂直方向使用10像素的边距,并使文本在label中居中对齐。
图 12.1显示了该应用程序的界面。
前面说过,也可采用下述方式来创建label:
lab = TkLabel.new(root) do
text str
pack("padx" => 15, "pady" => 10,
"side" => "top")
end
屏幕的度量单位(这里用于padx和pady的单位)默认为像素。也可使用其他单位,为此只需在数字后面加上相应的后缀即可,当然,这样值将变成字符串,但Ruby/Tk并不关心这一点,因此程序员也不用关心。可用的单位有厘米(c)、毫米(m)、英寸(i)和点(p)。下面这些都是设置padx的合法方式:
pack("padx" => "80m")
pack("padx" => "8c")
pack("padx" => "3i")
pack("padx" => "12p")
在这个例子中,side属性没有什么作用,因为它被设置为默认值。如果调整应用程序的窗口大小,将发现文本始终停留在其所在的区域的顶端,该属性的其他可能取值包括right、left和bottom。
pack方法还有其他用于控制窗口部件在屏幕上的位置的选项,下面介绍其中的几个。
fill选项指定窗口部件是否(在水平方向和/或垂直方向)充满分配给它的矩形区域,可能的取值包括x、y、both和none(默认为none)。
anchor选项使用“指南针”表示法在矩形区域中锚定窗口部件,其默认值为center,其他的可能取值有n、s、e、w、ne、nw、se和sw。
in选项可将窗口部件相对于除父容器外的其他容器进行放置,其默认值当然是父容器。
before和after选项可用于改变容器的排列顺序,这很有用,因为窗口部件的创建顺序可能与其在屏幕上显示的顺序不同。
总之,Tk在排列窗口部件方面非常灵活,读者可搜索相关的文档并进行试验。
12.1.3 使用按钮
在所有GUI中,最常用的窗口部件是按钮,要在Ruby/Tk应用程序中使用按钮,可使用TkButton类。
在重要的应用程序中,通常创建框架来包含要在屏幕显示的各种窗口部件,按钮可放到框架中。
通常,至少需要设置按钮的三个属性:
· 按钮的文本;
· 与按钮相关联的命令(单击按钮时将执行的命令);
· 按钮在其容器中的对齐方式。
下面是一个例子:
btn_OK = TkButton.new do
text "OK"
command (proc { puts "The user says OK." })
pack("side" => "left")
end
这里创建了一个新按钮,并将该新对象赋给btn_OK变量。这里传递了一个代码块给构造函数,但也可以使用散列。这里使用了多行形式(这是作者喜欢的风格),但也可以在每行中包含尽可能多的代码。顺便提一下,前面说过,代码块是使用instance_eval执行的,因此,它是在对象(这里为新的TkButton对象)的上下文中执行的。
作为参数传递给text方法的文本将显示在按钮上,这可以是多个单词甚至多行。
pack方法在前面介绍过,它不是很有趣,但如果要显示窗口部件,必须使用它。
这里比较有趣的地方是command方法,它接受一个Proc对象作为参数,并将其同按钮关联起来。通常使用Kernel的lambdaproc方法将代码块转换为Proc对象,这里也是这样做的。
这里执行的操作很愚蠢。当用户单击按钮时,将执行(非图形的)puts,输出将显示在命令行窗口中,该窗口可能启动程序的窗口,也可能是辅助的控制台窗口。
下面来看一个更好的例子。程序清单12.1是一个模拟温度调节的应用程序,它提高和降低显示的温度(给人以控制温度的感觉)。在代码后将对其进行解释。
程序清单12.1 模拟温度调节器
require 'tk'
# Common packing options...
Top = { 'side' => 'top', 'padx'=>5, 'pady'=>5 }
Left = { 'side' => 'left', 'padx'=>5, 'pady'=>5 }
Bottom = { 'side' => 'bottom', 'padx'=>5, 'pady'=>5 }
temp = 74 # Starting temperature...
root = TkRoot.new { title "Thermostat" }
top = TkFrame.new(root) { background "#606060" }
bottom = TkFrame.new(root)
tlab = TkLabel.new(top) do
text temp.to_s
font "{Arial} 54 {bold}"
foreground "green"
background "#606060"
pack Left
end
TkLabel.new(top) do # the "degree" symbol
text "o"
font "{Arial} 14 {bold}"
foreground "green"
background "#606060"
# Add anchor-north to the hash (make a superscript)
pack Left.update({ 'anchor' => 'n' })
end
TkButton.new(bottom) do
text " Up "
command proc { tlab.configure("text"=>(temp+=1).to_s) }
pack Left
end
TkButton.new(bottom) do
text "Down"
command proc { tlab.configure("text"=>(temp-=1).to_s) }
pack Left
end
top.pack Top
bottom.pack Bottom
Tk.mainloop
这里创建了两个框架。上面的框架用于显示温度,为提高逼真度,使用大字体显示华氏温度(使用位置合理的小字母o表示度),下面的框架显示向上和向下的箭头。
注意,这里使用了TkLabel对象的一些新属性。font方法指定label中文本的字体和字号。字符串值是平台相关的,这里显示的字符串在Windows系统中有效。在UNIX系统中,通常是X-风格的字体名,很冗长,如-Adobe-Helvetica-Bold-R-Normal—*-120-*-*-*-*-*-*。
foreground方法设置文本本身的颜色,这里传递了字符串green(它在Tk内部有预定义的含义)。如果不知道某种颜色在Tk是否有预定义,一种简单方式就是试一试。
同样,background设置文本的背景色,这里传递了另一种字符串作为参数:用十六进制格式red-green-blue表示的典型颜色,就像在HTML或在其他各种情形下一样(字符串#606060表示一种灰色)。
注意,这里没有添加“退出”按钮(为避免破坏简单设计)。可以像通常那样单击窗口右上角的“关闭”图标来关闭该应用程序。
注意,在按钮的命令中使用了configure方法,它在温度升高或降低时修改label的文本。前面说过,基本上所有属性都可以这种方式在运行时修改,修改将立即在屏幕上反映出来。
下面将提到另外两种使用文本按钮的技巧。justify方法接受一个参数(left、right或center)来指定按钮中的文本应如何排列(默认为center)。前面说过,按钮可显示多行文本,wraplength方法指定多少列后换行。
可使用relief方法来修改按钮的样式,使其有三维外观。该方法的参数必须是下列字符串之一:flat、groove、raised、ridge(默认值)、sunken和solid。width和height方法显式地控制按钮的大小,另外还有borderwidth等方法。其他的选项(还有很多),请参阅参考手册。
下面再来看一个使用按钮的例子,这个按钮将使用图像而不是文本。
首先创建了两个表示向上箭头和向下箭头的GIF图像(up.gif和down.gif),可使用TkPhotoImage类来获取图像的引用,然后在实例化按钮时使用这些引用。
up_img = TkPhotoImage.new("file"=>"up.gif")
down_img = TkPhotoImage.new("file"=>"down.gif")
TkButton.new(bottom) do
image up_img
command proc { tlab.configure("text"=>(temp+=1).to_s) }
pack Left
end
TkButton.new(bottom) do
image down_img
command proc { tlab.configure("text"=>(temp-=1).to_s) }
pack Left
end
这个按钮的代码将取代前一个例子中相应的代码行。除按钮的外观外,按钮的行为没变。图12.2显示了这个温度调节器应用程序。
12.1.4 使用文本框
可使用窗口部件TkEntry来显示和操纵文本框,该窗口部件也有很多属性用于控制其大小、颜色和行为,下面将通过一个较大的示例来说明其中的几个。
文本框仅在能够检索其值时才有用,通常文本框将与一个变量(实际上是TkVariable,下面将看到)绑定,但也可使用get方法。
在代码段中,假设要编写的是接受下列4项信息的telnet客户端:主机名、端口号(默认为23)、用户ID和密码。这里将添加两个用于执行登录和取消操作的按钮(它们实际上不执行任何操作)。
编写代码时,还将使用一些处理框架的技巧,使界面更整齐美观。这些代码并非真正可移植的,Tk大师可能会鄙视这种方法。但这里旨在演示,因此采用了这种快速而糟糕的排列方法。
其屏幕截图如图12.3所示,代码如程序清单12.2所示。
程序清单12.2 模拟telnet客户端
require "tk"
def packing(padx, pady, side=:left, anchor=:n)
{ "padx" => padx, "pady" => pady,
"side" => side.to_s, "anchor" => anchor.to_s }
end
root = TkRoot.new() { title "Telnet session" }
top = TkFrame.new(root)
fr1 = TkFrame.new(top)
fr1a = TkFrame.new(fr1)
fr1b = TkFrame.new(fr1)
fr2 = TkFrame.new(top)
fr3 = TkFrame.new(top)
fr4 = TkFrame.new(top)
LabelPack = packing(5, 5, :top, :w)
EntryPack = packing(5, 2, :top)
ButtonPack = packing(15, 5, :left, :center)
FramePack = packing(2, 2, :top)
Frame1Pack = packing(2, 2, :left)
var_host = TkVariable.new
var_port = TkVariable.new
var_user = TkVariable.new
var_pass = TkVariable.new
lab_host = TkLabel.new(fr1a) do
text "Host name"
pack LabelPack
end
ent_host = TkEntry.new(fr1a) do
textvariable var_host
font "{Arial} 10"
pack EntryPack
end
lab_port = TkLabel.new(fr1b) do
text "Port"
pack LabelPack
end
ent_port = TkEntry.new(fr1b) do
width 4
textvariable var_port
font "{Arial} 10"
pack EntryPack
end
lab_user = TkLabel.new(fr2) do
text "User name"
pack LabelPack
end
ent_user = TkEntry.new(fr2) do
width 21
font "{Arial} 12"
textvariable var_user
pack EntryPack
end
lab_pass = TkLabel.new(fr3) do
text "Password"
pack LabelPack
end
ent_pass = TkEntry.new(fr3) do
width 21
show "*"
textvariable var_pass
font "{Arial} 12"
pack EntryPack
end
btn_signon = TkButton.new(fr4) do
text "Sign on"
command proc {} # Does nothing!
pack ButtonPack
end
btn_cancel = TkButton.new(fr4) do
text "Cancel"
command proc { exit } # Just exits
pack ButtonPack
end
top.pack FramePack
fr1.pack FramePack
fr2.pack FramePack
fr3.pack FramePack
fr4.pack FramePack
fr1a.pack Frame1Pack
fr1b.pack Frame1Pack
var_host.value = "addison-wesley.com"
var_user.value = "debra"
var_port.value = 23
ent_pass.focus
foo = ent_user.font
Tk.mainloop
这里先将布局问题放到一边。注意,首先创建了几个框架,它们将沿垂直方向从上到下叠放。最上面的框架包含两个小框架,这两个框架在屏幕上从左到右排列。
程序清单12.2中还有一个packing方法,使用它旨在使代码更简洁。它返回一个散列,其中包含为padx、pady、side和anchor选项指定的值。
这里使用TkVariable对象旨在将文本框与变量关联起来,TkVariable有一个存取器value,可用于设置和获取这些值。
创建TkEntry(如ent_host)时,使用textvariable选项将其同对应的TkVariable对象关联起来。在一些情况下,使用width来设置文本框的宽度,如果省略该选项,将使用一个合理的默认值,这通常是根据存储在文本框中的当前值选择的。
文本框的字体、颜色的设置与label相同,但这里没有使用颜色。如果字体是均衡的,两个指定宽度相同的文本框在屏幕上的大小可能不同。
同样,必须调用pack。这里使用常量简化了调用。
文本框password调用了show方法,因为它的值是隐藏的,以防有人偷看。每当用户按键时,显示的是作为参数传递给show的字符(这里为星号)。
前面说过,这里的按钮只是为了美观。Sign on按钮不执行任何操作,但Cancel按钮确实退出程序。
还有其他选项可用于操纵文本框,可在程序的控制下修改其值,而不是让用户修改。可指定字体、前景色和背景色,可以修改插入光标的特征,将其移到所需的位置等。详情请参阅参考手册。
因为这里介绍的是输入文本,因此有必要提一下相关的窗口部件Text。它与文本框的关系就像双座飞机与航天飞机的关系。它是专门为处理大型多行文本设计的,是功能完善的编辑器的基础。由于其复杂性,这里不介绍它。
12.1.5 使用其他窗口部件
Tk还有很多其他的窗口部件,下面将提到其中的几个。
复选框常用于开关值,这是一种简单的true/false或on/off控件。Tk将其称为复选按钮(check button),该窗口部件对应的类为TkCheckButton。
程序清单12.3所示的例子是一个框架代码段,没有任何按钮。它显示了针对三个领域(计算机科学、音乐和文学)方面的复选框,用户选择(或取消选择)其中之一时,控制台将输出一条消息。
程序清单12.3 Tk复选框
require 'tk'
root = TkRoot.new { title "Checkbutton demo" }
top = TkFrame.new(root)
PackOpts = { "side" => "top", "anchor" => "w" }
cb1var = TkVariable.new
cb2var = TkVariable.new
cb3var = TkVariable.new
cb1 = TkCheckButton.new(top) do
variable cb1var
text "Computer science"
command { puts "Button 1 = #{cb1var.value}" }
pack PackOpts
end
cb2 = TkCheckButton.new(top) do
variable cb2var
text "Music"
command { puts "Button 2 = #{cb2var.value}" }
pack PackOpts
end
cb3 = TkCheckButton.new(top) do
variable cb3var
text "Literature"
command { puts "Button 3 = #{cb3var.value}" }
pack PackOpts
end
top.pack PackOpts
Tk.mainloop
复选框被选中时,与其相关联的变量的值为1;被取消选中时,值为0。可使用方法onvalue和offvalue来修改默认值,另外,可在创建复选框前设置变量,以初始化复选框的开关状态。
如果要使复选框呈灰色不可用状态,可使用state方法将其状态设置为disabled。另外两个状态是active和normal,后者为默认状态。
下面修改程序清单12.3中的例子。假设要表示的不是可能的领域而是实际的大学专业。如果不考虑双学位,则同时选择多个选项是不合适的。当然,在这种情况下需要用单选按钮(由TkRadioButton类实现)。
程序清单12.4中的例子与程序清单12.3的例子几乎相同。显然类名不同,另一个区别是,所有单选按钮共享同一个变量,这样Tk知道这些按钮属于同一组。可以有多组单选按钮,但每组的按钮都必须共享一个变量。
程序清单12.4 Tk单选按钮
require 'tk'
root = TkRoot.new() { title "Radiobutton demo" }
top = TkFrame.new(root)
PackOpts = { "side" => "top", "anchor" => "w" }
major = TkVariable.new
b1 = TkRadioButton.new(top) do
variable major
text "Computer science"
value 1
command { puts "Major = #{major.value}" }
pack PackOpts
end
b2 = TkRadioButton.new(top) do
variable major
text "Music"
value 2
command { puts "Major = #{major.value}" }
pack PackOpts
end
b3 = TkRadioButton.new(top) do
variable major
text "Literature"
value 3
command { puts "Major = #{major.value}" }
pack PackOpts
end
top.pack PackOpts
Tk.mainloop
这里使用value方法将特定值与每个按钮关联起来,可使用任何值(如字符串),明白这一点很重要。这里没有使用字符串,旨在强调在窗口部件的文本与其返回值之间没有直接关系。
有很多选项可用于定制复选框和单选按钮组的外观和行为。例如,image方法让程序员能够显示图像,而不是文本。大多数常见的格式和显示选项也适用于这两种窗口部件,详情请参阅参考手册。
如果本书(或本章)是专门介绍Tk的,将介绍多一些,但不可能很详细地介绍这些,这里只提一下让读者知道存在这些窗口部件。
列表框(TkListBox)窗口部件让程序员能够以下拉列表的形式指定值的列表,供用户从中选择。选择模式(由selectmode方法指定)可以是single、extended或browse,前两种模式指定用户只能同时选择一个值还是可以选择多个值,browse模式与single模式相似,但当用户按住鼠标时可改变选取。可将列表框设置为可滚动的,并能够包含任意数量的选项。
Tk提供了高级菜单功能,包括下拉菜单、分离式菜单(tear-off menu)、级联子菜单、键盘快捷键、单选按钮菜单等。读者可尝试使用TkMenu、TkMenubar和TkMenuButton等类。
最有吸取力的窗口部件可能是TkCanvas,它让程序员能够在像素级操纵图像。它提供了绘制线条和形状、操纵颜色和加载各种格式的图像等功能,如果应用程序涉及高级图形或用户控制的绘图功能,可使用该窗口部件。
scrollbar窗口部件处理自定义的滚动行为,包括水平方向和垂直方向(例如,同步滚动两个独立的窗口)。scale窗口部件是一种表示数值的滑块,可以水平或垂直放置,可用于输入或输出。有关其他窗口部件,请参阅高级文档。
12.1.6 其他说明
Tk的未来是不确定的(任何软件系统都如此),但不会很快消失。编写本书时,Ruby/Tk基于Tk 8.4,将来很可能会更新。
关于操作系统还要说几句。从理论上说,Tk是独立于平台的,实际情况很接近理论。然而,有些用户发现Windows版本不如UNIX版本稳定。本章的所有例子都在Windows上进行了测试,都能够正常运行。







