明辉手游网中心:是一个免费提供流行视频软件教程、在线学习分享的学习平台!

简单完成Java用户界面编程

[摘要]Buoy 是一个构建在 Swing 之上的免费用户界面(UI)工具包,它为 UI 开发人员提供了方便性和简单性。在本文中作者用一个简单的 fractal 用户界面程序,介绍了 Buoy 可以做什么、为什么这么做。第一次尝试用 Java 语言构建简单的用户界面时,我对 Swing 接口的复杂性感到有...

Buoy 是一个构建在 Swing 之上的免费用户界面(UI)工具包,它为 UI 开发人员提供了方便性和简单性。在本文中作者用一个简单的 fractal 用户界面程序,介绍了 Buoy 可以做什么、为什么这么做。

第一次尝试用 Java 语言构建简单的用户界面时,我对 Swing 接口的复杂性感到有些惊讶。老实说,有点想打退堂鼓。最近,一个朋友向我提到,他使用的渲染程序 Art of Illusion(请参阅 参考资料)基于一个不同的工具包:Buoy。推荐它的原因之一是它的界面更友好。当他第一次提到它时,我以为他在谈 "BUI",而它与 GUI 这个名字的相似是故意的。在这里 B 代表 better(更好),但是名字 Buoy 并不是缩写。

Buoy 是免费的。实际上,它是公共的东西。它并没有在某个开放程度合理的许可下发布,实际上它根本不受任何许可控制。这意味着在任何用 Java 语言编写的能够运行 Buoy 的项目中都可以使用 Buoy,而不用考虑许可问题。因为提供了完整的源代码,所以这个工具包很容易修改和扩展。本文基于 Buoy 1.3 发行版,要求读者对 Swing 有基本的了解,虽然不了解也能对付过去。

示例程序

我曾经尝试用 Swing 构建的第一个应用程序最后以失败告终。为了看出工具包之间的对比情况,我决定使用 Buoy 来构建这同一个程序。文章中的代码示例全部来自该程序的 Buoy 版本。程序生成了一些分形,具体地说,是迭代的分形。基本思想很简单:在平面上定义一系列的线条区段,从(0,0) 到(1,0),围绕任意一个单位线条定位。绘制这些区段之后,绘制同一套变形线条,用这个区段作为单位向量。做起来比说的更容易,就像在图 1 中看到的。

图 1. 分形编辑器中的分形

这个程序的界面相当简单。它有一些界面小部件,有一个画布,在画布上绘制漂亮的图片,还支持用鼠标操纵图片。实际上,必须要做的全部工作就是操纵构成原始曲线的点,原始曲线会迭代地绘制出来。界面还有一个最小化的菜单;它可以打开和关闭文件,关闭窗口,或者把当前图像保存为 PNG 格式的文件。虽然简单,但是这个界面简要地提供了一个 Buoy 小部件的合理示例,还有相当数量对事件处理系统的体验。

程序实际的核心代码 —— 分形生成器 —— 已经写好了,这把这个示例变成一个很好的测试程序。当然,在更新它的过程中,我也发现并且修补了一些 bug。


[page_break]    发行包中包含示例程序的源代码,还有编译好的类文件和 Buoy 的 JAR 文件(单击本文顶部或底部的 Code 图标,下载 factal.tar)。包中还包含一个叫做 frac 的目录,里面包含一些示例分形。如果使用一台 UNIX 风格的机器,在路径中有 Java 编译器,那么只要运行 make 就能运行它。否则,需要设置 classpath 包含当前路径和 Buoy 的 JAR 文件所在的目录,然后运行 FractalViewer 类。在 Windows 系统上,正确的命令行应当是 java -classpath .;Buoy.jar FractalViewer。

  sed -e s/J/B/g

  在第一次深入研究代码时,也许会形成这样的印象:把 Swing 代码转换成 Buoy 代码简单得就像把 UI 元素名称中的字母 J 换成 B 一样简单。例如, FractalViewer 类不再扩展 JFrame;它现在扩展的是 BFrame。主要的小部件名称也可以照此推测得到。Spinner 和 slider 像以前一样有相同的名字,只是换了一个字母。 MenuBar(菜单条) 仍然由 Menus(菜单)构成,菜单则容纳 MenuItems。

  有些命名转换略有不同。在 Swing 引用 BorderLayout 的地方,Buoy 有 BorderContainer。一般来说,Buoy 的命名转换相当统一,虽然不总是与 Swing 的命名一样。一个明显的区别是 Buoy 几乎组合了容器和布局管理器的概念;每种容器类型都知道自己如何布局。这大大简化了设计。例如,在分形生成器中使用的 LabelWidget 类是一个 BorderContainer;在 Swing 中,这可能是一个带有 BorderLayout 布局管理器的 JPanel。

  但是,两者还是有许多相似之处。这对适应新东西有很大帮助。更重要的是,Buoy 构建在 Swing 之上。这意味着,一般来说,如果需要做的事不能轻松地用 Bouy 完成时,可以把 Buoy 对象传递给它包装的 Swing 对象。对于这种情况,如果想访问一些没有 Buoy 对应物的 Swing 对象,可以简单地把它包装在 AWTWidget 对象中,这个对象提供了非常薄的包装器,通过它,不仅 Buoy 自己的小部件,而且所有的小部件都能访问 Buoy 的小部件 API。例如,如果发现确实需要 GridBagLayout,可能就需要这样做。

  例如,FractalPanel 类是一个 AWTWidget。在早期设计中, 它是 JPanel 的子类, 但实际上我并不需要 JPanel 代码。相反,我构建了包装定制类的类 FractalCanvas, 它本身是普通的 Canvas 类的一个子类。把它变成一个 AWTWidget,就可以在它上面利用 Buoy 高效的事件处理机制。

  事件处理代码非常简单。在按下鼠标按钮时,通过 addEventLink() 的魔力,Buoy 发送一个新的 MousePressedEvent 事件到 mousePressed() 函数。我忽略了按下哪个按钮这个问题,只考虑按住 shift 单击或普通单击。普通单击选择最靠近的点,而按住 shift 单击则重新把显示居中。然后,如果鼠标移动,那么每次 Buoy 注意到移动时都会开始发送 MouseDraggedEvent 事件。在处理这些事件时,FractalPanel 会生成自己的事件。

  近观 PointChangedEvent

  为了让一些讨论更加具体,请来看 PointChangedEvent。这是一个试验性的类,如果不喜欢它,那也只能怪老天了。这个类的想法是:让一个类来表示状态点中的变化。编辑器跟踪“当前”点 —— 也就是编辑器小部件目前正在编辑的点。可以用这些小部件或在分形面板中单击选择新的点,选择的是最靠近的点。

  我得出这样一个结论:在代码中,大概有三类涉及到点的事件需要从一个类发送到另一个类。

  一个是改变某个点的特征: POINT 事件类型。如果由编辑器发送,就是告诉分形改变原型线条上的点,并要求重画线条。如果由分形发送,则是告诉编辑器刚刚选中的点的特性。

  下一个是选择某个点。可以按索引或位置进行选择。所以,如果只提供了索引或位置,那么构造函数会认为意图是填充其他值。有一点特殊的地方,点索引 -1 用来表示没有选中的点,所以必须用 -2表示编辑器正在寻找指定位置的点。这可能不漂亮,但是有效。

  有点意思的是 Fractal 类响应 SELECT 事件的方式。如果成功地选择了一个点,就会发回一个新的 POINT 类型的 PointChangedEvent 事件,如清单 1 所示。

  清单 1. 用事件回答事件
case PointChangedEvent.SELECT:
if (e.getIndex() >= -1)
selectPoint(e.getIndex());
else
selectPoint(e.getPoint());
// just in case they don't know
event(new FractalChangedEvent(FractalChangedEvent.SIZE, size));
if (selectedPoint >= 0 && selectedPoint < size)
event(new PointChangedEvent(selectedPoint, points[selectedPoint]));
else
event(new PointChangedEvent(selectedPoint, null));
event(new FractalChangedEvent(FractalChangedEvent.REDRAW));
break;

  最后,移动点是一个特殊情况,如果不需要改变点的其他属性(例如颜色),那么所要处理的就是位置。这就是 MOVE 事件类型。在效果上,它与 POINT 事件类型效果很像,但它不需要事件生成器(通常是 FractalPanel 类)去关心那些它根本不知道的属性。
