Undo Manager
Undo Manager
Introduction
Microsoft has designed a set of interfaces for undo management as part of the OLE framework. This article documents my implementation classes for these interfaces and how they can be used with a demo project.
Although a simple management for undo and redo actions is provided in the demo application, the purpose is to show the use of the interfaces IOleUndoManager
, IOleUndoUnit
and IOleParentUndoUnit
.
The main interface IOleUndoManager
was designed to be used in container applications. Controls can obtain a pointer to the undo manager from the IServiceProvider
interface using the guid SID_SOleUndoManager
. By adding undo units to the hosts undo manager, controls can participate in centralized undo management.
Undo units can be nested by the use of parent undo units. This make it possible to group complex actions together so an end user can undo and redo them with one command. The undo manager only invokes the top level undo units as a whole.
The host application determines the scope of the undo manager. A scope could be at the document level, providing one undo manager for each document.
Changes since last update
- Fixed bug in
COleUndoManagerImpl
UndoTo
andRedoTo
implementations when using nonNULL
argument - Updated demo project with Undo To and Redo To menu items
Class Overview
The following classes implement the Ole Undo related interfaces.
The following class is a helper class.
COleUndoManagerImpl Manages the undo and redo stacks IOleUndoUnitImpl Basic implementation of an undo unit IOleParentUndoUnitImpl Manages child undo units
CComClassID Replacement for the CComCoClass
to implement theGetObjectCLSID
function
Requirements
- ATL
- STL
- Demo project uses WTL
Class Reference
COleUndoManagerImpl
template <class T>
class ATL_NO_VTABLE COleUndoManagerImpl : public IOleUndoManager
Parameters T Your class, derived from COleUndoManagerImpl The undo manager provides a centralized undo and redo service. It manages parent undo units and simple undo units on the undo and redo stacks. An object or, a control, can deposit undo units on these stacks by calling methods in the undo manager.
The centralized undo manager has the data necessary to support the undo and redo user interface for the host application and can discard undo information gradually as the stack becomes full.
IOleUndoUnitImpl
template <class T, LONG lTypeID=0>
class ATL_NO_VTABLE IOleUndoUnitImpl : public IOleUndoUnit
Parameters T Your class, derived from IOleUndoUnitImpl lTypeID Identifier that together with the CLSID uniquely identifies this type of undo unit The
IOleUndoUnit
interface is the main interface for an undo unit. An undo unit encapsulates the information necessary to undo or redo a single action.The actions and data necessary to execute the undo or redo is to be provided by the implementor of the derived class.
When using this template the derived class needs to implement the following methods
HRESULT IOleUndoUnitImpl_Do(IOleUndoManager* /*pUndoManager*/);
HRESULT IOleUndoUnitImpl_CreateUndoUnit(IOleUndoUnit** /*ppUU*/);
STDMETHOD(GetDescription)(BSTR* pBstr);
IOleParentUndoUnitImpl
template <class T, LONG lTypeID=0>
class ATL_NO_VTABLE IOleParentUndoUnitImpl : public IOleParentUndoUnit
Parameters T Your class, derived from COleUndoManagerImpl lTypeID Identifier that together with the CLSID uniquely identifies this type of undo unit The
IOleParentUndoUnit
interface enables undo units to contain child undo units. For example, a complex action can be presented to the end user as a single undo action even though a number of separate actions are involved. All the subordinate undo actions are contained within the top-level parent undo unit.When using this template the derived class needs to implement the following methods
HRESULT IOleParentUndoUnitImpl_CreateParentUndoUnit(IOleParentUndoUnit** /*ppPUU*/);
STDMETHOD(GetDescription)(BSTR* pBstr);
CComClassID
template <const CLSID* pclsid = &CLSID_NULL>
class CComClassID
Parameters pclsid A pointer to the CLSID of the object When creating simple ATL Objects, not deriving from
CComCoClass
, this class can be used to provide implementation for the static member functionGetObjectCLSID
.GetObjectCLSID
is used to implement theGetUnitType
menber of theIOleUndoUnit
interface.
Demo project
The demo project was created with the WTL application wizard as an SDI application with enabled hosting of ActiveX controls and with a generic view.
To demonstrate the usage of the undo manager I decided to create a simple graphics application. A single click with the mouse in the client area adds a graphic object at the location of the mouse pointer. An undo operation is added to the undo manager to remove the object just added. When an undo command is executed the graphic is removed from the display and a redo object is added to the undo manager.
The Main Frame
Here is how the CMainFrame
class is modified to make use of the IOleUndoManager
interface, implemented by CUndoManager
defined below.
class ATL_NO_VTABLE CUndoManager :
public CComObjectRootEx<CComSingleThreadModel>,
public COleUndoManagerImpl<CUndoManager>
{
public:
CUndoManager() {}
BEGIN_COM_MAP(CUndoManager)
COM_INTERFACE_ENTRY(IOleUndoManager)
END_COM_MAP()
};
An instance of the undo manager is created when the main window is created.
CComPtr<IOleUndoManager> m_spUndoMgr;
LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
// Create the UndoManager
CComObject<CUndoManager>* pObj = NULL;
HRESULT hr = pObj->CreateInstance(&pObj);
if (SUCCEEDED(hr))
{
pObj->AddRef();
hr = pObj->QueryInterface(&m_spUndoMgr);
pObj->Release();
}
The Redo menu item has to be manually added to the main frame menu. Don't forget to update the Accelerator table to make Ctrl+Y
work.
To keep the undo and redo menu items up to date, add them to the update ui map and add a UIUpdateUndoRedo
method to be called from OnIdle
.
// Added update for Undo and Redo
UPDATE_ELEMENT(ID_EDIT_UNDO, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)
UPDATE_ELEMENT(ID_EDIT_REDO, UPDUI_MENUPOPUP | UPDUI_TOOLBAR)
END_UPDATE_UI_MAP()
void UIUpdateUndoRedo()
{
USES_CONVERSION;
CComBSTR undoDesc;
HRESULT hr = m_spUndoMgr->GetLastUndoDescription(&undoDesc);
if (SUCCEEDED(hr))
{
// Format the undo string and update the undo text
CString str;
str.Format(_T("Undo %s"), OLE2CT(undoDesc));
UIEnable(ID_EDIT_UNDO, TRUE);
UISetText(ID_EDIT_UNDO, str);
}
else
{
// No undo exists
UIEnable(ID_EDIT_UNDO, FALSE);
}
CComBSTR redoDesc;
hr = m_spUndoMgr->GetLastRedoDescription(&redoDesc);
if (SUCCEEDED(hr))
{
// Format the redo string and update the redo text
CString str;
str.Format(_T("Redo %s"), OLE2CT(redoDesc));
UIEnable(ID_EDIT_REDO, TRUE);
UISetText(ID_EDIT_REDO, str);
}
else
{
// No redo exists
UIEnable(ID_EDIT_REDO, FALSE);
}
}
virtual BOOL OnIdle()
{
// Update undo - redo state
UIUpdateUndoRedo();
In the event handlers for the undo and redo commands. Make a call to the undo manager method UndoTo
or RedoTo
with a NULL
argument to undo or redo the last action.
// Undo last action
HRESULT hr = m_spUndoMgr->UndoTo(NULL);
if (FAILED(hr))
MessageBox(_T("Undo failed"),_T("Error"));
// Redo last action
HRESULT hr = m_spUndoMgr->RedoTo(NULL);
if (FAILED(hr))
MessageBox(_T("Redo failed"),_T("Error"));
The Graphics
In order to demonstrate how to implement undo units I had to have something that actually worked. So, I made a simple set of graphics classes representing a box, an ellipse and a round rectangle. They all derive from a base class CGUIObject
that defines two virtual functions; Draw
and Description
.
The following code is in a separate header file named gdigraph.h.
class CGUIObject
{
public:
...
virtual void Draw(HDC hDc) = 0;
virtual LPCWSTR Description() = 0;
};
class CGUIBox : public CGUIObject;
class CGUIEllipse : public CGUIObject;
class CGUIRoundRect : public CGUIObject
{
public:
...
virtual void Draw(HDC hDc)
{
CDCHandle dc(hDc);
POINT pt = { m_size.cx / 4, m_size.cy / 4 };
RECT rc = GetRect();
dc.RoundRect(&rc, pt);
}
virtual LPCWSTR Description()
{
return L"Create round rect";
}
};
All gui objects are stored with a unique identifier in an object map.
typedef std::map<long, CGUIObject*> GUIObjectMap;
A function object is used with the for_each
algorithm to draw the objects to a Device Context
.
struct DrawFunctor
{
HDC m_dc;
DrawFunctor(HDC dc) : m_dc(dc) {}
void operator()(std::pair<long, CGUIObject*> p)
{
p.second->Draw(m_dc);
}
};
A context class has static members which points to an active object map and a map for deleted objects. In the demo project the context members are intitialized when the view is created.
class GUIContext
{
public:
void static Initialize(GUIObjectMap* pA, GUIObjectMap* pD)
{
pActiveMap = pA;
pDeletedMap = pD;
}
static GUIObjectMap* pActiveMap;
static GUIObjectMap* pDeletedMap;
};
GUIObjectMap* GUIContext::pActiveMap;
GUIObjectMap* GUIContext::pDeletedMap;
A set of helper functions are used to create the undo and redo units.
HRESULT CreateUndoUnit(long id, IOleUndoUnit** ppUU);
HRESULT CreateRedoUnit(long id, IOleUndoUnit** ppUU);
HRESULT CreateGroupUnit(IOleParentUndoUnit** ppPUU);
Here is the implementation of the undo unit.
class ATL_NO_VTABLE CUndoUnit :
public CComObjectRootEx<CComSingleThreadModel>,
public IOleUndoUnitImpl<CUndoUnit>,
public CComClassID<>
{
public:
CUndoUnit() { }
BEGIN_COM_MAP(CUndoUnit)
COM_INTERFACE_ENTRY(IOleUndoUnit)
END_COM_MAP()
long m_id;
The IOleUndoUnitImpl_Do
method is called from the Do
method provided by the IOleUndoUnitImpl
class. Here we simply need to find the object in the active map, remove it and add it to the deleted map. We should return either S_OK
indicating success or E_ABORT
.
HRESULT IOleUndoUnitImpl_Do(IOleUndoManager* /*pUndoManager*/)
{
GUIObjectMap::iterator iter = GUIContext::pActiveMap->find(m_id);
if ( iter != GUIContext::pActiveMap->end() )
{
GUIContext::pDeletedMap->insert(*iter);
GUIContext::pActiveMap->erase(iter);
return S_OK;
}
// The object map has become corrupt.
return E_ABORT;
}
If we succeeded with the operation above, we are asked for a undo unit that can redo the operation we just undone. This unit is added to the undo manager which will put it on the redo stack. Here we just call the helper function.
HRESULT IOleUndoUnitImpl_CreateUndoUnit(IOleUndoUnit** ppUU)
{
HRESULT hr = CreateRedoUnit(m_id, ppUU);
return SUCCEEDED(hr) ? S_OK : E_ABORT;
}
We also need to implement the GetDescription
method.
STDMETHOD(GetDescription)(BSTR* pBstr)
{
GUIObjectMap::iterator iter = GUIContext::pActiveMap->find(m_id);
if ( iter != GUIContext::pActiveMap->end() )
{
*pBstr = ::SysAllocString((*iter).second->Description());
return S_OK;
}
return E_FAIL;
}
};
The redo unit is implemented in the same way except that it inverts the use of the active and the deleted object map. Also we only need one pair of undo and redo units since all graphic objects have the same interface.
In the demo project there is also the notion of groups by using the IOleParentUndoUnit
interface to nest a set of actions together so they can be treated as one single action.
class ATL_NO_VTABLE CGroupUnit :
public CComObjectRootEx<CComSingleThreadModel>,
public IOleParentUndoUnitImpl<CGroupUnit>,
public CComClassID<>
{
public:
CGroupUnit() { }
BEGIN_COM_MAP(CGroupUnit)
COM_INTERFACE_ENTRY(IOleParentUndoUnit)
COM_INTERFACE_ENTRY(IOleUndoUnit)
END_COM_MAP()
We need to implement the IOleParentUndoUnitImpl_CreateParentUndoUnit
method. Here we use the helper function wich actually only creates a new instance of the same class.
HRESULT IOleParentUndoUnitImpl_CreateParentUndoUnit(IOleParentUndoUnit** ppPUU)
{
HRESULT hr = CreateGroupUnit(ppPUU);
return SUCCEEDED(hr) ? S_OK : E_ABORT;
}
Again we also need to implement the GetDescription
method.
STDMETHOD(GetDescription)(BSTR* pBstr)
{
*pBstr = ::SysAllocString(L"Create group");
return S_OK;
}
};
The View
The only thing left to do is plugging the graphics classes into the view.
The constructor initializes a counter used for unique id's and the graphic context class is initialized with pointers to the active and deleted map.
CUndomgr_wtlView()
{
// Reset counter
m_id = 0;
// Intialize graphic context class, used for undo - redo operations
GUIContext::Initialize(&m_displayMap, &m_deletedMap);
}
// Added IOleUndoManager as a member
CComPtr<IOleUndoManager> m_spUndoMgr;
// Added an object map for display objects and one for deleted objects
GUIObjectMap m_displayMap;
GUIObjectMap m_deletedMap;
// Counter for unique identifiers
long m_id;
In the OnPaint
method the DrawFunctor
class is used with the for_each
algorithm to draw all objects in the display map.
LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
CPaintDC dc(m_hWnd);
// Draw each graphic object in the display map
std::for_each(m_displayMap.begin(), m_displayMap.end(), DrawFunctor(dc));
return 0;
}
The OnLButtonUP
method calls the CreateGuiObject
or CreateGroupObject
dependent on the value of the current m_id
counter.
Here a new graphic object is added to the display map. A CUndoUnit
is also created and added to the undo manager which puts it on the undo stack.
void CreateGuiObject(LONG type, POINT pt)
{
CGUIObject* p = NULL;
switch (type)
{
case 0:
p = new CGUIBox(pt);
break;
case 1:
p = new CGUIRoundRect(pt);
break;
case 2:
p = new CGUIEllipse(pt);
break;
}
if (p)
{
// Increment counter
m_id++;
// Insert the object in the display map by id
m_displayMap.insert(GUIObjectMap::value_type(m_id, p));
// Add object to the undo stack
CComPtr<IOleUndoUnit> spUU;
HRESULT hr = CreateUndoUnit(m_id, &spUU);
if (SUCCEEDED(hr))
{
hr = m_spUndoMgr->Add(spUU);
}
}
}
This method creates a new CGroupUnit
which implements the IOleParentUndoUnit
interface. When calling Open
on the IOleUndoManager
interface with this parent unit, subsequent calls to Add
on the undo manager will put the IOleUndoUnits in this parent unit. After creating a few graphic objects we close the parent unit commiting it to the undo stack.
void CreateGuiGroup(POINT pt)
{
// Create parent undo unit to group units into one action
CComPtr<IOleParentUndoUnit> spPUU;
HRESULT hr = CreateGroupUnit(&spPUU);
if (SUCCEEDED(hr))
{
// Open the parent unit, following units added through the
// manager will be added to this unit.
hr = m_spUndoMgr->Open(spPUU);
if (SUCCEEDED(hr))
{
POINT pt0, pt1, pt2, pt3;
pt0.x = pt.x - 20; pt0.y = pt.y - 20;
pt1.x = pt.x - 20; pt1.y = pt.y + 20;
pt2.x = pt.x + 20; pt2.y = pt.y - 20;
pt3.x = pt.x + 20; pt3.y = pt.y + 20;
CreateGuiObject(0,pt0); // box
CreateGuiObject(1,pt1); // round rect
CreateGuiObject(2,pt2); // ellipse
CreateGuiObject(0,pt3); // box
// Commit to undo stack
m_spUndoMgr->Close(spPUU, TRUE);
}
}
// Increment counter, otherwise we will create only groups from now
m_id++;
}
This concludes the demo project. Please note that this sample doesn't provide any memory management. Graphic objects created are never deleted. The purpose is only to show how IOleUndoUnit
s and IOleParentUndoUnit
s can be used together with the undo manager to provide a vehicle for implementing nested undo and redo operations.
Appendix
More information about these interfaces can be found in the MSDN Online Library.
- About the IOleUndoManger
- About the IOleUndoUnit
- About the IOleParentUndoUnit
Description of the IOleUndoManager
, IOleUndoUnit
and IOleParentUndoUnit
interfaces found here is extracted from the MS Platform SDK documentation.
IOleUndoManager Methods | Description |
Open (IOleParentUndoUnit* pPUU) | Opens a new parent undo unit, which becomes part of its containing unit's undo stack. |
Close (IOleParentUndoUnit* pPUU, BOOL fCommit) | Closes the specified parent undo unit. |
Add (IOleUndoUnit* pUU) | Adds a simple undo unit to the collection. |
GetOpenParentState (DWORD* pdwState) | Returns state information about the innermost open parent undo unit. |
DiscardFrom (IOleUndoUnit* pUU) | Instructs the undo manager to discard the specified undo unit and all undo units below it on the undo or redo stack. |
UndoTo (IOleUndoUnit* pUU) | Instructs the undo manager to perform actions back through the undo stack, down to and including the specified undo unit. |
RedoTo (IOleUndoUnit* pUU) | Instructs the undo manager to invoke undo actions back through the redo stack, down to and including the specified undo unit. |
EnumUndoable (IEnumOleUndoUnits** ppEnum) | Creates an enumerator object that the caller can use to iterate through a series of top-level undo units from the undo stack. |
EnumRedoable (IEnumOleUndoUnits** ppEnum) | Creates an enumerator object that the caller can use to iterate through a series of top-level undo units from the redo stack. |
GetLastUndoDescription (BSTR* pBstr) | Returns the description for the top-level undo unit that is on top of the undo stack. |
GetLastRedoDescription (BSTR* pBstr) | Returns the description for the top-level undo unit that is on top of the redo stack. |
Enable (BOOL fEnable) | Enables or disables the undo manager. |
IOleUndoUnit Methods | Description |
Open (IOleParentUndoUnit* pPUU) | Opens a new parent undo unit, which becomes part of its containing unit's undo stack. |
Do (IOleUndoManager* pUndoManager) | Instructs the undo unit to carry out its action. |
GetDescription (BSTR* pBstr) | Returns a string that describes the undo unit and can be used in the undo or redo user interface. |
GetUnitType (CLSID* pClsid, LONG* plID) | Returns the CLSID and a type identifier for the undo unit. |
OnNextAdd () | Notifies the last undo unit in the collection that a new unit has been added. |
IOleParentUndoUnit Methods | Description |
Open (IOleParentUndoUnit* pPUU) | Opens a new parent undo unit, which becomes part of the containing unit's undo stack. |
Close (IOleParentUndoUnit* pPUU, BOOL fCommit) | Closes the most recently opened parent undo unit. |
Add (IOleUndoUnit* pUU) | Adds a simple undo unit to the collection. |
FindUnit (IOleUndoUnit* pUU) | Indicates if the specified unit is a child of this undo unit or one of its children, that is if the specified unit is part of the hierarchy in this parent unit. |
GetParentState (DWORD* pdwState) | Returns state information about the innermost open parent undo unit. |
License
This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.
A list of licenses authors might use can be found here