Undo/Redo的C#實現方式(原創)
現代軟件很多都配備了Undo/Redo功能,這個功能對於用戶來說是十分方便,可以明顯節約用戶時間,增加軟件的易用性,其中以Office和AutoCAD最爲典型。本文以.NET環境中最常用的C#語言爲例,詳細分析了實現Undo/Redo功能的必要技術,並提供了可運行的C#代碼清單。
實現Undo/Redo功能,就必須記錄用戶的操作和這些操作發生前受該操作影響的對象的值。當然不是每個操作都是值得關注的,比如“打開文件”,“最大化窗口”等這類不改變軟件核心對象數據的操作,是完全不必記錄下來的。
其中一些操作只改變一個對象的一個域的值,而一些操作可以改變很對個對象的多個域的值(例如AutoCAD中的分解命令,可以同時改變N個對象的狀態)。爲了簡化問題,我們肯定希望不管是隻改變一個對象的某個域的操作(被稱爲原子操作)還是一次改變多個對象的操作(被稱爲複合操作)都被以一種單一接口被提供 。這就要對操作進行包裝,將原子操作打包成複合操作,或者說把複合操作僞裝成原子操作,因爲對於操作的調用者來說,這完全是透明的,他將感覺不到自己調用的是原子操作還是複合操作,也不必關心這個問題。
一個複合操作到底含有多少原子操作是事先無法確定的。ArrayList是.NET提供的一種可變的萬能數組,它實際上是一個****對象,但是它的使用方式上看上去更像個可變的一維數組,很適合用於這種未知長度的情況。所以複合操作用ArrayList存放是合適的。
接下來的事情就是理解Undo/Redo的本質了。用戶在Undo時,軟件到底做了些什麼呢?上面分析了所有有效操作都必須被記錄下來,而且是被存放在數組或者是鏈表中,當用戶Undo時,顯然是要從數組或者鏈表中取回被保存的數據,然後賦值給相對應的對象。Redo發生時大概也是發生了這些事情,只是方向和Undo剛好是相反的。用戶的操作不一定總是要插入數組/鏈表的末端。舉個例子:當用戶的先進行了10個操作,然後又Undo掉了5個操作,然後又進行了一個操作,這個操作是應該保存在數組/鏈表的第11個位置上還是應該保存在第6個位置上呢?答案顯然是後者。只要又進行了一個操作,先前的被保存在Undo數組/鏈表中的第6到10個記錄將全部變成無效。
那麼用戶在Redo時發生了什麼呢?還是剛纔的例子,用戶Undo掉5個操作後又Redo了3次,顯然應該和用戶只進行了前8個操作是等價的。如果用戶Undo掉5個操作而沒有Redo,而是又進行了3個其他操作呢?這時候他再Redo會怎樣?根據經驗,結論是無法Redo了,Redo操作必須緊隨Undo才行。因爲Undo數組中第9,10兩個操作早已經是無效了,再Redo它們,是絕對錯誤的事情。
至此,關於Undo/Redo的分析就完成了,接下來就是編碼。下面提供的是在VS2005上編譯通過的C#源代碼:
public class CUndo
{
//private ArrayList actionList;//複合操作隊列,每個actionList的子成員爲CAction對象
private CActionNode[] actionList;
private int iMaxUndoTime;//操作隊列的最大長度
private bool bCanUndo=false;//當前狀態下是否可以進行Undo操作
public bool CanUndo
{
get { return bCanUndo; }
}
private bool bCanRedo = false;//當前狀態下是否可以進行Redo操作
public bool CanRedo
{
get { return bCanRedo; }
}
public bool bStartState = false;//指示是否是第一次記錄操作
private int iActionListLength= 0;//有效操作隊列的長度,它並不一定等於actionList.count
private int iCurrentActionPointer= -1;//指向actionList當前操作的指針,新添加的操作從該
//位置的下一位開始,Undo操作從它的上一個位置開始,Redo操作從它下一位開始
public CUndo(int maxUndoTime)
{
actionList = new CActionNode[maxUndoTime];//爲actionList分配內存空間
iMaxUndoTime = maxUndoTime;//設置可Undo的最大次數
}
public int ActionListLength//獲取當前操作隊列的實際長度
{
get { return iActionListLength;}
}
public int CurrentActionPointer//獲取當前Undo指針的位置
{
get {return iCurrentActionPointer;}
}
//向操作隊列中加入操作,插入成功就返回true,否則爲false
public bool addActionIntoActionList(CActionNode act)
{
//如果不超過Undo隊列的最大允許長度就可以插入
if (iCurrentActionPointer< iMaxUndoTime-1)
{
actionList[++iCurrentActionPointer]= act;
iActionListLength = iCurrentActionPointer;
bCanUndo = true;//只要有操作進入隊列,bCanUndo就爲真
return true;
}
else
return false;
}
public bool undo(int iUndoTime)
{
//如果undo次數大於iCurrentActionPointer指示的長度,就報錯
//因爲Undo隊列的最大長度就是iCurrentActionPointer+1
if (iUndoTime > iCurrentActionPointer+1)
return false;
CActionNode act;
while (iUndoTime > 0 && iCurrentActionPointer>0)
{
//從Undo隊列中取回操作
act = actionList[--iCurrentActionPointer];
iUndoTime--;
//如果取回操作失敗,返回false
if (act.getActionsInActionNode() == false)
return false;
}
bCanRedo = true;//如果成功Undo,就置bCanRedo爲真
return true;
}
public bool redo(int iRedoTime)
{
if (iRedoTime > iActionListLength - iCurrentActionPointer)
return false;
CActionNode act;
while (iRedoTime>0 )
{
act = actionList[++iCurrentActionPointer];
iRedoTime--;
Debug.Assert(act != null, iCurrentActionPointer.ToString());
if (act.getActionsInActionNode()==false)
return false;
}
bCanUndo = true;//如果成功Redo,就置bCanUndo爲真
return true;
}
}
實現Undo/Redo功能,就必須記錄用戶的操作和這些操作發生前受該操作影響的對象的值。當然不是每個操作都是值得關注的,比如“打開文件”,“最大化窗口”等這類不改變軟件核心對象數據的操作,是完全不必記錄下來的。
其中一些操作只改變一個對象的一個域的值,而一些操作可以改變很對個對象的多個域的值(例如AutoCAD中的分解命令,可以同時改變N個對象的狀態)。爲了簡化問題,我們肯定希望不管是隻改變一個對象的某個域的操作(被稱爲原子操作)還是一次改變多個對象的操作(被稱爲複合操作)都被以一種單一接口被提供 。這就要對操作進行包裝,將原子操作打包成複合操作,或者說把複合操作僞裝成原子操作,因爲對於操作的調用者來說,這完全是透明的,他將感覺不到自己調用的是原子操作還是複合操作,也不必關心這個問題。
一個複合操作到底含有多少原子操作是事先無法確定的。ArrayList是.NET提供的一種可變的萬能數組,它實際上是一個****對象,但是它的使用方式上看上去更像個可變的一維數組,很適合用於這種未知長度的情況。所以複合操作用ArrayList存放是合適的。
接下來的事情就是理解Undo/Redo的本質了。用戶在Undo時,軟件到底做了些什麼呢?上面分析了所有有效操作都必須被記錄下來,而且是被存放在數組或者是鏈表中,當用戶Undo時,顯然是要從數組或者鏈表中取回被保存的數據,然後賦值給相對應的對象。Redo發生時大概也是發生了這些事情,只是方向和Undo剛好是相反的。用戶的操作不一定總是要插入數組/鏈表的末端。舉個例子:當用戶的先進行了10個操作,然後又Undo掉了5個操作,然後又進行了一個操作,這個操作是應該保存在數組/鏈表的第11個位置上還是應該保存在第6個位置上呢?答案顯然是後者。只要又進行了一個操作,先前的被保存在Undo數組/鏈表中的第6到10個記錄將全部變成無效。
那麼用戶在Redo時發生了什麼呢?還是剛纔的例子,用戶Undo掉5個操作後又Redo了3次,顯然應該和用戶只進行了前8個操作是等價的。如果用戶Undo掉5個操作而沒有Redo,而是又進行了3個其他操作呢?這時候他再Redo會怎樣?根據經驗,結論是無法Redo了,Redo操作必須緊隨Undo才行。因爲Undo數組中第9,10兩個操作早已經是無效了,再Redo它們,是絕對錯誤的事情。
至此,關於Undo/Redo的分析就完成了,接下來就是編碼。下面提供的是在VS2005上編譯通過的C#源代碼:
public class CUndo
{
//private ArrayList actionList;//複合操作隊列,每個actionList的子成員爲CAction對象
private CActionNode[] actionList;
private int iMaxUndoTime;//操作隊列的最大長度
private bool bCanUndo=false;//當前狀態下是否可以進行Undo操作
public bool CanUndo
{
get { return bCanUndo; }
}
private bool bCanRedo = false;//當前狀態下是否可以進行Redo操作
public bool CanRedo
{
get { return bCanRedo; }
}
public bool bStartState = false;//指示是否是第一次記錄操作
private int iActionListLength= 0;//有效操作隊列的長度,它並不一定等於actionList.count
private int iCurrentActionPointer= -1;//指向actionList當前操作的指針,新添加的操作從該
//位置的下一位開始,Undo操作從它的上一個位置開始,Redo操作從它下一位開始
public CUndo(int maxUndoTime)
{
actionList = new CActionNode[maxUndoTime];//爲actionList分配內存空間
iMaxUndoTime = maxUndoTime;//設置可Undo的最大次數
}
public int ActionListLength//獲取當前操作隊列的實際長度
{
get { return iActionListLength;}
}
public int CurrentActionPointer//獲取當前Undo指針的位置
{
get {return iCurrentActionPointer;}
}
//向操作隊列中加入操作,插入成功就返回true,否則爲false
public bool addActionIntoActionList(CActionNode act)
{
//如果不超過Undo隊列的最大允許長度就可以插入
if (iCurrentActionPointer< iMaxUndoTime-1)
{
actionList[++iCurrentActionPointer]= act;
iActionListLength = iCurrentActionPointer;
bCanUndo = true;//只要有操作進入隊列,bCanUndo就爲真
return true;
}
else
return false;
}
public bool undo(int iUndoTime)
{
//如果undo次數大於iCurrentActionPointer指示的長度,就報錯
//因爲Undo隊列的最大長度就是iCurrentActionPointer+1
if (iUndoTime > iCurrentActionPointer+1)
return false;
CActionNode act;
while (iUndoTime > 0 && iCurrentActionPointer>0)
{
//從Undo隊列中取回操作
act = actionList[--iCurrentActionPointer];
iUndoTime--;
//如果取回操作失敗,返回false
if (act.getActionsInActionNode() == false)
return false;
}
bCanRedo = true;//如果成功Undo,就置bCanRedo爲真
return true;
}
public bool redo(int iRedoTime)
{
if (iRedoTime > iActionListLength - iCurrentActionPointer)
return false;
CActionNode act;
while (iRedoTime>0 )
{
act = actionList[++iCurrentActionPointer];
iRedoTime--;
Debug.Assert(act != null, iCurrentActionPointer.ToString());
if (act.getActionsInActionNode()==false)
return false;
}
bCanUndo = true;//如果成功Redo,就置bCanUndo爲真
return true;
}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.