图 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 类)去关心那些它根本不知道的属性。
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),或者是需要重绘的通知,如清单 5 所示。
清单 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 是不公平的。
一个更微妙、也与底层 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 编程的工具包。
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),或者是需要重绘的通知,如清单 5 所示。
清单 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 是不公平的。
一个更微妙、也与底层 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 编程的工具包。