Undo(撤销)与Redo(重做)详解

UndoableAction可撤销的操作和UndoManager撤销管理器这两个类协同配合可实现程序中的撤销与重做功能。

UndoableAction是抽象基类,使用时需派生该类的子类,每个子类代表程序所执行的相同逻辑、相同数据类型、相同功能的单次操作(这一点非常关键)。可以这么理解:每个UndoableAction派生类的对象就是一次需要撤销和重做的操作。通常,该类的构造参数为内容组件类的指针和需要完成本次操作所需的具体数据。利用内容组件类的指针调用其功能性函数执行具体的操作,而本次操作所需的数据又可以作为撤销时的恢复数据。也就是说,要实现撤销与重做功能,关键有二:

 内容组件类不直接调用自己的功能性函数,功能性函数在UndoableAction派生类中的perform()或undo()函数中,由内容组件的指针所调用。基本上,每个可撤销和重做的操作均需定义一个对应的UndoableAction派生类。可将该类声明为内容组件的友元类。

 UndoableAction类的对象由UndoManager类的perform()函数来创建和管理,客户端永远无需显式调用UndoableAction类的任何成员函数。也就是说,内容组件类需持有UndoManager对象,执行某个需撤销和重做的操作时,UndoManager对象调用其perform()函数,该函数的参数即为功能性操作所对应的UndoableAction派生类指针。

下面是两个UndoableAction派生类的完整代码。这两个类,一个用于撤销/重做绘制直线,一个用于撤销/重做绘制矩形。这两项操作所需的数据以及所对应的内容组件类中的功能性函数均不同。(网站发布,代码及详解略)


UndoManager撤销管理器类用来管理UndoableAction对象,并提供实现撤销与重做的接口。用法如下:

 需实现撤销和重做的内容组件类中,声明、创建并管理本类的对象。项目较复杂,多个编译单元中均有撤销重做功能时,可声明一个全局性的UndoManager指针,在主程序类的初始化函数中创建之。或者写一个单例类,其他编译单元可随时获取该单例类所提供的UndoManager堆对象(指针)。

 内容组件类执行某个需撤销的操作时,不直接调用相关的功能性函数,而是由本类对象调用perform()函数。该函数的1参为UndoableAction类的指针,可直接new一个。2参为文本描述,默认为空。

 内容组件类每次执行相关操作,均需使用本类对象显式调用perform(),而每次调用perform()均创建一个全新的UndoableAction堆对象。可理解为:此处所创建的UndoableAction堆对象代表一次具体的操作。

 需完成撤销功能时,使用本类对象调用undo(),需重做时,则调用redo()。

 如需判断当前是否可撤销、是否可重做,则调用本类的canUndo()和canRedo()。

 操作和操作序列不是一回事。UndoManager的撤销与重做以操作序列为单位,而不是以perform()单次操作为单位,这一点很迷惑人。只有显式调用beginNewTransaction(),才将一到多个操作合并为一个操作序列。也就是说:两次调用 beginNewTransaction()之间的所有perform(),自动组成一个操作序列。

 每次调用perform()之前或之后,显式调用beginNewTransaction(),可将本次perform()作为一个操作序列。也可以这么解释,每次显式调用beginNewTransaction(),均产生一个新的操作序列。

 本类继承自ChangeBroadcaster可变生成器类, 调用perform(), undo(), redo(), clearUndoHistory()等函数后均自动产生消息(这些函数的内部调用了sendChangeMessage()函数)。因此,可绑定ChangeListener可变捕获类以让其捕获并处理本类所产生的消息。比如:判断撤销与重做按钮是否可用,触发重绘,关联改变其他事物等等。

下图是示例程序的运行界面,主界面即内容组件,允许用户以鼠标拖拽的方式绘制直线或矩形,所绘制的每一条直线和每一个矩形均需实现撤销与重做功能。绘制直线还是绘制矩形,由界面中的Radio按钮的状态来确定,界面中另有撤销与重做按钮,点击后实现绘制的撤销与重做。

undo-demo

其具体实现有六个步骤:(网站发布,代码及详解略)。