Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VisualC++

Minimal Undo - An Undo/Redo Framework

5.00/5 (4 votes)
26 Jun 2014CPOL3 min read 14K   455  
A solution for conversion of a non-undoable application to an undoable one.

Image 1

Introduction

This article presents a simple way to add undo/redo support (http://en.wikipedia.org/wiki/Undo) to your applications. The framework is trying to simplify the conversion of a non-undoable application to an undoable one.

To use it, basic C++ & STL understanding is required. To understand the inner workings you will have to know a bit more about STL and templates. To understand the demo application, basic MFC understanding is also required; however, you do not need MFC for the simple demo application.

I have used Microsoft Visual C++ 2010/2013 and Windows XP/7. The framework can easily be ported to other operating systems (the compiler must be C++ 11 compliant if you want to use all features).

Two demo applications are provided: a simple, command line application and a more complex one, a useless drawing application.

I tested the solution with relatively simple applications. For complex application I guess you should use a command based solution (see my other undo/redo implementation).

The source code may be used/modified free of charge in commercial/non-commercial applications. If you use it, send me a message; I just want to know.

How to

The undo stack stores the current-state and a reference to a client implemented swap function which is able to swap the state with the one sent by the framework.

The Client Interface

The framework is enclosed in mundo namespace (MinimalUNDO) and contains the following:

  1. A swap interface, C_Swap.
  2. A history interface, C_History.
  3. A history creator, CreateHistory.
  4. An automatic transaction opener/coser, C_AutoTransaction.
  5. Generic swap creators, CreateSwap.

Usage

The examples in this section are taken from the MimimalUndoSimpleDemo project (a text editor simulacrum) and provide directions for minimal use of the framework.

Change Project Settings

  1. Add the path to mundo header files to “C/C++ | General | Additional Include Directories”. For the provided demos this path is “$(TargetDir)\I”.
  2. Add the path to mundo library to “Linker | General | Additional Library Directories”. For the provided demos this path is “$(TargetDir)”.

Prepare Your Document Class

  1. Add mundo.h to stdafx.h.
C++
//...
#include "mundo.h"
//...
  1. Add a history object to your document class.
C++
class C_Document
{
//...
public:
  C_Document()
  : m_upHistory( mundo::CreateHistory() )
  {
    //...
  }
protected:
  std::auto_ptr< mundo::C_History > m_upHistory;
//...
};
  1. Add an undo method
C++
void C_Document::Undo()
{
  m_upHistory->Undo();
}
  1. Add a redo method.
C++
void C_Document::Redo()
{
  m_upHistory->Redo();
}

Making Undoable Changes

An editor’s common command is Clear, which erases the contents of the document. A possible implementation would be:

C++
void C_Document::Clear()
{
  if ( ! m_Text.size() )
    return; // nothing to do
  m_Text.clear();
}

Making the Clear command undoable requires one additional line of code:

C++
void C_Document::Clear()
{
  if ( ! m_Text.size() )
    return; // nothing to do
  m_upHistory->Push( mundo::CreateSwap( m_Text ) );
  m_Text.clear();
}

CreateSwap creates a C_Swap interface on the free store. The newly created object is stored on the undo stack, via Push, just before the text content is erased. The swap class is easily created using the helper function mundo::CreateSwap.

For many simple applications, that’s about all you need to know

Grouping

Sometimes you may need to build a command out of other commands, but you would like all to be undone/redone in a single step. To accomplish this you may use the helper class mundo::C_AutoTransaction.

C++
void C_Document::Replace( const char* a_Text )
{
  mundo::C_AutoTransaction at( m_upHistory.get() );
  Clear();
  Append( a_Text );
}

A transaction may include other transactions.

Complex States

For complex states, additional work may be needed. You may need as little as correct copy semantics or as much as a newly derived swap class.

For a swap class example search the drawing demo for C_UndoRedo_Push.

Naming

You may associate a name with a swap object and later retrieve that name:

C++
void C_Document::Clear()
{
  if ( ! m_Text.size() )
    return; // nothing to do
  m_upHistory->Push( mundo::CreateSwap( m_Text ), L"Clear Content" );
  m_Text.clear();
}

A name may be associated with a transaction, too:

C++
void C_Document::Replace( const char* a_Text )
{
  mundo::C_AutoTransaction at( m_upHistory.get(), L"Replace Content" );
  Clear();
  Append( a_Text );
}

You may iterate through steps names using C_History’s methods GetUndoCount, GetRedoCount, GetUndoName, GetRedoName. Retrieving the name of the topmost undo operation:

C++
CString Name = m_upHistory->GetUndoName( 0 );

Space

This version does not provide means for limiting the maximum amount of used memory. You may easily add methods to control the number of maximum steps. Controlling the amount of consumed memory requires more work.

A Final Word

I did not remove the non-undoable functions in the drawing demo; I just commented them out using the NUNDO macro definition. You may define/undefined NUNDO to remove/add undo capability.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)