Redo 和 Undo 机制
Andy Liu
2007-10-11
提到应用程序的Redo和Undo机制,大家马上会想到Command模式,的确任何一个软件在响应用户命令和操作时,都是用了Command模式将这些请求封装成一个命令对象,命令对象含有状态改变的相关信息,此外命令对象所属的类至少提供了Do和Undo方法。在此基础上,我们再使用一个队列来存储这些命令对象,也就是命令的历史记录。此外,还有一个指针来用以维护该队列。每当执行一个用户的请求时,我们先把命令对象加到队列尾部,指针指向它的下一位,然后调用其Do方法,对于应用程序来说,它并不知道该请求是做什么的,也不知道具体怎么做,只知道我现在收到一个请求,然后转发个command对象去做就OK了。当用户请求Undo时,向后遍历,指针后移,调用所指对象的Undo方法;当用户请求Redo的时候,向前遍历,指针前移,调用其指对象的Do方法。注意每当新加一个命令对象的时候,要检查是否已经有了Undo操作,有了的话,所有已经做过Undo操作的那些命令对象已经已无意义,应丢弃掉。就是删除从指针所指的位置到队尾的一系列命令对象。这就是最简单得Redo和Undo 机制。
Command 模式
在讨论Redo和Undo机制前,我们先来谈谈Command模式。
简单的说,Command模式就是将请求本身封装成一个对象,这样可以给客户提供可参数化的用户接口,比如一个菜单项可在不同的时间加载不同的Command对象,这就是一个可动态改变的菜单;还有对请求排队或记录日志;以及可撤销操作。这一模式的关键是一个抽象的Command类,它定义了一个可执行操作的接口。其最简单的形式是一个抽象的Execute操作。具体的Command子类将接收者作为其一个实例变量,并实现Execute操作,指定接收者采取的动作。而接收者有执行该请求所需的具体信息。
如果Command提供方法逆转它们操作的执行(例如Undo操作),就可支持取消和重做功能。为达到这个目的,每一个具体的类可能需提供额外的状态信息。这个状态包括:
n 接收者对象,它真正执行处理该请求的各操作。
n 接收者上执行操作的参数。
n 如果处理请求操作会改变接收者对象中的某些值,那么这些值夜必须先存储起来。接收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态。
若应用只支持一次取消操作,那么只需存储最近一次被执行的命令。而若要支持多级的
取消和重做,需要有一个已被执行的命令的历史列表,该列表的最大长度决定了取消和重做的级数。历史列表存储了已被执行的命令序列。向后遍历该列表并逆向执行命令是取消他们的结果;向前遍历并执行命令是重执行他们。
实际应用中的Redo和Undo机制
在实际的软件中,有不同的需求,比如,将一个物体从一个位置移动到下一个位置,这是一个操作。细化移动这个操作,鼠标Down事件,Move事件,Move事件,Move事件,。。。鼠标Up 事件。每一步对应着一个状态改变。有的时候用户关心物体是怎么详细地移动的,更多得时候用户只关心物体前后位置的变化。同样,我们在画一条线的时候,画第一个点,第二个点,第三个,。。。第N个点,最后连成一条线,然后画第二条线,等等。大部分情况下用户并不需要关心这条线是怎么画出来的,不满意的时候,要求直接销该条线。因此,在这种情况下,Redo和Undo机制就需要支持将多个微小的或者连续的操作压缩成一个大的操作。
怎样解决上面的需求?有两种方法。一是将用户的请求与一个大的操作对应起来,这个大的操作或者变化是由一系列相关的小的操作或者变化组成的。我们称之为Group。只要是属于同一个Group中的操作,将会被一起执行,用户是看不到每一个细小的操作的;另一种方法是选择一些关键的操作或变化,这些操作或变化对用户来说是很明显的,我们标记为discrete( 或者non-dynamic),比如物体移动中的Mouse Down操作,相对于这些关键的操作,就是那些连续的很小的变化或操作,比如物体移动中的Mouse Move操作,我们标记为dynamic。在响应Do或Undo请求时,让dynamic操作或变化附加到non-dynamic操作或变化中,即每完成一个non-dynamic操作或变化,都需要把紧跟着的那些dynamic操作或变化做一次,直到碰到下一个non-dynamic为止。比如物体移动时,完成Mouse Down,然后接着完成Mouse Move, Mouse Move,Mouse Move 。。。Mouse Up,直到碰到Mouse Down。这就算完成了一次用户的请求。
本文给出的Undo机制同时采用了上面两种方法,不同的是被标记的是Group,而不是每一次细小的变化或操作。事实上,每一个Group只包含一个小变化或操作时,就等同于标记的标记的微小操作。
由Redo和 Undo的工作原理及用户的需求我们可以得到Redo和Undo机制的大致轮廓。用一个类来抽象一微小的状态的变化,这个类叫做command,其重要作用就是,它定义了commandDoIt()和commandUndoIt()接口方法,派生类实现这两个接口,来完成从当前状态变化到下一状态和恢复到上一状态;用一个类来表示将一组微小的连续相关的状态变化经压缩后的对应着一个有着质变的变化,这个类叫做Group,它一般对应着用户在编辑菜单栏下的一次撤销和重做操作;还有一个类,真正实现Undo机制的类,它是一个容器,用来存储一系列Group,同时也是一个桥梁,将物体模型或者应用软件与Undo机制连接起来;最后,为了使之更好的工作,我们还需要用一个类来抽象状态,描述模型当前是出于Redo或者Undo状态,或者是否有效,这个类叫做State.
现在我们可以画出Redo和Undo机制的类图,如下图所示。下面来具体分析每个类的职责。
1. Class State
用来跟踪命令对象的状态。比如在该命令对象上最后执行的是Redo操作还是Undo操作。该命令对象是否有效。
常用的方法:
lastWasDoIt();
lastWasUndoIt();
isValid();
stateDone();
stateUndone();
stateInvalid()
2. Class Command
在Undo机制中代表物体模型的最底层的状态改变。它继承于Class State,是一个纯虚类。定义了两个接口:
commandDoIt()
commandUndoIt()
每一个Command对象被Group对象管理,而每一个Group对又象被Manger对象管理。一个Manget包含一系列的Group实例,而一个Group又包含了一系列的Command实例。
当每一个Command被构造的时候,需要指出这个Command属于哪个Group对象。这样,Command就被添加到特定的Group中,指定的Group负责Command的销毁。
常用方法:
l
Command( Group *group, const char *name )
构造函数。需给出所属的Group。若Group不位空的话,该Group将负责Command的内存管理。若Group为空的话,表示该Command不属于任何Group,只是简单的使用其doIt()方法。
l AddToGroup(Group *group)
内部使用。
l Virtual bool commandDoIt(void) = 0
l Virtual bool commandUndoIt(void) = 0
两个纯虚方法。每一个从Command派生出来的具体类,在这两个函数里真正实现状态变化。显然它们是依赖于具体的应用程序或者物体模型的。每一个具体的Command子类,一般存储命令的接收者对象,以及与状态变化相关的信息。
l bool doIt(void)
l bool undoIt(void)
当客户代码创建了一个Command,必须调用doIt()方法,该command才被执行。而undoIt()方法则是Undo机制的一部分。
用户请求撤销或者重做操作的时候,这两个方法被Manager所调用。通过调用doIt()或者undoIt(),来执行commandDoIt()或者commandUndoIt(),该请求就被响应。
代码:
doIt()
{
bool ok = isValid();
if (ok)
{
if (!lastWasDoIt())
{
ok = commandDoIt();
}
stateDone();
if (!ok)
{
stateInvalid ();
}
}
return ok;
}
l Virtual int getNumBytesUsed()
l Int getID()
3. Class Group
包含一系列Command的容器类。由Manager来管理和分配。每一个Undo Group包含0个或多个Command实例。理论上说,一个用户操作对应着一个Group,每一个操作是由一系列的不同的微小的状态变化组成的。每一个微小的状态变化被实现成一个Command。
l Group( Manager *manager );
构造函数。需指出所属Manager,manager负责它的内存管理。
l bool doIt()
l bool undoIt()
内部方法。只被Manager所使用。
Bool doIt()
{
If( !isValid() )
{
Return false;
}
If( lastWasDoIt() )
Return true;
Bool ok = true;
Int size = m_commandList.size();
For( int i = 0; i < size; ++I )
{
Ok = m_commandList[i]->doIt();
If( !ok )
{
stateInvalid();
return false;
}
}
stateDone();
return ok;
}
Bool undoIt()
{
If( !isValid() )
{
Return false;
}
If( lastWasUnDoIt() )
Return true;
Bool ok = true;
Int size = m_commandList.size();
For( int i = size - 1; i >= 0 ; --i )
{
Ok = m_commandList[i]->undoIt();
If( !ok )
{
stateInvalid();
return false;
}
}
stateDone();
return ok;
}
l void addCommand( Command *cmd )
同样是内部方法。将一个Command添加到Group中,同时给该Command分配一个唯一的标志符。最简单的添加一个Command的方法是在其构造的时候把Group传进去。
客户代码不应该直接调用该方法。
l int newID();
内部方法。为每一个Command分配一个唯一的标志符。
l void setGroupDynamic( bool )
l isGroupDynamic()
当正在进行do或者undo操作的时候,所有的动态的连续的group(dynamic)被附加到前面的第一个非连续的(non-dynamic,discrete)的group上,当成一个整体的group进行。这是为了解决前面移动物体的一个懒惰的实现。Mouse down是一个discrete的group,而mouse move/up 则是dynamic的group。
4. Class Manger
实现Undo机制的核心类。它是应用程序或者物体模型与Undo机制间的桥梁。同样它也是一个容器类。每一个模型对应着一个Manager,而一个Manager保存着一系列的Group,每一个Group对应着用户可请求的一个操作。
Manager类有两个重要的接口: doIt() 和 undoIt()。用户通过这两个接口来请求模型回到前一状态或是重新回到当前状态。Manager还负责维护保存请求命令的历史列表的长度,内存的大小。
常用方法:
l Bool undoIt()
使Manager 撤销当前的Group.如果在进行一系列的do操作后进行的第一个undo操作时,列表中的最后一个Group被撤销;否则,则前一个Group被撤销。
当进行撤销的过程中,若正被撤销的这个Group是dynamic类型的,则前一个Group也要被撤销,直到碰到一个non-dynamic的Group,撤销它,然后停止。
返回值表示当前的动作执行成功与否。若失败,Manager则把这个Group标记为无效的。
bool Manager::undoIt()
{
bool ok = true;
if ( m_currentGroup != m_groupList.begin( ))
{
int numCommandsUndone = 0;
do {
--m_currentGroup;
numCommandsUndone +=
(*m_currentGroup)->getNumCommands();
ok = (*m_currentGroup)->undoIt();
if (!ok)
currentGroupIsInvalid ();
} while ( ok
&& m_currentGroup != m_groupList.begin()
&& (*m_currentGroup)->isGroupDynamic()
);
// If there were no commands undone, try again
if (numCommandsUndone == 0)
ok = undoIt();
}
else
{ }
return ok;
}
l Bool doIt()
同doIt()一样,重做当前的这个Group时,要判断后面的那个Group是不是dynamic类型,若是的话则需继续重做那个Group,直到碰到non-dynamic类型的Group为止。
Bool Manager::doIt()
{
Bool ok = true;
If( m_currentGroup != m_groupList.end() )
{
Int numCommandDone = 0 ;
Do{
numCommandDone += (*m_currentGroup)->getNumCommands();
ok = (*m_currentGroup)->doIt();
if( ok )
++m_currentGroup;
Else
currentGroupIsInvalid();
}while( ok
&&m_currentGroup != m_groupList.end()
&&( *m_currentGroup )->isGroupDynamic()
);
If( numCommandDone == 0 )
ok == doIt();
}
Else{}
Return ok;
}
l Group *newGroup( const char *name, bool isDynamic = false )
创建一个Group,同时将该Group加到Manager的命令历史列表中。如果Manager正处于撤销了一系列的Group状态,在插入这个新的Group时,需要删除这些已处于撤销状态的Group,同时还需要验证以前的Group的合法性,无效的都要删除掉。
有时候,Group列表的长度和内存大小有限制,当插入的这个新的Group,这都将超过限制,这时候,需要对列表进行调整。
Group* Manager::newGroup( const char *name, bool isDynamic )
{
If( m_groupLimt < 0 )
Return 0;
// The new group goes after the last group ‘done’ – invalid
// all groups which were previously undone
pruneUndoneGroups();
// If any of the trailing groups are invalid, now is a good time
// to get rid of them
pruneTrailingInvalidGroups();
// If the queue has grown too larger, trim it down
respectLimits( true );
//Create the new group now
Group *group = new Group( this, name );
Group->setGroupDynamic( isDynamic );
//Add the group to our list of managed group
m_groupList.push_back( group );
m_currentGroup = m_groupList.end();
return group;
}
l Group *getCurrentGroup()
l Void setUndoGroupLimt( int numGroups )
l Void setUndoMemoryLimt( int bytes )
使用Undo机制
l 定义一个接收者;
class Dag_node
{
public:
Dag_node (const char *_name) : name(_name), tx(0), sx(0) {}
const char *getName() { return name; }
void translate (double x) { tx += x; }
void getTranslate (double &x) { x = tx; }
void scale (double x) { sx = x; }
void getScale (double &x) { x = sx; }
private:
const char *name;
double tx, sx;
};
l 从Command类派生具体的子类,实现commandDoIt()和commandUndoIt()方法;
class dagTranslateCmd : public Command
{
public:
dagTranslateCmd (Group *grp, Dag_node *_node, double x)
: Command (grp)
{
node = _node;
tx = x;
++activeCmds;
};
~dagTranslateCmd ()
{
--activeCmds;
}
virtual bool commandDoIt ()
{
node->translate (tx);
return true;
}
virtual bool commandUndoIt()
{
node->translate (-tx);
return true;
}
virtual int getNumBytesUsed()
{
return sizeof (*this);
}
private:
Dag_node *node;
double tx;
};
l 通过Manager来响应请求。
{
Dag_node *node1 = new Dag_node ("node1");
Manager *manager = newManager();//必须有一个Manager
manager->setUndoGroupLimit (4);//设置限制
Group *grp = manager->newGroup ("move");
Command *cmd = new dagTranslateCmd (grp, node1, 1);
cmd->doIt();
assert (manager->undoAvailable() == true);
assert (manager->doAvailable() == false);
cmd = new dagTranslateCmd (grp, node2, 10);
cmd->doIt();
grp = manager->newGroup ("move");
cmd = new dagTranslateCmd (grp, node3, 100);
cmd->doIt();
cmd = new dagTranslateCmd (grp, node4, -1);
cmd->doIt();
//撤销
manager->undoIt();
assert (manager->undoAvailable() == true);
assert (manager->doAvailable() == true);
//重做
manager->doIt();
assert (manager->undoAvailable() == true);
assert (manager->doAvailable() == false);
//继续新的操作
grp = manager->newGroup ("move");
cmd = new dagTranslateCmd (grp, node3, 50);
cmd->doIt();
//释放manager
delete manager;
}