为你的Swing应用增加undo/redo功能
理解Swing GUI组件如何最大限度的使用Command模式获得undo/redo功能
By Tomer Meshorer
翻译:flyingbug
第一部分:概述:
大多数用户会犯错,而作为开发者,我们要通过在用户接口中加入undo功能而尽量的帮助他们简单优雅的从错误中恢复。Swing中新的undo/redo机制可以使我们做到这一点。这篇文章描述了如何在java应用程序中加入undo/redo能力。它通过介绍Command模式以及Swing对该模式的支持,解释了undo/redo的机制。另外,作者提供了一个关于Swing的undo功能的介绍,并使用apple写了一个简单的例程。
在不久以前,Sun介绍了JFC,一个全面的UI组件集和基础库,可以使java开发者使用更灵活的界面风格来开发他们的程序。这就是Swing组件库,一个非常丰富的轻量级UI组件库。
历史上,已经有很多应用程序框架通过Command模式建立undo/redo机制。这也是我们要做的。我们将讨论Command模式并且描述它是怎样支持undo/redo系统的设计的。我们将检验java对该模式的支持并看看Swing的undo包如何使你可以建立一个完整的undo/redo机制。
一个undo/redo机制需要的条件
Undo允许用户纠正他们的错误并且无风险的尝试应用程序的不同方面。
至少,一个undo/redo功能应该为用户提供以下功能:
- 取消此前最后执行的动作
- 重做最近取消的动作
- 取消或重做最近的几个动作(有更好,但不是必须)
为了设计这样的一个功能,我们必须将用户的操作看成是独立的原子操作(自含操作:知道怎样undo/redo他们对系统状态的改变)以便可以存储下来为undo/redo提供支持。我们将使用设计模式来解决这个问题,尤其是Command模式。如果你对设计模式非常的的熟悉(尤其是Command模式以及java对它的支持),那么你可以直接阅读第三部分。
Design patterns
设计模式:
In a nutshell,设计模式鼓励在特定的上下文中复用成功的设计解决方案,以便设计更强健的系统。
你可能会奇怪我为什么会在一篇讲述undo/redo机制的文章中讨论设计模式,这很简单,为了更好的理解这个技术的实现的文章,我相信理解undo/redo背后的设计的本质是非常有用处的。
按照设计模式的解释:根据现在著名的GOF的说法,面向对象可复用软件的基本元素,各种模式的本质包括:
- 目的 -- 这个模式的设计目标
- 适用性 -- 适用于什么情况下
- 结构 -- 设计模式的组成元素和它们之间的关系
- 效果 -- 各种方案的权衡
目前存在许多不同的模式,但是我们这里只讨论与undo/redo机制实现有关的:Command模式。
第二部分:Command模式:
根据设计模式的解释,Command模式的目的是:
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
另外:Command模式有两个别名:Action(动作)、Transcation(事务)
让我们来看看它如何工作
这个模式的核心是Command接口,它定义了execute()方法。
001 public interface Command {
002
003 public void execute();
004
005 }
所有的用户动作(比如插入文本,剪切,粘贴;每一个用户能够进行的操作)都封装成一个Command对象。一个Command对象是一个类,用于实现用户请求的操作。举个例子,在一个文本编辑器中当用户输入一些文字,我们并不直接将这些文字加入到UI界面的文档中去;而是创建一个InsertTextCommand对象来实现这个操作。
所有的Command对象都必须实现Command接口。这个接口声明了一个execute()方法,这个方法将调用实际的操作方法,下面的代码演示了用命令模式实现一个文本编辑程序的剪切操作。
001 public class CutCommand implements Command{
002
003 private TextArea target_;
004
005 public CutCommand(TextArea area) {
006 target_ = area;
007 }
008
009 public execute() {
010
011 // target_.cut();
012 int startPos = target_.getSelectionStart();
013 int endPos = target_.getSelectionEnd ();
014 String text = target_.getText ();
015 target_.setText (text.substring (0, startPos) + text.substring (endPos));
016
017 }
018
019 }
当一个CutCommand对象被创建,它的操作对象被作为参数传入构造函数 -- 在这个例子中,是一个文本域。为了给这个文本编辑器构建一个cut操作,我们将这个CutCommand对象添加到编辑器的Cut菜单项当中,并将文本域作为它的操作对象。当Cut菜单项被选中,这个Command对象的execute()方法被自动调用然后cut操作被执行。
从设计的观点看,Command模式最重要的事情就是它将每个用户操作都视为一个对象。如果我们添加unexecute()和reexecute()方法到这些对象中(确切的说,是添加到Command接口中),我们就能支持撤销和重做操作,这给我们提供了一个基本的undo/redo机制。
当然,在这个机制当中添加undo和redo能力需要更”聪明”的命令。一个命令不但要能够调用它的操作对象的操作,而且还应该可以准确取消和重做这些操作。举个例子:剪切命令对象将储存所选择的字符串和剪切起始的位置,以便undo()操作被调用时它能够将所选择的内容放回到文本域中。
对每一个操作都包装成一个单独的对象意味着我们能够很容易的支持多级undo/redo操作:我们在一个历史列表中存储用户执行的每个操作。当用户选择undo,我们执行列表中当前项目的undo操作,然后将列表中当前项目设为前前一个项目。对于redo操作,我们执行列表中当前项目的下一个项目并且将此项目变为当前项目。如果用户在一系列undo操作后执行了一个新的操作,我们清除列表中它之前的项目以便去掉它们redo操作。
不管怎么说,因为命令模式只在创建时添加到菜单项当中,我们必须提供手段将其复制并存储在历史列表当中。这可以通过两个方法做到:
- 使用Prototype模式 -- 为每一个Command对象定义一个clone()方法,当命令被执行之后,它被克隆到历史列表中
- 将命令操作的结果从执行中分离处理 -- 当命令操作被执行,它创建一个”结果”对象来存储命令操作执行的结果。然后将这个对象存储到离职列表当中。
Swing的设计者选择了第二个方法。
Java对Command模式的支持 -- 事件监听器(event listeners)
Java对Command模式最直接的应用就是AWT的代理事件机制。在这个框架中,Command对象实现AWT监听器接口而不是实现Command接口。为了将Command对象和一个AWT组件结合起来,我们只需要简单的将其注册为一个事件监听器。这个组件依赖于监听器的接口而不关心你如何实现它;也就是说,组件不关心实际的命令操作是什么,它只是去调用它。
举个例子,为了支持剪切操作我们应提供一个ActionListener并实现它的actionPerformed()方法(而不是execute()方法)去执行剪切操作,并且将这个ActionListener添加到剪切操作的调用方法中(MenuItem addActionListener())
不幸的是,这个系统不支持简单的undo/redo操作。为了支持undo,每个操作必须导致一个单独的Command对象被调用。这些对象能够保存它们所执行操作的结果信息。而在AWT框架中,只有一个简单的Command对象被添加到每个操作中,然后被用户操作反复的调用。
下一部分将讲述在Swing中实现undo/redo功能...