使用 Swing 进行动态界面设计

通往 Swing API 外部领域的旅程

Swing UI 工具包使动态更新用户界面以响应事件或用户操作成为可能(虽然并不总是很容易)。本文回顾了一些用于构建能够动态更新的 UI 的常用方法、其间可能遇到的一些陷阱和一些有助于您决定何时它是完成工作的正确方法的原则。

Peter Seebach (dw-java@seebs.net), 作家, 自由作家

Peter SeebachPeter Seebach 确信在某个地方的四周存在 File 菜单。他使用计算机的时间比他编写程序的时间要长一些,他仍然认为 GUI 有一些新奇。



2006 年 6 月 30 日

Swing 工具包提供各种用于创建用户界面的工具和几乎令人眼花缭乱的选项,这些选项用于在程序生存期间修改界面。小心地使用这些功能可以导致界面能够适应用户的需要并简化交互过程。粗心地使用同样的功能可以导致非常混乱或彻底不可用的程序。本文介绍动态 UI 的技术和体系,并提供有关构建有效的界面的帮助。您将修改随 Sun JDK 一起提供的基于 SwingSet2 示例应用程序的源代码(参见 参考资料);此应用程序的 UI 使用许多动态的特性并且可以作为理解它们的极好的起点。

禁用小部件

动态 UI 的最简单形式是使不可用的菜单项或按钮变灰的 UI。禁用 UI 小部件与禁用所有小部件的方法都是相同的。setEnabled() 函数是 Component 类的一个功能。清单 1 显示了禁用按钮的代码:

清单 1. 禁用按钮
button.setEnabled(false);

即使像把不可用的菜单选项或对话框变灰这样简单的操作都涉及到是否方便用户使用的权衡。尽管变灰的按钮立即通知用户不能执行特定的操作,但它没有告知用户为什么。这对于不理解原因的用户可能是一个问题(参见 一般原则)。

正如您看到的,十分简单。关键问题是何时应该 启用或禁用一个按钮。通常的设计决策是当按钮不可用时禁用它。例如,当一个文件从上一次保存以来还没有被修改时,很多程序禁用 Save 按钮(以及任何相应的菜单项)。

关于禁用按钮的重要警告是要记住在适当的时候重新启用它们。例如,如果在单击按钮和按钮的动作完成之间有一个确认步骤,即使确认失败也应该重新启用按钮。


调整范围

有时,应用程序需要动态地调整数值小部件的范围,例如 Spinner 或者 Slider。这可能比它看起来要复杂许多。特别是 Slider 有二级功能 —— 刻度、刻度间隔和标签 —— 这些可能需要随着范围的调整而加以调整以避免灾难发生。

SwingSet2 示例没有进行任何一项调整,所以您需要通过把 ChangeListener 连接到一个可以修改其他滑块的滑块来修改它。输入新的 SliderChangeListener 类, 如清单 2 所示:

清单 2. 更改滑块的范围
class SliderChangeListener implements ChangeListener {
       JSlider h;

       SliderChangeListener(JSlider h) {
              this.h = h;
       }

       public void stateChanged(ChangeEvent e) {
           JSlider js = (JSlider) e.getSource();
           int i = js.getValue();
           h.setMaximum(i);
           h.repaint();
       }
}

当创建第三个水平滑块时(最初示例中的滑块在每个单位处带有标记,在 5、10 和 11 等处带有标签), 另外还创建了一个新的 SliderChangeListener,它把滑块作为构造器参数传递。当创建第三个垂直的滑块(范围从 0 到 100)时,新的 SliderChangeListener 作为变更监听器添加到它。这大致按预期的那样工作:调整垂直滑块改变了水平滑块的范围。

不幸的是,刻度和标签根本不能很好地工作。当范围变得不是太大时,每五个刻度处的标签能正确地工作,但是刻度 11 处的额外标签很快成为一个可用性问题,如图 1 所示:

图 1. 一起运行的标签
一起运行的标签

更新刻度和标签

明显的解决方案是,只要滑块的最大值被更新,就在水平滑块上简单地设置刻度间隔,如清单 3 所示:

清单 3. 设置刻度间隔
// DOES NOT WORK
int tickMajor, tickMinor;
tickMajor = (i > 5) ? (i / 5) : 1;
tickMinor = (tickMajor > 2) ?  (tickMajor / 2) : tickMajor;
h.setMajorTickSpacing(tickMajor);
h.setMinorTickSpacing(tickMinor);
h.repaint();

目前清单 3 是正确的,但是它没有引起画在屏幕上的标签的任何变化。必须使用 setLabelTable() 分别设置标签。 添加额外一行修复它:

h.setLabelTable(h.createStandardLabels(tickMajor));

