手把手教你寫Undo、Redo程式
手把手教你寫Undo、Redo程式
Undo、Redo操作是很多具體編輯功能的軟體所不能缺少的。最典型兩種型別就是文字編輯和影象編輯軟體。然而它的實現在某種程度上來說也不是很簡單。我也廢話少說。要在程式中支援Undo、Redo操作,就需要儲存一些必要的資訊,這個是眾所周知的。如果想支援無限級的Undo、Redo操作,儲存的資訊就會無限的膨脹,問題來了,如何設計才能使每一步操作儲存的資料儘可能少。
下面我就以影象編輯軟體為例。說明如何在影象編輯中新增Undo、Redo功能。在我們開始進行編碼設計前,對一些問題進行簡單說明:
1、如何儲存影象編輯操作中的操作資訊。影象編輯可簡單分為兩類:一類是可逆的。也就是我們施加在影象上的操作可以根據操作演算法進行逆操作。比如旋轉,在旋轉某個角度後如果需要
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。
// 注意這個函式的定義我會在後面根據需要修改,不是最後的版本。
boolExecuteOperation(CImageOperation * pCmd);
};
下面是CImageOperation類的基本定義:
class CImageOperation
{
public:
…........//其他的成員及成員函式
virtualboolExecute(CImageData * pData) = 0;
};
注意CImageOperation是一個抽象類,因為它並知道具體的影象操作。它的Execute函式也需要由派生的具體操作類實現。我下面就給一個具體操作實現類(以旋轉為例):
class CImageRatate : public CImageOperation
{
public:
CImageRatate(floatfAngle) : m_fRotateAngel(fAngle) {}
virtualboolExecute(CImageData * pData)
{
// 把pData所指的影象按時鐘方向(m_fRotateAngle>0時)旋轉m_fRotateAngle度數
// 如果小於0就是逆時鐘方向,這裡沒有具體的實現程式碼,可參考其他影象庫
}
private:
floatm_fRotateAngle;
};
注意:這個旋轉操作是可逆的。
怎麼樣你應該理解這個簡單的影象操作框架了吧!下面開始我們真正的Undo、Redo部分。基於前面第三點所述,我們可以把Undo的抽象基類設計如下:
class CUndoData
{
public:
CUndoData() : m_ToolTip(0) {}
virtualboolUndoAction(CImageData * pData) = 0;
unsigned intm_ToolTip;
};
成員m_ToolTip所表示的值是一個字串資源的ID,如果我們希望在工具欄的Undo、Redo按鈕上新增操作提示功能,就可以使用它。預設值是0,表示沒有提示資訊。
函式UndoAction是真正的Undo、Redo實現函式,也是一個抽象類。它的引數是由外部傳入的Undo物件(通常是文件類中的CImageData物件)。
根據前面第二點的說明,影象的可逆操作我們認為儲存的資料是一樣,都是CImageData物件。而不可逆操作是不同型別的。所以下面再定義兩個類,分別表示可逆操作的Undo類和一個不可逆的操作類。(不可逆操作很多,仍以旋轉為例)
class CFullImageUndo : public CUndoData
{
public:
virtualboolUndoAction(CImageData * pData)
{
// 這裡進行真正的Undo,我們只需把m_UndoData和pData的資料相互互動即可
// 為什麼交換就實現了Undo呢?因為m_UndoData是儲存的操作前的資料,而參
// 數pData指向的正是文件中的資料,交換為文件的資料就被舊的資料替換啦!
}
public:
CImageDatam_UndoData;
};
CFullImageUndo主要是針對不可逆操作的,因為只有這類操作我們才需要儲存整個的影象資料。下面是可逆的旋轉操作:
class CRatateUndo : public CUndoData
{
public:
CRotateUndo(float fAngle) : m_fRotateAngle(fAngle) {}
virtualboolUndoAction(CImageData * pData)
{
// 這裡根據m_fRotateAngle對pData所指資料進行旋轉
m_fRotateAngle *= -1;
// 這裡為什麼需要把角度乘以-1呢?因為在進行一步Undo操作後,這個Undo資料
// 馬上就會變為Redo資料了,而進行Redo操作的演算法是逆向的,這裡來說就是
// 應該把旋轉是方向改變一下。
}
private:
floatm_fRotateAngle;//此成員意義與CImageRatate中的一樣。
};
現在基本的Undo類有了。還沒有實現給外部文件類使用的Undo/Redo列表啦!我們需要儲存所有的Undo/Redo列表。從使用其他軟體你應該可以感受出:最後的操作總是被最先Undo。Redo也是這樣的。使用什麼樣的資料結構儲存列表就好實現了。我們也找一種後進先出的列表:棧。我們就來實現這個介面類:(這裡的棧我直接使用了STL的棧工具,其實STL的棧也是封裝STL的Duque實現的)
#pragma warning(disable : 4786)
#include
class CUndoList
{
public:
CUndoList(){}
~CUndoList()
{
ClearUndo();
ClearRedo();
}
public:
// 下面兩個函式判斷Undo/Redo棧是否已經空
boolIsUndoEmpty() const{return m_UndoList.empty();}
boolIsRedoEmpty() const{return m_RedoList.empty();}
//返回Undo資料的m_ToolTip資料,實現略
unsigned intGetUndoTips() const;
unsigned intGetRedoTips() const;
voidAddUndo(CUndoData *pUndo);
{
if (pUndo)
{
m_UndoList.push(pUndo);
ClearRedo();
}
}
voidUndo(CImageData * pData);
{
CUndoData *pUndo = m_UndoList.top();
pUndo->UndoAction(pData);
// 在呼叫pUndo的UndoAction後,內部就已經把pUndo變為了Redo資料
m_RedoList.push(pUndo);
}
voidRedo(CImageData * pData);
{
CUndoData *pUndo = m_RedoList.top();
pUndo->UndoAction(pData);
// 在呼叫pUndo的UndoAction後,內部就已經把pUndo變為了Undo資料
m_UndoList.push(pUndo);
}
voidClearUndo();// 清除Undo棧,實現略
voidClearRedo();// 清除Redo棧,實現略
private:
std::stackm_UndoList;
std::stackm_RedoList;
};
好了現在介面類實現。我們就可以在文件類中使用這個CUndoList類,並根據CUndoList類的函式返回指,實現工具欄安裝狀態的改變以及工具欄按鈕的提示資訊。
進一步內容可參考: