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:
- A swap interface,
C_Swap
. - A history interface,
C_History
. - A history creator,
CreateHistory
. - An automatic transaction opener/coser,
C_AutoTransaction
. - 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
- Add the path to
mundo
header files to “C/C++ | General | Additional Include Directories”. For the provided demos this path is “$(TargetDir)\I”. - Add the path to
mundo
library to “Linker | General | Additional Library Directories”. For the provided demos this path is “$(TargetDir)”.
Prepare Your Document Class
- Add
mundo.h
to stdafx.h
.
#include "mundo.h"
- Add a history object to your document class.
class C_Document
{
public:
C_Document()
: m_upHistory( mundo::CreateHistory() )
{
}
protected:
std::auto_ptr< mundo::C_History > m_upHistory;
};
- Add an
undo
method
void C_Document::Undo()
{
m_upHistory->Undo();
}
- Add a
redo
method.
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:
void C_Document::Clear()
{
if ( ! m_Text.size() )
return; m_Text.clear();
}
Making the Clear
command undoable requires one additional line of code:
void C_Document::Clear()
{
if ( ! m_Text.size() )
return; 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
.
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:
void C_Document::Clear()
{
if ( ! m_Text.size() )
return; m_upHistory->Push( mundo::CreateSwap( m_Text ), L"Clear Content" );
m_Text.clear();
}
A name may be associated with a transaction, too:
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:
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.