Multiple Level Undo/Redo

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

INSTRUCTIONS


These are the steps used in the project file to add multi-level undo/redo capability to the Scribble application.

Copy Files



  • Copy scribble tutorial files (I used step 3) into new folder.
  • Copy “command” and “ref object” files into the folder and add them to the project. You could also put them in a library which would be more convenient. These files are:

    Command.hCommand.cppCommandHistory.hCommandHistory.cppRefObject.hRefObject.cpp

  • Add includes for RefObject.h, Command.h, and CommandHistory.h to stdafx.h
  • Edit Resources


  • Add undo/redo buttons to the IDR_MAINFRAME toolbar. Make sure their id’s are ID_EDIT_UNDO and ID_EDIT_REDO (there are string table entries for both in MFC). You can grab the images from this project.
  • Add “&Redo” to the IDR_SCRIBBTYPE edit menu and use ID_EDIT_REDO command id.
  • CStroke Changes


  • Derive CStroke from CRefObject instead of CObject
  • Change CStroke’s IMPLEMENT_SERIAL macro to use new base class
  • Under “Operations” add GetBoundingRect declaration to CStroke:

    CRect GetBoundingRect() const;

  • Add CStroke::GetBoundingRect definition to ScribDoc.cpp
  • Immediately after class definition, add CStrokeList typedef for easy use:

    typedef CTypedRefObList< CStroke* > CStrokeList;

  • CScribbleDoc Changes


  • Add class ICustomCommandHistory to override OnCommandExecute
  • Multiply derive CScribbleDoc from ICustomCommandHistory
  • Change m_strokeList’s type from CTypedPtrList< CObList,CStroke* > to CStrokeList
  • Add GetPenWidth member
  • Delete NewStroke declaration and definition
  • Add AddStroke, AddStrokes, RemoveStroke, and RemoveStrokes declarations and definitions
  • Modify CScribbleDoc::DeleteContents definition as shown. Notice that we do not delete the strokes in the list as this is handled by the CRefObList class
  • Add command handlers for ID_EDIT_UNDO and ID_EDIT_REDO (both Command and update handlers).
  • Modified CScribbleDoc::OnEditClearAll to use a command (so that the clear all event can be undone).
  • Add include for ScribVw.h (to see command declarations). This is not nessassary if you move the ID_EDIT_CLEARALL handler to the view class. It is my personal preference to have the view generate commands for the doc rather than letting the doc make commands on it’s own.
  • CScribbleView Changes


  • Add command enumeration
  • Add command declarations/definitions
  • Make changes to CScribbleView::OnLButtonDown and CScribbleView::OnLButtonUp
  • Add OnUpdate override to the view
  • CScribbleView::OnDraw strokeList variable’s type changed to const CStrokeList&
  • DESCRIPTION

    There are many ways you could implement multi-level undo/redo in your
    applications. This mechanism and the provided classes represent my personal
    preference. Perhaps you will take what you learn here and build something
    more suited to your own taste.

    The first change we made to Scribble was to derive the CStroke objects from
    CRefObject rather than CObject. If you look at CRefObject you will notice
    that it’s brutally simple, and works much like COM objects do. The advantage
    is it’s companion class CRefObList. It is identical to CObList except that
    it automatically implements reference counting on objects it stores.

    The reason we implement reference counting on our stroke objects is that
    they need to remain in memory even after they have been deleted. In order to
    undo a stroke, we need to remember what that stroke was. So each of the
    commands maintains a pointer (and a reference count) to the original stroke
    object.

    We also added a new member to CStroke, GetBoundingRect. GetBoundingRect
    exists so that we can remove a stroke by invalidating the region where it
    used to exist.

    Next we added a base class to CScribbleDoc, ICommandHistory.
    ICommandHistory is our undo/redo buffer (and interface to it). It’s what
    turns the ordinary CDocument into a multi-level undo/redo document.

    Let’s talk about commands. The commands are what give us the undo/redo
    capability. The basic rule for using commands is: Any time you would call
    SetModifiedFlag(TRUE) on the document, do the action through a command. Once
    you commit to multi-level undo/redo, you cannot cheat. Any action that
    modifies the document must have a command (and subsequent undo command), and
    must be done via the command mechanism.

    Note that in some cases, you can reuse commands for both undo and redo. Take
    for instance a move object command:

    CCmdMoveObject::CCmdMoveObject(CObject* pObject, CSize sizeOffset);
    CCmdMoveObject::GetUndoCommand()
    	{ return new CCmdMoveObject(m_pObject, -m_sizeOffset); }
    CCmdMoveObject::GetRedoCommand()
    	{ return new CCmdMoveObject(m_pObject, m_sizeOffset); }
    

    Originally in the CScribbleView::OnLButtonUp, we asked the document to
    create a new Stroke and add it to it’s list before the stroke was even
    defined (CScribbleDoc::NewStroke). We then defined the stroke as the user
    moved the mouse around. Now the view creates a stroke object, defines it as
    the user moves the mouse and then creates an “add stroke” command which it
    passes to the document (via it’s new Do member) for the document to execute.

    One benefit of this is that we can elect not to add a stroke (or whatever
    objects we are creating) at this point. One reason may be to prevent
    duplicate strokes from being entered into the document’s list, or perhaps
    your application has some form of rules that can be checked here and validated
    before the command is even issued.

    At this point it’s your choice to either let the command manipulate the
    document’s contents itself, or to do it via some interface to the document.
    I choose the latter because it better encapsulates a document’s functionality,
    and keeps my command objects fairly simple. The document’s Do member calls
    the command’s Execute member which in turn asks the document to add the stroke
    that is has a reference to.

    The Do member also logs this command (if it’s Execute member returns TRUE),
    and now it can be undone simply by calling CScribbleDoc::Undo.

    The last change we made to scribble was to add an OnUpdate handler to the
    view. After a command executes, the virtual OnCommandExecute member is called.
    We overrode this member to display some text on the status bar and to call
    UpdateAllViews, passing the command as the hint. OnUpdate then updates the view
    to reflect the document change that occured by the execution of the command.
    Using this override is simpler than having everyone of your commands call
    UpdateAllViews on successfull execution.

    The commands are given a static m_nID member through the use of the
    DECLARE_COMMAND and IMPLEMENT_COMMAND macro’s. This ID can be accessed with the
    COMMAND_ID macro. The use of the static ID allows us to use a case statement
    to determine what command it is, rather than a long list of if (IsKindOf)s.
    You can even have a base class for a group of commands and give the base class
    a command ID, but not the derived classes (use DECLARE_COMMAND_NOID and
    IMPLEMENT_COMMAND_NOID). This is handy if you have a bunch of commands that
    are updated in the same way and so you only need one update case that can
    update any of the commands in this group.


    USING THE COMMAND HISTORY “CLEAN” STATE

    Normally you use CDocument’s m_bModified member (through SetModifiedFlag
    and IsModified) to maintain the “clean/dirty” state of the document. When using
    multi-level undo/redo, a document can be returned to a “clean” state by use of
    the undo or redo commands.

    There are two steps you need to take to use the command history’s “clean”
    state to reflect the state of the document:


  • Override CDocument::IsModified to return IsDirty();
  • Override CDocument::OnSaveDocument to call SetClean(); if the document was properly saved.
  • As a precaution, I override CDocument::SetModifiedFlag to do nothing at
    all. You don’t have to do this as long as your app never calls
    SetModifiedFlag.

    Note that you do not manually dirty the document. The command history handles
    it’s “dirty” state. You only have to tell it when the document has been saved.

    NOTIFYING THE USER OF THE DOCUMENT’S “CLEAN” STATE

    You can show the user a “dirty flag” in the document’s title by following
    these steps:


  • Override ICommandHistory::OnStatusChange. Have ICustomCommandHistory::OnStatusChange call an UpdateTitle member of the document.
  • Add m_strTitleOrg member to CScribbleDoc.
  • Add UpdateTitle member to CScribbleDoc.
  • Override CDocument::SetTitle to set m_strTitleOrg.
  • Note that if CDocument::GetTitle had been virtual, the m_strTitleOrg and
    the messy manipulation of the title would have been unnessassary, but I don’t
    know another way to handle it.

    Download demo project – 45KB

    More by Author

    Get the Free Newsletter!

    Subscribe to Developer Insider for top news, trends & analysis

    Must Read