Undo / Redo in C++

Undo/Redo

I was unable to find any source code for an undo/redo system on the web when I needed it. Maybe this will help someone else in the future.

In the Borland environment (the mere mention of which dates this article something fierce), the Visual Class Library cannot use multiple inheritance with ‘forms’. Multiple inheritance must be accomplished via aggregation. (That held true until BCB6, I believe.) The following framework proved useful for implementing an undo/redo system in those conditions.

  • An ‘UndoFrontEnd’ class was created as a global class to manage the classes that implement undo/redo. You can reimplement it as a Singleton, but I don’t see a good reason to.
  • A virtual base class called ‘Undo’ is created to derive undoable/redoable classes from.
  • A StrClass and a StrClassUndoer are created. The StrClassUndoer is derived from Undo to handle processing of undo/redo logic. Keeping your ‘undoer’ separated from the objects being undone enables you to keep a smaller memory footprint in the case where you have many discrete elements. (In the case of MEdit, it would be very inefficient if each note contained an ‘undoer’ inside of it.)
  • A NumClass is derived from ‘Undo’ to show that it is not necessary to handle the undo/redo processing outside of the class. (The original version of this example only showed integrated processing of this nature.)

These are put together in a small console project showing how everything works together. When ran, the project shows how it is intended to be used, so with that, let us simply show the completed code. A zip file, including an executable, is included after the code.

