1. 程式人生 > >手把手教你寫Undo、Redo程式

手把手教你寫Undo、Redo程式

 

手把手教你寫UndoRedo程式

UndoRedo操作是很多具體編輯功能的軟體所不能缺少的。最典型兩種型別就是文字編輯和影象編輯軟體。然而它的實現在某種程度上來說也不是很簡單。我也廢話少說。要在程式中支援UndoRedo操作,就需要儲存一些必要的資訊,這個是眾所周知的。如果想支援無限級的UndoRedo操作,儲存的資訊就會無限的膨脹,問題來了,如何設計才能使每一步操作儲存的資料儘可能少。

下面我就以影象編輯軟體為例。說明如何在影象編輯中新增UndoRedo功能。在我們開始進行編碼設計前,對一些問題進行簡單說明:

1、如何儲存影象編輯操作中的操作資訊。影象編輯可簡單分為兩類:一類是可逆的。也就是我們施加在影象上的操作可以根據操作演算法進行逆操作。比如旋轉,在旋轉某個角度後如果需要

Undo我們可以直接按相反的方向再旋轉同樣的角度;另一類是不可逆的。這裡的不可逆不是絕對的。比如我們根據某個模板演算法對影象的每個象素進行修改。這時我們就直接把此類操作歸為不可逆。因為即使它可能是可逆的,但是實現起來的難道如果很大,這裡只是為了方便說明。

2、對操作有了基本分類後。我們可以發現不可逆操作的UndoRedo功能實現應該比較容易一些。為什麼呢?因為操作不可逆,我們必須在操作前把全部的象素儲存起來。這就相當於對原來的資訊做了一份拷貝。所有的不可逆操作儲存的資訊可以認為是相同的:都是整個影象象素。此類操作實現簡單,但是程式碼卻高。而對於可逆操作,不同的操作演算法就對應不同的UndoRedo

。每次操作儲存的資訊不同,但是我們只需要儲存操作的演算法。此類操作實現稍微麻煩。但是所需空間較小。對比兩種操作,正如魚和熊掌不能兼得。

3、在我們開啟一副影象後,通常在軟體的文件類中應該有一個最基本的影象資料類。所有的操作都是基於此類的資料。而且在我們進行UndoRedo操作時,需要傳遞一個外部(也就是文件的影象資料)作為UndoRedo的物件。

好了,我們開始對一些類進行說明。為了把資料資料與影象操作進行分離,我們定義兩個基類:CImageDataCImageOperation。分別表示影象資料類和影象操作的基類。

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;

};

注意:這個旋轉操作是可逆的。

怎麼樣你應該理解這個簡單的影象操作框架了吧!下面開始我們真正的UndoRedo部分。基於前面第三點所述,我們可以把Undo的抽象基類設計如下:

class CUndoData

{

public:

CUndoData() : m_ToolTip(0) {}

virtualboolUndoAction(CImageData * pData) = 0;

unsigned intm_ToolTip;

};

成員m_ToolTip所表示的值是一個字串資源的ID,如果我們希望在工具欄的UndoRedo按鈕上新增操作提示功能,就可以使用它。預設值是0,表示沒有提示資訊。

函式UndoAction是真正的UndoRedo實現函式,也是一個抽象類。它的引數是由外部傳入的Undo物件(通常是文件類中的CImageData物件)。

根據前面第二點的說明,影象的可逆操作我們認為儲存的資料是一樣,都是CImageData物件。而不可逆操作是不同型別的。所以下面再定義兩個類,分別表示可逆操作的Undo類和一個不可逆的操作類。(不可逆操作很多,仍以旋轉為例)

class CFullImageUndo : public CUndoData

{

public:

virtualboolUndoAction(CImageData * pData)

{

// 這裡進行真正的Undo,我們只需把m_UndoDatapData的資料相互互動即可

// 為什麼交換就實現了Undo呢?因為m_UndoData是儲存的操作前的資料,而參

// pData指向的正是文件中的資料,交換為文件的資料就被舊的資料替換啦!

}

public:

CImageDatam_UndoData;

};

CFullImageUndo主要是針對不可逆操作的,因為只有這類操作我們才需要儲存整個的影象資料。下面是可逆的旋轉操作:

class CRatateUndo : public CUndoData

{

public:

CRotateUndo(float fAngle) : m_fRotateAngle(fAngle) {}

virtualboolUndoAction(CImageData * pData)

{

// 這裡根據m_fRotateAnglepData所指資料進行旋轉

m_fRotateAngle *= -1;

// 這裡為什麼需要把角度乘以-1呢?因為在進行一步Undo操作後,這個Undo資料

// 馬上就會變為Redo資料了,而進行Redo操作的演算法是逆向的,這裡來說就是

// 應該把旋轉是方向改變一下。

}

private:

floatm_fRotateAngle;//此成員意義與CImageRatate中的一樣。

};

現在基本的Undo類有了。還沒有實現給外部文件類使用的Undo/Redo列表啦!我們需要儲存所有的Undo/Redo列表。從使用其他軟體你應該可以感受出:最後的操作總是被最先UndoRedo也是這樣的。使用什麼樣的資料結構儲存列表就好實現了。我們也找一種後進先出的列表:棧。我們就來實現這個介面類:(這裡的棧我直接使用了STL的棧工具,其實STL的棧也是封裝STLDuque實現的)

#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);

// 在呼叫pUndoUndoAction後,內部就已經把pUndo變為了Redo資料

m_RedoList.push(pUndo);

}

voidRedo(CImageData * pData);

{

CUndoData *pUndo = m_RedoList.top();

pUndo->UndoAction(pData);

// 在呼叫pUndoUndoAction後,內部就已經把pUndo變為了Undo資料

m_UndoList.push(pUndo);

}

voidClearUndo();// 清除Undo棧,實現略

voidClearRedo();// 清除Redo棧,實現略

private:

std::stackm_UndoList;

std::stackm_RedoList;

};

好了現在介面類實現。我們就可以在文件類中使用這個CUndoList類,並根據CUndoList類的函式返回指,實現工具欄安裝狀態的改變以及工具欄按鈕的提示資訊。


進一步內容可參考: