Click here to Skip to main content
15,880,427 members
Articles / Desktop Programming / MFC
Article

Simple and Easy Undo/Redo

Rate me:
Please Sign up or sign in to vote.
4.98/5 (22 votes)
30 Jan 2000CPOL 269.7K   2.5K   84   55
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
I work at Tektronix in Beaverton OR. I've been programming for fun since 1975 (I started while in a Computer Explorer Scout group in Spokane WA). I've been programming in C since 1979 and I've been working professionally since 1983.

I really enjoy www.codeproject.com. It has saved me an incredible amount of time. I only hope my small contributions have given back some of what I've taken.

Comments and Discussions

 
Generalundocumented functions Pin
ralfsch17-Jun-03 2:48
ralfsch17-Jun-03 2:48 
GeneralRe: undocumented functions Pin
Keith Rule21-Aug-03 6:19
professionalKeith Rule21-Aug-03 6:19 
Generalsome modified Pin
fabio bacherini15-Apr-03 4:02
sussfabio bacherini15-Apr-03 4:02 
GeneralRe: some modified Pin
stf.stefanini15-Apr-03 7:06
stf.stefanini15-Apr-03 7:06 
GeneralProblem with calling Undo followed by UpdateAllViews Pin
David Fulmer10-Apr-03 11:12
David Fulmer10-Apr-03 11:12 
GeneralRe: Problem with calling Undo followed by UpdateAllViews Pin
David Fulmer11-Apr-03 3:51
David Fulmer11-Apr-03 3:51 
GeneralApplication-level vs. Document-level undo Pin
JChampion8-Apr-03 19:29
JChampion8-Apr-03 19:29 
GeneralI have some question. Pin
Mousie13-Nov-02 18:16
Mousie13-Nov-02 18:16 
Confused | :confused: Now,I used the class For my application,but I found it very slowly when the file Is larger,Furthermore ,when the "Undo" command Is executed,the cursor went To the begin of the file,it Is Not my wish,can you help me?thanks.
GeneralRe: I have some question. Pin
Christian Graus13-Nov-02 18:22
protectorChristian Graus13-Nov-02 18:22 
GeneralRe: I have some question. Pin
Keith Rule13-Dec-02 7:10
professionalKeith Rule13-Dec-02 7:10 
GeneralIMPLEMENT_DYNCREATE Pin
withoutdruck26-Sep-02 8:04
withoutdruck26-Sep-02 8:04 
GeneralRe: IMPLEMENT_DYNCREATE Pin
Keith Rule26-Sep-02 9:48
professionalKeith Rule26-Sep-02 9:48 
GeneralRe: IMPLEMENT_DYNCREATE Pin
YinBoChao10-Jan-03 1:00
YinBoChao10-Jan-03 1:00 
GeneralRe: IMPLEMENT_DYNCREATE Pin
Keith Rule27-Jan-03 13:54
professionalKeith Rule27-Jan-03 13:54 
GeneralRe: IMPLEMENT_DYNCREATE Pin
park jae woong26-Nov-06 2:59
park jae woong26-Nov-06 2:59 
GeneralRe: IMPLEMENT_DYNCREATE Pin
Keith Rule27-Nov-06 5:10
professionalKeith Rule27-Nov-06 5:10 
GeneralRe: IMPLEMENT_DYNCREATE Pin
danny h111119-Aug-08 1:35
danny h111119-Aug-08 1:35 
GeneralRe: IMPLEMENT_DYNCREATE Pin
Keith Rule19-Aug-08 4:10
professionalKeith Rule19-Aug-08 4:10 
GeneralThanks Pin
10-May-02 4:12
suss10-May-02 4:12 
Questionneed to serialize every variable ? Pin
18-Feb-02 2:29
suss18-Feb-02 2:29 
QuestionOne problem with this. Can anyone advise? Pin
alan9317-Jan-02 8:55
alan9317-Jan-02 8:55 
AnswerRe: One problem with this. Can anyone advise? Pin
alan9317-Jan-02 9:21
alan9317-Jan-02 9:21 
Generalcreating new document through code. Pin
24-Dec-01 23:22
suss24-Dec-01 23:22 
GeneralJust a question Pin
3-Oct-01 4:36
suss3-Oct-01 4:36 
Generalfew comments/another solution Pin
9-Sep-01 20:28
suss9-Sep-01 20:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.