这仍然出现在刻度 11 处存在最初设置的标签这样的错误。当然本来的意图是想在滑块的最右端始终有一个标签。可以通过删除旧的标签(在设置新的最大值之前)然后添加一个新的标签达到这一目的。此代码 “几乎” 可以工作:

清单 4. 替换标签
public void stateChanged(ChangeEvent e) {
       JSlider js = (JSlider) e.getSource();
       int i = js.getValue();

       // clear old label for top value
       h.getLabelTable().remove(h.getMaximum());

       h.setMaximum(i);

       int tickMajor, tickMinor;
       tickMajor = (i > 5) ? (i / 5) : 1;
       tickMinor = (tickMajor > 2) ? (tickMajor / 2) : tickMajor;
       h.setMajorTickSpacing(tickMajor);
       h.setMinorTickSpacing(tickMinor);
       h.setLabelTable(h.createStandardLabels(tickMajor));
       h.getLabelTable().put(new Integer(i),
       new JLabel(new Integer(i).toString(), JLabel.CENTER));
       h.repaint();
}

如果我已经告诉过您一次,那么我就已经告诉您两次了。

我使用几乎 的意思是,虽然清单 4 中的代码删除了刻度 11 处的标签,但是它没有在刻度 i 处添加新标签;相反,只能看到间隔为 tickMajor 的标签。首先此解决方法相当令人讨厌:

清单 5. 强迫显示更新
h.setLabelTable(h.getLabelTable());

这个看起来无意义的操作实际上有重大的作用。每当设置标签表时就生成滑块的标签。没有为了修改对表进行特殊回调,所以添加到表中的新值不必产生效果;很显然,清单 5 中的空操作具有使 Swing 知道它必须更新显示的副作用。(以免您认为这是我自己发明的,请注意最初的 SwingSet 代码包括这样一个调用。)

这只留下了一个问题。标签出现在滑块的末端这个非常合理的期望有时使两个标签互相直接相邻,乃至重叠,如图 2 所示:

图 2. 滑块末端的重叠标签
滑块末端的重叠标签

很多解决此问题的方法都是可行的。编写自己的代码以使用值来填充标签表并停止以前的序列以便序列中的最后标签与滑块的末端有一些隔离。我将把这个作为作业留给您。


更新菜单

在许多情况下,为了启用和禁用菜单项而限制菜单修改是很实际的。此方法容易受到用于禁用项的常规警告的影响:避免由于偶然地禁用重要项而使程序处于不可用状态。

添加或删除菜单项或子菜单也是可能的。修改 JMenuBar 没有这么容易;没有从工具栏上删除和替换单个菜单的接口。如果您想修改工具栏(而不是向最右端添加菜单),需要制作一个新的工具栏并用它替换旧的工具栏。

修改单个菜单会立即生效;您不必在将菜单连接到工具栏或另一个菜单之前构建一个菜单。当需要修改菜单选项的选择时,最容易的方法是修改选定的菜单。您可能仍然想添加或删除完整的菜单,并且这么做并不是特别难。清单 6 显示一个将菜单插入到菜单条中给定索引前的方法的简单示例。 此示例假定要替换的 JMenuBar 连接到 JFrame 对象,但是任何能使您获得和设置菜单条的东西的工作方式都是一样的:

清单 6. 把一个菜单插入到菜单条中
public void insertMenu(JFrame frame, JMenu menu, int index) {
       JMenuBar newBar = new JMenuBar();
       JMenuBar oldBar = frame.getJMenuBar();
       MenuElement[] oldMenus = oldBar.getSubElements();
       int count = oldBar.getMenuCount();
       int i;

       for (i = 0; i < count; ++i) {
              if (i == index)
                     newBar.add(menu);
              newBar.add((JMenu) oldMenus[i]);
       }
       frame.setJMenuBar(newBar);
}

上面的代码我不是开始时就试图编成这样;这是最终的版本,已经很好地修复过了所以它能够运行并反映一些有趣的怪事。初看起来,实现此功能的明显方法似乎应该是使用 getComponentAtIndex(),但是这种方法已经受到了反对。幸运的是,getSubElements() 已经足够好。为 newBar.add() 而进行到 JMenu 的强制类型转换可能是安全的,但是我不喜欢这样。getSubElements() 接口不仅对菜单条而且对菜单进行操作,菜单可能具有几种类型的子元素,JMenu 是可以添加到 JMenuBar 的惟一元素。所以必须把元素强制转换为 JMenu 以把它传递到 JMenuBar.add() 方法。不幸的是,如果将来的 API 修订版允许将除 JMenu 类型之外的元素添加到 JMenuBar,就不再需要把返回的元素强制转换 JMenu了。

清单 6 中的代码反映了另外一个相当微妙的界面怪事;菜单数必须提前缓存起来。当把菜单添加到新的菜单条时,它们从旧的菜单条中被删除。虽然看起来相似,但是清单 7 中的代码不能工作,因为循环提前结束了:

清单 7. 循环结束太早
// DOES NOT WORK
for (i = 0; i < oldBar.getMenuCount(); ++i) {
       if (i == index)
              newBar.add(menu);
       newBar.add((JMenu) oldMenus[i]);
}

界面保持一致性对用户有益;特定的菜单应该总是位于相同的位置。为了用户的方便,应该把可能改变的菜单放置在菜单列表的右端,把不会改变的菜单放在左边的固定位置。同样地,当可能的时候,把菜单项放置在菜单中的相同位置。变灰的菜单项比时而添加时而删除的菜单项对用户具有更少的干扰性,因为菜单中的其他菜单项不会来回变动位置。

清单 7 中的循环只复制一半数量的菜单。例如,如果开始菜单条上有 4 个 菜单,它复制前面的两个菜单。复制完第一个菜单以后, i 的值为 1 并且 getMenuCount() 返回 3;在复制完第二个菜单以后,i 的值为 2 并且 getMenuCount() 返回 2,因此循环结束。我没有找到任何介绍通过向菜单条添加菜单从而从另一个菜单条删除菜单这样的特性的文档,因此可能不是有意这样。但是,它很容易达到这个目的。

从菜单条删除菜单稍微容易一些;只是把所有其他的菜单从旧的菜单条复制到新的菜单条,就完成了删除菜单。很容易!

如果界面使用了很多动态菜单更新,创建一组菜单条并在它们之间切换而不是一直动态地更新它们也许会更好一些。但是,如果如此快地改变菜单,可能会使用户完全发疯。

勘误:在本文的草稿阶段,我忽略了 JMenuBar 类的继承方法的列表。其实,它有 remove 和 add 方法可以用来在指定的索引处进行删除和插入。另外一个教训是:查看继承的方法而不仅仅是特定于类的方法。


调整窗口大小

所幸的是对于大多数情况,窗口大小调整是自动进行的。但是需要考虑调整大小产生的一些影响。在非常小的窗口中,按钮条、菜单条和类似功能可能变成有问题的。管理程序自身的图形面板需要响应调整大小事件。让 Swing 处理对 UI 元素的包装,但是要密切注视组件的大小;不要获取一次组件的尺寸然后就一直使用这些值。

更微妙的地方是,一些设计决策(例如滑块上刻度的密度)可能被适度地更新以响应窗口大小调整事件。100 像素宽度的滑块不能具有像 400 像素宽度的滑块那样多的可读标签。您可能想通过添加全新的有用功能来让 UI 更进一步用在大型显示器上。

但是,在多数情况下,可以忽略窗口大小调整。您不应该做的是不必要地阻止或重写它。布局代码中的窗口一侧的便捷工具不是必需的。最小的窗口大小可能是无可厚非的,但是要让人们能把窗口调整到他们所需要的大小。


一般原则

Swing 工具包在 UI 设计方面提供了很大的灵活性。如果小心地使用,动态更新界面的选项能够极大地简化该界面;例如,只有应用菜单的选项时,用户才能容易地显示菜单。

不幸的是,一些 API 的特性可能使此方法产生一些离奇的行为,并且副作用和相互影响并不总是像您期望的那样记录下来。如果您有使用动态界面的想法,就要准备在调试上花费一些额外的时间。您可能从 Swing 库的困境中走出来并发现自己需要处理出人意料的行为和/或 bug。

不要让缺乏明显的实现让您气馁。像本文的 JMenuBar 示例所显示的,即使当 API 不支持某个任务时,您也能自己实现它,虽然有一点间接。

尽量不要走极端。当动态 UI 让用户清楚它们的固有限制时,它们才能最好地发挥作用。理想的情况是,用户甚至可能不会注意到界面变化。如果他们能够使用程序的 Object 菜单的惟一时刻是当他们使某个对象被选择时,那么其余的时间他们将不会介意不存在该菜单。

另一方面,如果存在这种可能性:用户不能猜测出一个选项不可用的原因,这时让用户尝试操作并获得包含信息的消息也许会更好。这对于一些操作尤其重要。如果保存选项被禁用,而我想保存数据,那么这不会发生作用。程序可能认为已经保存了数据,但是为什么不让我无论如何都保存它呢?如果存在不能保存文件的特殊原因,我可能想知道是什么原因。

尽管研究了很多年,界面设计在很多方面仍旧是一个较新的领域,只进行了很少的试验。动态改变 UI 是一个很好的特性,能够使 UI 更清晰、更简单和反应更迅速。添加动态特性需要从几分钟的工作到大量时间的花费不等。

参考资料

学习

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=124527
ArticleTitle=使用 Swing 进行动态界面设计
publish-date=06302006