[page_break]  INSERT 和 DELETE 事件类型只有部分相关,可能应当属于 FractalChangedEvent 事件。

  事件处理

  正如已经开始看到的,事件处理是 Buoy 与 Swing 最明显的不同之处。事件处理提供了大量灵活性。Buoy 本身的事件集相当丰富,且允许您挑选自己感兴趣的事件,从任何小部件向其他对象发送事件。例如,如果想在 Swing 中捕获鼠标事件,捕获事件的类需要实现 MouseListener 接口。这个接口有 5 个函数需要实现,即使它们就是摆设也必须实现。而且必须使用接口提供的函数名称。更糟的是,函数必须是侦听器接口的公共部分;要么把这作为公共接口的一部分公开,要么创建一个什么都不做、只是包装事件侦听器代码的内部类。

  在 Buoy 中,每个小部件都是 EventSource 。这意味着可以从每个小部件侦听事件。什么类型的事件呢?任何类型都可以。关键的函数是 addEventLink()。这允许您指定类、侦听器以及可选的方法。每当 EventSource 分派这个类或它的子类的事件时,侦听器都会接收到事件,要么是通过一个叫做 processEvent()的方法,要么是通过在开始调用 addEventLink() 时提供的方法名称。提供的函数不能接受参数,也不能接受与指定事件类型兼容的类的对象;父类和接口可以。

  这是一个方便的设置。可以把不同的事件路由到不同的函数或相同的函数。例如,MousePressedEvent 和 MouseReleasedEvent 会被分别处理。在示例程序中,鼠标的按下、释放和拖动分别有不同的线程,如清单 2 所示。注意,这远远超过 Swing 的 MouseListener 所能做的。如果用 Swing 编程的话,就需要实现 MouseListener 和 MouseMotionListener 这两个接口。

  清单2. 只挑感兴趣的事件
this.addEventLink(MousePressedEvent.class, this, "mousePressed");
this.addEventLink(MouseReleasedEvent.class, this, "mouseReleased");
this.addEventLink(MouseDraggedEvent.class, this, "mouseDragged");
[...]
public void mouseReleased(WidgetMouseEvent ev) {
lastCenter = null;
dispatchEvent(new FractalChangedEvent(FractalChangedEvent.SLOW));
setAntiAliasing(true);
}

  mouseReleased() 函数只有最少的工作要做。它只是在 mousePressed() 函数之后进行清理,告诉 Fractal 对象到了开始全面重绘的时候了。

  Buoy 的事件处理还有另外一个有趣的特性。如果愿意的话,可以创建新的事件类型。一个事件类型就是一个类。确实如此。它甚至不需要继承任何类或实现什么。它就是一个类。如果这个类的对象被发送到 dispatchEvent(),那么它或它的父类的侦听器就会被调用。在 Swing 中也可以创建新的事件类型,但是完全要自己进行;必须设计 Listener 接口,还要编写自己的代码生成事件并侦听事件。在示例程序中,设计了 Fractal 类,演示了可以相对容易地把事件处理功能加到任何原有的类中。只需要声明一个 FractalViewer 类用来添加侦听器的事件源 EventSource。FractalViewer 类就会把来自事件源(例如 FractalEditor)的事件链接设置到它们的侦听器,如清单 3 所示。

  清单3. 绑定
private void tieEvents() {
// Set up event handling relations.
addEventLink(WindowResizedEvent.class, this, "layoutChildren");
addEventLink(WindowResizedEvent.class, panel, "repaint");
tieControlEvents();
tieFractalEvents();
tiePanelEvents();
}

  定制事件类一般是为了表示用户行为。在 Buoy 中,一般只通过用户行为,而不是系统接口生成事件 —— 除非自己想显式地调用 dispatchEvent() 自行生成事件。当分形对象以某种会造成字段更新的方式变化的时候,所有部件的控制面板都会得到通知。这样,我们发明一个新类 ParameterChangedEvent,用它表示参数已经变化。或者,如果变化的是选中的点的位置或是索引,就发送一个新的 PointChangedEvent。如果行为足够明显的话,那么事件处理器甚至不需要接受参数。作为事件处理的一个示例,请看清单 4,它演示了 FractalEditor 的 parameterChanged() 方法的开始部分。

  清单 4. 参数发生了变化
void parameterChanged(ParameterChangedEvent ev) {
FractalParameters p = ev.getParams();
int v = ev.getValue();

switch (ev.getType()) {
case ParameterChangedEvent.ALL:
maxSlider.setValue(p.getMaxIterations());
minSlider.setMaximum(p.getMaxIterations());
minSlider.setValue(p.getMinIterations());
maxSlider.setMinimum(p.getMinIterations());
zoomSlider.setValue(p.getZoom());
break;
[...]
  在这个例子中,用事件处理系统把各种信息前后传递。在以前的版本中,每个类都有对其他每个类的引用,而且乱七八糟的 get 方法是按天排序的。而在目前的版本中,Buoy 的事件处理系统被用来处理各种通知。例如,FractalChangedEvent 类可以用来让代码的其他部分知道对分形的修改,可能是点的数量变化(编辑器用点的数量为点选择器定义正确的 SpinnerNumberModel),或者是需要重绘的通知
[page_break] 清单 5. 显然到了重绘的时候
public void fractalChanged(FractalChangedEvent e) {
switch (e.getType()) {
case FractalChangedEvent.REDRAW:
repaint();
break;
}
}
  Buoy 的文档详细讨论了 Swing 事件模型与 Buoy 事件模型的差异,以及这些差异的原因。有很好的理由,而且 Buoy 的模型通常会导致更小、更清晰的代码。当然,仍然可以做多余的或愚蠢的事情,就像在任何系统中都可以做的那样,但是至少在做这些事情的时候有一个干净漂亮的界面。

  学习曲线

  我曾经观察到,学习使用一个 GUI 工具,一下午的时间还不够长。对于 Buoy,我大概需要 6 个小时或者差不多一整个工作日。我确实从更有经验的 Buoy 用户那里得到了很棒的帮助。以前学习 Swing 的经验也是有帮助的,但实际上,我并不认为 Swing 的经验是必需的。Buoy 的文档相当好,而它的简单性确实有帮助。对于基本的 UI 事物,没有太多要学的东西。

  Buoy 的文档并不像 Swing 文档那样完整,但是覆盖了许多细节,而且非常好。另外,源代码也在那儿,所以回答一些关于界面的简单问题非常容易。具有更完整的文档当然是好事。但是,既然这个项目放在 SourceForge 上,所以如果您愿意,您可以编写更多的东西为它做贡献。

  Buoy 的学习曲线比起 Swing 是一个很大的优势。用相当简单的界面就能让大多数界面小部件正确工作。要使用 Buoy 文档中的一个示例:在 Swing 中,JList 要求要么使用静态列表,要么构建一个实现 ListModel 接口的新类。在 Buoy 中,只需向列表中添加项目;在大多数常见情况下,艰巨的工作已经由 Buoy 替您做了。

  Buoy 相当小。完整的发行包中包含源代码、JAR文件和文档,总共不到 1 MB。代码的组织良好,可以容易地找到任何特定的代码段,如果需要调整设计,也不困难。

  Bug

  尽管 Buoy 是一个稳定、有用的系统,但并不是一个绝对完美的东西。偶尔在明显选择很合理的地方它也会有奇怪的表现,产生令人惊讶的行为。如果考虑用 Buoy 来完成一个实际的项目,就需要了解 bug:它们的普遍程度、严重性,以及克服它们的难度。

  在开发这个应用程序的过程中,我碰到一些事情,当时看起来像是 bug。但不全是。有一些可能是文档中的 bug —— 在这些情况中,代码的行为不是预期的,但是却非常合理。实际上,我可以非常肯定,从实质上讲并不是 Buoy 中的 bug,但它们确实呈现了在调试 Buoy 应用程序时可能会遇到一些事情。在调试了几天代码之后,我可以非常肯定,我遇到的每个明显的 bug,要么是我的错误,要么是我不太喜欢的底层 Swing 中的设计决策。可以肯定地说,在 Swing 中不可能避免这些问题。

  滚动条刻度

  早期我最常遇到的一个 bug 是处理标尺中的刻度标记的时候。最初,我无法得到在它们上面显示的标签。

  清单 6. 让人郁闷的滚动条代码
minSlider.setShowLabels(true);
minSlider.setMajorTickSpacing(2);
  清单 6 中的代码不起作用。可以看到,标签只是在设置了刻度间距后才显示。如果在告诉滚动条显示标签之前没有设置刻度间距,它不显示任何刻度就结束了。更微妙的是,随后也不能改变刻度间距;改变刻度间距的尝试没有效果。但是,这实际不是 Buoy 的 bug,而是 Swing 的工作方式。由于 BSlider 类只是把请求传递给 JSlider,所以责怪 Buoy 是不公平的。

[page_break]  一个更微妙、也与底层 JSlider 的毛病有关的 bug 发生在对齐刻度的行为上。 BSlider 的构造函数把次要刻度设为5,把主刻度设为 20 —— 相对于默认尺寸 100 来说这两个值是合理的。但是,当用 1-10 的范围创建滚动条时,却看不到次要刻度,因此只能把主刻度间距值设为 1。结果产生一个刻度值为 1-10 的滚动条,而且只停留在 1 和 6 处;对齐刻度的行为妨碍了采用其他的值,因为对齐到了次要刻度而不是主刻度。

  虽然这个问题源自 JSlider 的实现,但却在 Buoy 的默认行为中发生了,在即将发布的 1.4 发行版中会修复它。

  注意,这对我是个问题的惟一原因是,示例程序要不断地更新一些滚动条的范围。例如,如果有一个线条区段,只允许进行最多 50 次迭代,那么要在滚动条上标上每个数字的工作量可能有点多。另一方面,如果只允许少数迭代,那么遗漏某些数字看起来就不好了。在一个滚动条范围不常更新的界面,还是很方便的。

  菜单快捷键

  Buoy 用字符或键盘事件(KeyEvent)方便地为菜单快捷键提供了构造函数。在第一次测试它时,我没法让它工作。看起来必须使用小写字母;但在调用构造函数时必须用 'W' 代替 'w',如清单 7 所示。

  清单 7. 添加 close 菜单项
mi = new BMenuItem("Close", new Shortcut('W'));
mi.setActionCommand("close");
mi.addEventLink(CommandEvent.class, this, "menuEvent");
fileMenu.add(mi);
  这样可能必须要处理 Java 5.0 SDK 与 1.42 不小心模糊重复的地方。表面来看,如果把大写字母传递给构造器,所做的事情正与期望的一样。底层的问题 —— JVM 要用哪一套键或修饰符来表示 Ctrl-? —— 还需要一个小的自由的库才能完全解决。

  文件选择

  出于一些不明显的原因,在 Mac 系统上启动新的文件选择器时,Buoy 默认启动的是根目录。我做了一个详尽的 bug 报告,是关于它看起来是怎样遗漏大量文件的,但是随后我就认识到我已经把我的主目录从 /Users/seebs 移动到了 /Volumes/Home/seebs,而文件选择器确实显示了磁盘上的东西。分数:Buoy 1,Seebs 0。

  我仍然想知道为什么它要从文件系统的根开始。这也许是 JVM 的 Mac 实现的毛病。

  结束语

  Buoy 遵循著名的古老的 UNIX 哲学:百分之十的工作解决百分之九十的问题。Buoy 并不想为所有人解决所有的问题,但是它可以完成界面用户或设计师需要的大部分工作。它拥有可能是最好的许可条款,而且还在不断发展。最好的是,如果发现它不能让您做自己确实需要做的事情,您可以随心所欲地研究它、修改它,要么修改 Buoy 的源代码,要么调用 getComponent() 并编写自己的 Swing 代码。

  如果觉得较大的 UI 工具包太可怕,那么 Buoy 是个不错的选择。它可以让简单的 UI 继续简单,把复杂的代码留到需要的时候。在实践中,对于少数 Swing 比 Buoy 有优势的情况,直接在 Buoy 构建的程序中编写少数代码就能处理。这是一个让我值得花时间用 Java 进行 UI 编程的工具包。