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 IOleUndoManagerIOleUndoUnit 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.

COleUndoManagerImpl Manages the undo and redo stacks
IOleUndoUnitImpl Basic implementation of an undo unit
IOleParentUndoUnitImpl Manages child undo units
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

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 function GetObjectCLSIDGetObjectCLSID is used to implement the GetUnitType menber of the IOleUndoUnit interface.

Demo project

Image of undo action

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 IOleUndoUnits and IOleParentUndoUnits 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 IOleUndoManagerIOleUndoUnit 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

posted @ 2022-12-13 14:50  小风风的博客  阅读(78)  评论(0编辑  收藏  举报