手把手教你寫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

    // 注意這個函數的定義我會在後面根據需要修改,不是最後的版本。

    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;

};

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

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

class CUndoData

{

public:

    CUndoData() : m_ToolTip(0) {}

    virtual bool        UndoAction(CImageData * pData) = 0;

    unsigned int        m_ToolTip;

};

    成員m_ToolTip所表示的值是一個字符串資源的ID,如果我們希望在工具欄的UndoRedo按鈕上添加操作提示功能,就可以使用它。默認值是0,表示沒有提示信息。

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

 

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

class CFullImageUndo : public CUndoData

{

public:

    virtual bool        UndoAction(CImageData * pData)

    {

        // 這裏進行真正的Undo,我們只需把m_UndoDatapData的數據相互交互即可

        // 爲什麼交換就實現了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_fRotateAnglepData所指數據進行旋轉

        m_fRotateAngle *= -1;

        // 這裏爲什麼需要把角度乘以-1呢?因爲在進行一步Undo操作後,這個Undo數據

        // 馬上就會變爲Redo數據了,而進行Redo操作的算法是逆向的,這裏來說就是

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

    }

private:

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

};

 

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

 

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

        // 在調用pUndoUndoAction後,內部就已經把pUndo變爲了Redo數據

        m_RedoList.push(pUndo);

    }

    void    Redo(CImageData * pData);

    {

        CUndoData *pUndo = m_RedoList.top();

        pUndo->UndoAction(pData);

        // 在調用pUndoUndoAction後,內部就已經把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類的函數返回指,實現工具欄安裝狀態的改變以及工具欄按鈕的提示信息。


 

進一步內容可參考:

 手把手教你寫Undo、Redo程序(續)

發佈了77 篇原創文章 · 獲贊 0 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章