Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simple and Easy Undo/Redo

0.00/5 (No votes)
30 Jan 2000 4  
Easily add Undo/Redo to your CDocument/CView based applciation.

Overview

We've come a long way in the past few years. Program features that were considered niceties only a short while ago, are now expected on nearly every application. Undo/Redo is one of those features that has made the transition from nicety to requirement.

There are many techniques that can be used to implement an Undo/Redo strategy. Most are complicated, and require elaborate planning and thought to add them to your application. These complicated techniques have many advantages like require a small amount of storage to hold many previous states, and infinite undo/redo capabilities. You can find articles and code for an Undo/Redo manager of this scale.

This article describes a much simpler snapshot method. It has the advantage nearly trivial to any MFC Document/View application. The down side is that it uses memory equal to the size of the serialized file to store each previous state.

For many applications, these serialized files are small enough to allow several previous states to be held with little problem.

Adding Undo to an Existing Application

For me, the utility of a new class often depends on how easy it is to add to an existing application. I really like the Scribble tutorial that is found on the Visual C++ CD-ROM, so I'm going to use the Scribble application as an example application for adding undo support.

There are four steps necessary to add undo to an application.

  1. Add CUndo to the project by adding the undo.h header to stdafx.h and by adding a reference to this class in the CDocument derived classes that you wish to support undo.
  2. Add new code to save the current state of the project whenever the user makes a change worthy of noting.
  3. Wire Undo/Redo into the menuing system.

Adding CUndo to Scribble

Setup

The first step is to move the undo.h include into your project directory. The next step is to make undo.h available to the files that need it. You could do this by adding an include command at the top of each file that refers to undo.h. I tend to be a bit lazy and just add the line to the stdafx.h file. This causes undo.h to be included into most of the files in the project. I would add the following line to the stdafx.h file:

#include "undo.h"

Now the project knows about the CUndo class. To make the functionality of CUndo available to the Scribble application, we must add a reference to this class in the definition of the CScribbleDoc class. To do this, we must edit one line in the file scribdoc.h. That line is changed from:

class CScribbleDoc : public COleServerDoc

to

class CScribbleDoc : public COleServerDoc, public CUndo

At this point, the CScribbleDoc class contains support for undo/redo, all we need to do is take advantage of it.

Add Code to Save the Undo State

The key to making undo useful is deciding when to save the state of the application. I won't kid you, in many applications this can be a difficult decision. However, in the Scribble application, we will save the state whenever a stroke is completed and in a couple of other special places.

In the Scribble application, the stroke is saved in the OnLButtonUp() member function of the CScribView class. The logical place to save the state would be to save it after the stroke has been added to the CScribbleDoc. The OnLButtonUp() member function looks like this (minus a bunch of comments):

void CScribbleView::OnLButtonUp(UINT, CPoint point) 
{
    if (GetCapture() != this)
        return; 

    CScribbleDoc* pDoc = GetDocument();

    CClientDC dc(this);
    OnPrepareDC(&dc);  
    dc.DPtoLP(&point);

    CPen* pOldPen = dc.SelectObject(pDoc->GetCurrentPen());
    dc.MoveTo(m_ptPrev);
    dc.LineTo(point);
    dc.SelectObject(pOldPen);
    m_pStrokeCur->m_pointArray.Add(point);

    m_pStrokeCur->FinishStroke();

    pDoc->UpdateAllViews(this, 0L, m_pStrokeCur);

    ReleaseCapture();   
    pDoc->NotifyChanged();
    return;
}

The natural place to save the state would be after the m_pStrokeCur->FinishStroke() statement. To save the state, add the following line of code after that statement:

pDoc->CheckPoint();

The CheckPoint() member function saves the current state. Placing a CheckPoint() command here saves the state after every stroke is completed.

At first glance, this may seem like all the states that need to be saved for Undo/Redo support. However, it turns out there are a couple of other cases that are important. When the CScribbleDoc class is first instantiated or when the file is newed or opened, it is necessary to save the state, otherwise you won't be able to undo to the initial document state.

To do this, we need to execute the CheckPoint() member function in both the CScribbleDoc::OnNewDocument() member function and the CScribbleDoc::OnOpenDocument() member function. Both of these member functions' implementation are similar. The CheckPoint() member function should be added after the statement containing the reference to InitDocument(). The following code shows the updated functions:

BOOL CScribbleDoc::OnNewDocument()
{
    if (!COleServerDoc::OnNewDocument())
        return FALSE;
    InitDocument();
    CheckPoint();
    return TRUE;
}

BOOL CScribbleDoc::OnOpenDocument(LPCTSTR lpszPathName) 
{
    if (!COleServerDoc::OnOpenDocument(lpszPathName))
        return FALSE;
    InitDocument(); 
    CheckPoint();
    return TRUE;
}

After this change, the undo/redo commands are implemented. All that is necessary to make Undo/Redo work is to add menu support so that the user can access this functionality.

Add Undo/Redo Menu Support

Most applications created using the AppWizard in Visual C++ have a menu selection for undo. However, they don't provide a menu selection for redo. To make redo available, we need to add a redo menu item following the undo item in all of the edit menus defined in the application (and there are several of them). I used the identifier ID_EDIT_REDO and defined the caption as "&Redo\tCtrl+Y". As the caption suggests, I also defined the accelerator Ctrl+Y for each of the redo menu entries.

Using the ClassWizard, we can now add the skeleton code that implements undo and redo. To do this, select the Message Maps tab and CScribbleView class. Add functions for both the COMMAND and UPDATE_COMMAND_UI message to the ID_EDIT_UNDO and ID_EDIT_REDO Object identifier's. This will create skeleton functions for OnEditUndo(), OnEditRedo(), OnUpdateEditRedo(), and OnUpdateEditUndo(). The implementation for each of these functions follows:

void CScribbleView::OnEditRedo() 
{
    CScribbleDoc* pDoc = GetDocument();
    pDoc->Redo();
    pDoc->UpdateAllViews(NULL);
}

void CScribbleView::OnUpdateEditRedo(CCmdUI* pCmdUI) 
{
    CScribbleDoc* pDoc = GetDocument();
    pCmdUI->Enable(pDoc->CanRedo());
}

void CScribbleView::OnEditUndo() 
{
    CScribbleDoc* pDoc = GetDocument();
    pDoc->Undo();
    pDoc->UpdateAllViews(NULL);
}

void CScribbleView::OnUpdateEditUndo(CCmdUI* pCmdUI) 
{
    CScribbleDoc* pDoc = GetDocument();
    pCmdUI->Enable(pDoc->CanUndo());
}

Undo/Redo is now fully implemented. All that is necessary is to test the application.

How CUndo works

The code is straightforward. It serializes the CDocument into a CMemFile and saves that state on the undo list. When an undo is requested, the second item on the list is serialized into the CDocument and the first item placed on the Redo list (the first item isn't serialized because it contains the current state). There is only one trick used in the code. It is the concept of a Mix-in class.

A Mix-in is a class that is intended to add behavior to a derived class through multiple inheritance. The CUndo class knows that and derived class will include an implementation for the virtual functions Serialize() and DeleteContents(). All it has to do to access these functions in the derived class is declare them as abstract virtual functions. Now, when the class is added to the inheritance list of CDocument (or any class that supplies a Serialize() and DeleteContents() function) it is able to access these functions in the derived class.

Yes, Mix-in's aren't very intuitive, but they work well.

// undo.h implementation

// Author - Keith Rule (keithr@dsl-only.net)

//

// A detailed description of this code can be 

// found in May 1997 - Windows Tech Journal.


#ifndef _UNDO_H_
#define _UNDO_H_
//------------------------------------------------------------

//  Undo/Redo for MFC By Keith Rule

class CUndo {
private:
    CObList m_undolist;    // Stores undo states

    CObList m_redolist;    // Stores redo states

    long    m_growsize;    // Adjust for faster saves

    long    m_undoLevels;  // Requested Undolevels 

    long    m_chkpt;

    void AddUndo(CMemFile*);
    void AddRedo(CMemFile *pFile); 
    void Load(CMemFile*);
    void Store(CMemFile*);
    void ClearRedoList();

public:

    // Here are the hooks into the CDocument class

    virtual void Serialize(CArchive& ar) = 0;
    virtual void DeleteContents() = 0;

    // User accessable functions

    CUndo(long undolevels = 4, long = 32768);    // Constructor

    ~CUndo();            // Destructor

    BOOL CanUndo();      // Returns TRUE if can Undo

    BOOL CanRedo();      // Returns TRUE if can Redo

    void Undo();         // Restore next Undo state

    void Redo();         // Restore next Redo state

    void CheckPoint();   // Save current state 

    void EnableCheckPoint();
    void DisableCheckPoint();
};

// Constructor

inline CUndo::
CUndo(long undolevels, long growsize) : 
    m_growsize(growsize), m_undoLevels(undolevels),
    m_chkpt(0)
{
        ;
} 

// Remove contents of the redo list

inline void CUndo::
ClearRedoList()
{
    // Clear redo list

    POSITION pos = m_redolist.GetHeadPosition(); 
    CMemFile* nextFile = NULL;
    while(pos) {
        nextFile = (CMemFile *) m_redolist.GetNext(pos);
        delete nextFile;
    }
    m_redolist.RemoveAll();
}

// Destructor

inline CUndo::
~CUndo() 
{
    // Clear undo list

    POSITION pos = m_undolist.GetHeadPosition(); 
    CMemFile  *nextFile = NULL;
    while(pos) {
        nextFile = (CMemFile *) m_undolist.GetNext(pos);
        delete nextFile;
    }
    m_undolist.RemoveAll();

    // Clear redo list

    ClearRedoList();
}

// Checks undo availability, may be used to enable menus

inline BOOL CUndo::
CanUndo() 
{
    return (m_undolist.GetCount() > 1);
}

// Checks redo availability, may be used to enable menus

inline BOOL CUndo::
CanRedo() 
{
    return (m_redolist.GetCount() > 0);
}

// Adds state to the beginning of undo list

inline void CUndo::
AddUndo(CMemFile* file) 
{
    // Remove old state if there are more than max allowed

    if (m_undolist.GetCount() > m_undoLevels) {
        CMemFile* pFile = (CMemFile *) m_undolist.RemoveTail();
        delete pFile;
    }
    // Add new state to head of undo list

    m_undolist.AddHead(file);
}

// Saves current object into CMemFile instance

inline void CUndo::
Store(CMemFile* file) 
{
    file->SeekToBegin();
    CArchive ar(file, CArchive::store);
    Serialize(ar); 
    ar.Close();
}

// Loads CMemfile instance to current object

inline void CUndo::
Load(CMemFile* file) 
{
    DeleteContents(); 
    file->SeekToBegin();
    CArchive ar(file, CArchive::load);
    Serialize(ar); 
    ar.Close();
}

// Save current object state to Undo list

inline void CUndo::
CheckPoint() 
{
    if (m_chkpt <= 0) {
        CMemFile* file = new CMemFile(m_growsize);
        Store(file);
        AddUndo(file);
        ClearRedoList();
    }
}

inline void CUndo::
EnableCheckPoint()
{
    if (m_chkpt > 0) {
        m_chkpt--;
    }
}

inline void CUndo::
DisableCheckPoint()
{
    m_chkpt++;
}

// Place CMemFile instnace on Redo list

inline void CUndo::
AddRedo(CMemFile *file) 
{
    // Move state to head of redo list

    m_redolist.AddHead(file);
}

// Perform an Undo command

inline void CUndo::
Undo() 
{
    if (CanUndo()) {
        // Remember that the head of the undo list

        // is the current state. So we just move that

        // to the Redo list and load then previous state.

        CMemFile *pFile = (CMemFile *) m_undolist.GetHead();
        m_undolist.RemoveHead();
        AddRedo(pFile);
        pFile = (CMemFile *)m_undolist.GetHead();
        Load(pFile);
    }
}

//Perform a Redo Command

inline void CUndo::
Redo() 
{
    if (CanRedo()) {
        CMemFile *pFile = (CMemFile *) m_redolist.GetHead() ;
        m_redolist.RemoveHead();
        AddUndo(pFile);
        Load(pFile);
    }
}

#endif

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here