//Undo.h
#ifndef UndoH #define UndoH #include <vector> class Undo { private: std::vector<Undo*>pastUse; std::vector<Undo*>futureUse; protected: void addUndo(Undo* undoUser); public: virtual ~Undo() { } virtual void undo() = 0; virtual void redo() = 0; virtual void clearFuture() = 0; }; class UndoFrontEnd { //This is meant to be implemented as a global class to manage program undo/redo //requests private: std::vector<Undo*>classesUsingUndo; std::vector<Undo*>futureUse; std::vector<Undo*>pastUse; public: void undo(); void redo(); void addToUndoList(Undo* instance); void addToRedoList(Undo* instance); }; #endif
//Undo.cpp
#include "Undo.h" void UndoFrontEnd::undo() { if (pastUse.empty()) return; pastUse.back()->undo(); futureUse.push_back(pastUse.back()); pastUse.pop_back(); } void UndoFrontEnd::redo() { if (futureUse.empty()) return; futureUse.back()->redo(); pastUse.push_back(futureUse.back()); futureUse.pop_back(); } void UndoFrontEnd::addToUndoList(Undo* instance) { //first, check to see if this 'instance' has already been registered in //the classesUsingUndo vector. If not add it. int count = classesUsingUndo.size(); bool found = false; for (int i=0; i<count; i++) if (classesUsingUndo[i] == instance) found = true; if (!found) { classesUsingUndo.push_back(instance); count++; } //next, clear the futureUse vectors of the classes using undo, as well as this one for (int i=0; i<count; i++) classesUsingUndo[i]->clearFuture(); futureUse.clear(); //now, place the last command on the pastUse vector pastUse.push_back(instance); } void UndoFrontEnd::addToRedoList(Undo* instance) { futureUse.push_back(instance); }
//StrClassUndoer.h
#ifndef StrClassUndoerH #define StrClassUndoerH #include "Undo.h" #include <string> #include <vector> class StrClassUndoer : public Undo { private: std::vector<std::string> futureC; public: void addString(); virtual void undo(); virtual void redo(); virtual void clearFuture(); }; #endif
//StrClassUndoer.cpp
#include "StrClassUndoer.h" #include "StrClass.h" extern UndoFrontEnd undoer; extern StrClass strClass; void StrClassUndoer::addString() { //This simply registers with the UndoFrontEnd, letting it //know that this is the object last acted upon. undoer.addToUndoList(this); } void StrClassUndoer::clearFuture() { futureC.clear(); } void StrClassUndoer::undo() { std::string str = strClass.popBack(); if (str == "") return; futureC.push_back(str); } void StrClassUndoer::redo() { if (futureC.empty()) return; std::string str = futureC.back(); strClass.pushBack(futureC.back().c_str()); futureC.pop_back(); }
//StrClass.h
#ifndef StrClassH #define StrClassH #include <vector> #include <string> class StrClass { private: std::vector<std::string>future; std::vector<std::string>strings; public: //virtual overrides ~StrClass() { } //rest of declarations void push(const char * str); void pushBack(const char * str); //Only used for undoing/redoing std::string popBack(); void printStuff(); }; #endif
//StrClass.cpp
#include "StrClass.h" #include "StrClassUndoer.h" #include <iostream> using namespace std; StrClassUndoer strUndo; void StrClass::push(const char * str) { strings.push_back(str); strUndo.addString(); //this calls clearFuture() } void StrClass::pushBack(const char * str) { //This is only used by the undoer. strings.push_back(str); } std::string StrClass::popBack() { std::string str; if (strings.empty()) str = ""; else { str = strings.back(); strings.pop_back(); } return str; } void StrClass::printStuff() { int count = strings.size(); for (int i=0; i<count; i++) cout << "string [" << i << "]: " << strings[i] << endl; }
//Numbers.h
#ifndef NumbersH #define NumbersH #include <vector> #include "Undo.h" using namespace std; class Numbers : public Undo { private: enum AnAction { Add, Subtract, Push }; struct Actions { AnAction act; int num; }; vector<Actions>past; vector<Actions>future; vector<int>numbers; public: //virtual overrides ~Numbers() { }; void undo(); void redo(); void clearFuture(); //rest of declarations void push(int num); void add(); void subtract(); void printStuff(); }; #endif
//Numbers.cpp
#include "Numbers.h" #include <iostream> using namespace std; extern UndoFrontEnd undoer; void Numbers::push(int num) { numbers.push_back(num); Actions thisAction = { Push, num }; past.push_back(thisAction); undoer.addToUndoList(this); //this calls clearFuture() } void Numbers::add() { int count = numbers.size()-1; if (count < 1) { cout << "Not enough numbers to add\n"; return; } //take care of history Actions thisAction = { Add, numbers.back() }; past.push_back(thisAction); //now do processing numbers[count-1] += numbers[count]; numbers.pop_back(); undoer.addToUndoList(this); //this calls clearFuture() } void Numbers::subtract() { int count = numbers.size()-1; if (count < 1) { cout << "Not enough numbers to subtract\n"; return; } //take care of history Actions thisAction = { Subtract, numbers.back() }; past.push_back(thisAction); //now do processing numbers[count-1] -= numbers[count]; numbers.pop_back(); undoer.addToUndoList(this); //this calls clearFuture() } void Numbers::undo() { if (past.empty()) return; Actions undoneAction; switch (past.back().act) { case Add: undoneAction.act = Add; undoneAction.num = numbers.back(); numbers.back() -= past.back().num; numbers.push_back(past.back().num); break; case Subtract: undoneAction.act = Subtract; undoneAction.num = numbers.back(); numbers.back() += past.back().num; numbers.push_back(past.back().num); break; case Push: undoneAction.act = Push; undoneAction.num = numbers.back(); numbers.pop_back(); break; default: cout << "\nProgrammer needs to update Numbers::undo()\n"; } past.pop_back(); future.push_back(undoneAction); } void Numbers::clearFuture() { future.clear(); } void Numbers::redo() { if (future.empty()) return; Actions redoneAction; int count = numbers.size()-1; switch (future.back().act) { case Add: redoneAction.act = Add; redoneAction.num = numbers.back(); numbers[count-1] += numbers[count]; numbers.pop_back(); break; case Subtract: redoneAction.act = Subtract; redoneAction.num = numbers.back(); numbers[count-1] -= numbers[count]; numbers.pop_back(); break; case Push: redoneAction.act = Push; redoneAction.num = numbers.back(); numbers.push_back(future.back().num); break; default: cout << "\nProgrammer needs to update Numbers::redo()\n"; } past.push_back(redoneAction); future.pop_back(); } void Numbers::printStuff() { if (numbers.empty()) return; cout << "Last vector value (typically your answer) is: " << numbers.back() << endl; int count = numbers.size(); for (int i=0; i<count; i++) cout << "numbers[" << i << "]: " << numbers[i] << endl; }
//And at last, the project file:
#pragma hdrstop #include <condefs.h> #include "Numbers.h" #include "StrClass.h" #include "Undo.h" #include <iostream> #include <string> #include <stdlib.h> USEUNIT("Numbers.cpp"); USEUNIT("StrClass.cpp"); USEUNIT("Undo.cpp"); USEUNIT("StrClassUndoer.cpp"); using namespace std; const int strLen = 100; //Global class instantiation of UndoFrontEnd. For simplicity of example, StrClass //will also be a global. UndoFrontEnd undoer; StrClass strClass; bool isNumber(char* str, int& num) { int len = strlen(str); for (int i=0; i<len; i++) if (!isdigit(str[i])) return false; num = atoi(str); return true; } int main() { Numbers nums; char str[strLen]; cout << "Undo-Redo test, by David O'Neil, 4-27-01\n"; cout << "This test only uses positive integers and strings.\n"; cout << "Use first line character of:\n"; cout << " '-' for subtract command\n '+' for add command\n"; cout << " 'R' for redo command\n 'U' for undo command\n"; cout << "(This only allows positive number input for numbers)\n"; while (1) { int i; cout << "\nEnter something, or 'Enter' to exit: "; cin.getline(str, strLen-1); if (*str == '\0') break; if (*str == '-') nums.subtract(); else if (*str == '+') nums.add(); else if (toupper(*str) == 'U') undoer.undo(); else if (toupper(*str) == 'R') undoer.redo(); else if (isNumber(str, i)) nums.push(i); else strClass.push(str); nums.printStuff(); strClass.printStuff(); } return 0; }

Here is a zip file of the above units (79 kb).

Leave a Reply

Your email address will not be published. Required fields are marked *