手把手教你寫Undo、Redo程序
Undo、Redo操作是很多具體編輯功能的軟件所不能缺少的。最典型兩種類型就是文本編輯和圖像編輯軟件。然而它的實現在某種程度上來說也不是很簡單。我也廢話少說。要在程序中支持Undo、Redo操作,就需要保存一些必要的信息,這個是衆所周知的。如果想支持無限級的Undo、Redo操作,保存的信息就會無限的膨脹,問題來了,如何設計才能使每一步操作保存的數據儘可能少。
下面我就以圖像編輯軟件爲例。說明如何在圖像編輯中添加Undo、Redo功能。在我們開始進行編碼設計前,對一些問題進行簡單說明:
1、如何保存圖像編輯操作中的操作信息。圖像編輯可簡單分爲兩類:一類是可逆的。也就是我們施加在圖像上的操作可以根據操作算法進行逆操作。比如旋轉,在旋轉某個角度後如果需要Undo我們可以直接按相反的方向再旋轉同樣的角度;另一類是不可逆的。這裏的不可逆不是絕對的。比如我們根據某個模板算法對圖像的每個象素進行修改。這時我們就直接把此類操作歸爲不可逆。因爲即使它可能是可逆的,但是實現起來的難道如果很大,這裏只是爲了方便說明。
2、對操作有了基本分類後。我們可以發現不可逆操作的Undo、Redo功能實現應該比較容易一些。爲什麼呢?因爲操作不可逆,我們必須在操作前把全部的象素保存起來。這就相當於對原來的信息做了一份拷貝。所有的不可逆操作保存的信息可以認爲是相同的:都是整個圖像象素。此類操作實現簡單,但是代碼卻高。而對於可逆操作,不同的操作算法就對應不同的Undo、Redo。每次操作保存的信息不同,但是我們只需要保存操作的算法。此類操作實現稍微麻煩。但是所需空間較小。對比兩種操作,正如魚和熊掌不能兼得。
3、在我們打開一副圖像後,通常在軟件的文檔類中應該有一個最基本的圖像數據類。所有的操作都是基於此類的數據。而且在我們進行Undo、Redo操作時,需要傳遞一個外部(也就是文檔的圖像數據)作爲Undo、Redo的對象。
好了,我們開始對一些類進行說明。爲了把數據數據與圖像操作進行分離,我們定義兩個基類:CImageData和CImageOperation。分別表示圖像數據類和圖像操作的基類。
class CImageData
{
public:
…........ //其他的成員及成員函數
BYTE * m_pByte; //象素數據的BYTE指針
BITMAPINFO * m_pInfo; //Windows平臺的圖像數據結構,也可以自定義
public:
// 函數ExecuteOperation是對當前的圖像數據執行某種Operation。
// 注意這個函數的定義我會在後面根據需要修改,不是最後的版本。
bool ExecuteOperation(CImageOperation * pCmd);
};
下面是CImageOperation類的基本定義:
class CImageOperation
{
public:
…........ //其他的成員及成員函數
virtual bool Execute(CImageData * pData) = 0;
};
注意CImageOperation是一個抽象類,因爲它並知道具體的圖像操作。它的Execute函數也需要由派生的具體操作類實現。我下面就給一個具體操作實現類(以旋轉爲例):
class CImageRatate : public CImageOperation
{
public:
CImageRatate(float fAngle) : m_fRotateAngel(fAngle) {}
virtual bool Execute(CImageData * pData)
{
// 把pData所指的圖像按時鐘方向(m_fRotateAngle>0時)旋轉m_fRotateAngle度數
// 如果小於0就是逆時鐘方向,這裏沒有具體的實現代碼,可參考其他圖像庫
}
private:
float m_fRotateAngle;
};
注意:這個旋轉操作是可逆的。
怎麼樣你應該理解這個簡單的圖像操作框架了吧!下面開始我們真正的Undo、Redo部分。基於前面第三點所述,我們可以把Undo的抽象基類設計如下:
class CUndoData
{
public:
CUndoData() : m_ToolTip(0) {}
virtual bool UndoAction(CImageData * pData) = 0;
unsigned int m_ToolTip;
};
成員m_ToolTip所表示的值是一個字符串資源的ID,如果我們希望在工具欄的Undo、Redo按鈕上添加操作提示功能,就可以使用它。默認值是0,表示沒有提示信息。
函數UndoAction是真正的Undo、Redo實現函數,也是一個抽象類。它的參數是由外部傳入的Undo對象(通常是文檔類中的CImageData對象)。
根據前面第二點的說明,圖像的可逆操作我們認爲保存的數據是一樣,都是CImageData對象。而不可逆操作是不同類型的。所以下面再定義兩個類,分別表示可逆操作的Undo類和一個不可逆的操作類。(不可逆操作很多,仍以旋轉爲例)
class CFullImageUndo : public CUndoData
{
public:
virtual bool UndoAction(CImageData * pData)
{
// 這裏進行真正的Undo,我們只需把m_UndoData和pData的數據相互交互即可
// 爲什麼交換就實現了Undo呢?因爲m_UndoData是保存的操作前的數據,而參
// 數pData指向的正是文檔中的數據,交換爲文檔的數據就被舊的數據替換啦!
}
public:
CImageData m_UndoData;
};
CFullImageUndo主要是針對不可逆操作的,因爲只有這類操作我們才需要保存整個的圖像數據。下面是可逆的旋轉操作:
class CRatateUndo : public CUndoData
{
public:
CRotateUndo(float fAngle) : m_fRotateAngle(fAngle) {}
virtual bool UndoAction(CImageData * pData)
{
// 這裏根據m_fRotateAngle對pData所指數據進行旋轉
m_fRotateAngle *= -1;
// 這裏爲什麼需要把角度乘以-1呢?因爲在進行一步Undo操作後,這個Undo數據
// 馬上就會變爲Redo數據了,而進行Redo操作的算法是逆向的,這裏來說就是
// 應該把旋轉是方向改變一下。
}
private:
float m_fRotateAngle; //此成員意義與CImageRatate中的一樣。
};
現在基本的Undo類有了。還沒有實現給外部文檔類使用的Undo/Redo列表啦!我們需要保存所有的Undo/Redo列表。從使用其他軟件你應該可以感受出:最後的操作總是被最先Undo。Redo也是這樣的。使用什麼樣的數據結構保存列表就好實現了。我們也找一種後進先出的列表:棧。我們就來實現這個接口類:(這裏的棧我直接使用了STL的棧工具,其實STL的棧也是封裝STL的Duque實現的)
#pragma warning(disable : 4786)
#include <stack>
class CUndoList
{
public:
CUndoList(){}
~CUndoList()
{
ClearUndo();
ClearRedo();
}
public:
// 下面兩個函數判斷Undo/Redo棧是否已經空
bool IsUndoEmpty() const { return m_UndoList.empty(); }
bool IsRedoEmpty() const { return m_RedoList.empty(); }
// 返回Undo數據的m_ToolTip數據,實現略
unsigned int GetUndoTips() const;
unsigned int GetRedoTips() const;
void AddUndo(CUndoData * pUndo);
{
if (pUndo)
{
m_UndoList.push(pUndo);
ClearRedo();
}
}
void Undo(CImageData * pData);
{
CUndoData *pUndo = m_UndoList.top();
pUndo->UndoAction(pData);
// 在調用pUndo的UndoAction後,內部就已經把pUndo變爲了Redo數據
m_RedoList.push(pUndo);
}
void Redo(CImageData * pData);
{
CUndoData *pUndo = m_RedoList.top();
pUndo->UndoAction(pData);
// 在調用pUndo的UndoAction後,內部就已經把pUndo變爲了Undo數據
m_UndoList.push(pUndo);
}
void ClearUndo(); // 清除Undo棧,實現略
void ClearRedo(); // 清除Redo棧,實現略
private:
std::stack<CUndoData *> m_UndoList;
std::stack<CUndoData *> m_RedoList;
};
好了現在接口類實現。我們就可以在文檔類中使用這個CUndoList類,並根據CUndoList類的函數返回指,實現工具欄安裝狀態的改變以及工具欄按鈕的提示信息。
進一步內容可參考: