許多GUI程序中提供一個"撤銷&重做"的功能,這個功能對用戶來說非常友好;本文就簡單的介紹一下如何用C#實現該功能。
實現Undo&Redo功能的基本模型是帶撤銷功能的命令模式,它將每步操作保存為一個命令對象,如下所示:
interface Icommand
{
void Do();
void Undo();
}
其中Do函數執行功能,Undo函數回滾功能。這樣就把命令給實體化了,只要將命令對象給保存下來,需要撤銷時執行Undo函數,重做時執行Do函數即可。
有了這個基本思路后,下面就是實現細節了:
-
申請兩個Stack來保存命令對象:UndoStack和RedoStack
-
執行命令時,將命令序列化為Command對象,執行Do方法,存入UndoStack,清空RedoStack
-
撤銷命令時,從UndoStack中取出命令,執行Undo方法,存入RedoStack
-
重做命令時,從RedoStack中取出命令,執行Do方法,存入UndoStack
一個簡單的實現如下:
class CommandManager
{
Stack<Command> redoStack = new Stack<Command>();
Stack<Command> undoStack = new Stack<Command>();
public void AddCommand(Action doCmd, Action undoCmd)
{
var cmd = new Command(doCmd, undoCmd);
cmd.Do();
undoStack.Push(cmd);
redoStack.Clear();
}
public bool Undo()
{
if (undoStack.Count == 0)
return false;
var cmd = undoStack.Pop();
redoStack.Push(cmd);
cmd.Undo();
return true;
}
public bool Redo()
{
if (redoStack.Count == 0)
return false;
var cmd = redoStack.Pop();
undoStack.Push(cmd);
cmd.Do();
return true;
}
class Command
{
public Action Do { get; private set; }
public Action Undo { get; private set; }
public Command(Action doCmd, Action undoCmd)
{
this.Do = doCmd;
this.Undo = undoCmd;
}
}
}
用C#實現起來還是非常簡潔的,就幾十行代碼。
遺留問題:命令對象何時釋放
前面的實現雖然非常簡單,但存在一個遺留問題:每一個命令對象都保存在UndoStack中了,這樣隨着程序的執行,UndoStack中記錄的命令越來越多,占用內存得不到釋放。對於這個問題,一般有如下幾種策略:
-
不釋放命令對象。一般需要Undo&Redo功能都是些GUI程序,這些程序大多不會持續運行,並且對內存的占用也沒有太大限制,命令對象一般也不會占用多少內存。保存所有命令對象不會對程序造成什么影響。
-
命令堆棧維持固定的長度:當命令堆棧的長度超過閾值的時候,刪除最開始壓入的命令。這種策略用得最多,但這樣帶來的問題就是無法實現無限Undo。
-
將命令堆棧保存到文件:將命令序列化保存到文件,需要使用時從文件中還原。這種方式可以實現無限Undo,但序列化命令往往是件比較麻煩的事情,反序列化時也要消耗時間。
-
綜合2,3兩種方案:內存中保持固定長度的命令對象,超過閾值的保存到文件。這種方式能有效解決反序列化的耗時問題,也能實現無限Undo。但實現起來也最為麻煩。
基於篇幅所限,本文就不進一步討論和實現了。