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
and RedoTo
implementations when using non NULL
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.
CComClassID | Replacement for the CComCoClass to implement the GetObjectCLSID function |
Requirements
- ATL
- STL
- Demo project uses WTL
Class Reference
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.
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* );
HRESULT IOleUndoUnitImpl_CreateUndoUnit(IOleUndoUnit** );
STDMETHOD(GetDescription)(BSTR* pBstr);
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** );
STDMETHOD(GetDescription)(BSTR* pBstr);
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 function
GetObjectCLSID
. GetObjectCLSID
is used to implement the
GetUnitType
menber of the IOleUndoUnit
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 , WPARAM , LPARAM , BOOL& )
{
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
.
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))
{
CString str;
str.Format(_T("Undo %s"), OLE2CT(undoDesc));
UIEnable(ID_EDIT_UNDO, TRUE);
UISetText(ID_EDIT_UNDO, str);
}
else
{
UIEnable(ID_EDIT_UNDO, FALSE);
}
CComBSTR redoDesc;
hr = m_spUndoMgr->GetLastRedoDescription(&redoDesc);
if (SUCCEEDED(hr))
{
CString str;
str.Format(_T("Redo %s"), OLE2CT(redoDesc));
UIEnable(ID_EDIT_REDO, TRUE);
UISetText(ID_EDIT_REDO, str);
}
else
{
UIEnable(ID_EDIT_REDO, FALSE);
}
}
virtual BOOL OnIdle()
{
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.
HRESULT hr = m_spUndoMgr->UndoTo(NULL);
if (FAILED(hr))
MessageBox(_T("Undo failed"),_T("Error"));
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* )
{
GUIObjectMap::iterator iter = GUIContext::pActiveMap->find(m_id);
if ( iter != GUIContext::pActiveMap->end() )
{
GUIContext::pDeletedMap->insert(*iter);
GUIContext::pActiveMap->erase(iter);
return S_OK;
}
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()
{
m_id = 0;
GUIContext::Initialize(&m_displayMap, &m_deletedMap);
}
CComPtr<IOleUndoManager> m_spUndoMgr;
GUIObjectMap m_displayMap;
GUIObjectMap m_deletedMap;
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 , WPARAM , LPARAM , BOOL& )
{
CPaintDC dc(m_hWnd);
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)
{
m_id++;
m_displayMap.insert(GUIObjectMap::value_type(m_id, p));
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)
{
CComPtr<IOleParentUndoUnit> spPUU;
HRESULT hr = CreateGroupUnit(&spPUU);
if (SUCCEEDED(hr))
{
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);
CreateGuiObject(1,pt1);
CreateGuiObject(2,pt2);
CreateGuiObject(0,pt3);
m_spUndoMgr->Close(spPUU, TRUE);
}
}
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.
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. |
|