65.9K
CodeProject 正在变化。 阅读更多。
Home

撤销管理器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (9投票s)

2001 年 9 月 5 日

9分钟阅读

viewsIcon

123046

downloadIcon

3369

一篇关于管理撤销和重做操作的文章

引言

Microsoft 在 OLE 框架中设计了一套用于撤销管理的接口。本文档介绍了我的这些接口的实现类以及它们如何在演示项目中使用的。

尽管演示应用程序中提供了简单的撤销和重做操作管理,但其目的是展示 IOleUndoManagerIOleUndoUnitIOleParentUndoUnit 接口的用法。

主接口 IOleUndoManager 是为容器应用程序设计的。控件可以通过 IServiceProvider 接口,使用 GUID SID_SOleUndoManager 来获取撤销管理器的指针。通过将撤销单元添加到宿主撤销管理器中,控件可以参与集中式的撤销管理。

撤销单元可以通过父撤销单元进行嵌套。这使得可以将复杂的多个操作组合在一起,以便最终用户可以用一个命令撤销和重做它们。撤销管理器只会将顶层撤销单元作为一个整体来调用。

宿主应用程序决定了撤销管理器的范围。范围可以是文档级别的,为每个文档提供一个撤销管理器。

上次更新以来的更改

  • 修复了在使用非 NULL 参数时 COleUndoManagerImplUndoToRedoTo 实现中的错误
  • 更新了演示项目,添加了“撤销到”和“重做到”菜单项

类概述

以下类实现了 Ole 撤销相关的接口。

COleUndoManagerImpl管理撤销和重做堆栈
IOleUndoUnitImpl撤销单元的基本实现
IOleParentUndoUnitImpl管理子撤销单元
以下类是一个辅助类。
CComClassIDCComCoClass 的替代,用于实现 GetObjectCLSID 函数

要求

  • ATL
  • STL
  • 演示项目使用 WTL

类参考

COleUndoManagerImpl

template <class T>
class ATL_NO_VTABLE COleUndoManagerImpl : public IOleUndoManager

参数
T派生自 COleUndoManagerImpl 的类

撤销管理器提供集中的撤销和重做服务。它在撤销和重做堆栈上管理父撤销单元和简单的撤销单元。对象或控件可以通过调用撤销管理器中的方法,将撤销单元存入这些堆栈。

集中的撤销管理器拥有支持宿主应用程序的撤销和重做用户界面的必要数据,并且可以随着堆栈变满而逐渐丢弃撤销信息。

IOleUndoUnitImpl

template <class T, LONG lTypeID=0>
class ATL_NO_VTABLE IOleUndoUnitImpl : public IOleUndoUnit

参数
T派生自 IOleUndoUnitImpl 的类
lTypeID标识符,它与 CLSID 一起唯一标识此类型的撤销单元

IOleUndoUnit 接口是撤销单元的主接口。撤销单元封装了撤销或重做单个操作所需的信息。

执行撤销或重做所需的操作和数据应由派生类的实现者提供。

使用此模板时,派生类需要实现以下方法

  • 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

参数
T派生自 COleUndoManagerImpl 的类
lTypeID标识符,它与 CLSID 一起唯一标识此类型的撤销单元

IOleParentUndoUnit 接口使撤销单元能够包含子撤销单元。例如,一个复杂的动作可以被呈现给最终用户为一个单独的撤销动作,即使它涉及多个单独的操作。所有从属的撤销操作都包含在顶层父撤销单元中。

使用此模板时,派生类需要实现以下方法

  • HRESULT IOleParentUndoUnitImpl_CreateParentUndoUnit(IOleParentUndoUnit** /*ppPUU*/);
  • STDMETHOD(GetDescription)(BSTR* pBstr);

CComClassID

template <const CLSID* pclsid = &CLSID_NULL>
class CComClassID

参数
pclsid指向对象 CLSID 的指针

创建简单的 ATL 对象,而不派生自 CComCoClass 时,可以使用此类为静态成员函数 GetObjectCLSID 提供实现。GetObjectCLSID 用于实现 IOleUndoUnit 接口的 GetUnitType 成员。

演示项目

Image of undo action

演示项目是使用 WTL 应用程序向导创建的,作为 SDI 应用程序,启用了 ActiveX 控件托管和通用视图。

为了演示撤销管理器的用法,我决定创建一个简单的图形应用程序。在客户端区域单击鼠标一次会在鼠标指针处添加一个图形对象。一个撤销操作被添加到撤销管理器中,用于移除刚刚添加的对象。当执行撤销命令时,图形会从显示中移除,一个重做对象会被添加到撤销管理器中。

主框架

以下是如何修改 CMainFrame 类以利用 IOleUndoManager 接口,该接口由下面的 CUndoManager 实现。

class ATL_NO_VTABLE CUndoManager : 
	public CComObjectRootEx<CComSingleThreadModel>,
	public COleUndoManagerImpl<CUndoManager>
{
public:
	CUndoManager() {}

BEGIN_COM_MAP(CUndoManager)
	COM_INTERFACE_ENTRY(IOleUndoManager)
END_COM_MAP()

};

撤销管理器的一个实例在主窗口创建时被创建。

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();
	}		

必须手动将“重做”菜单项添加到主框架菜单中。不要忘记更新加速键表,以使 Ctrl+Y 生效。

为了使撤销和重做菜单项保持最新,将它们添加到更新 UI 映射中,并添加一个 UIUpdateUndoRedo 方法,该方法应从 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();

在撤销和重做命令的事件处理程序中。调用撤销管理器方法 UndoToRedoTo,并传递一个 NULL 参数来撤销或重做最后一个操作。

	// 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"));

图形

为了演示如何实现撤销单元,我必须要有实际可用的东西。因此,我创建了一组简单的图形类,代表一个矩形、一个椭圆和一个圆角矩形。它们都派生自基类 CGUIObject,该类定义了两个虚拟函数:DrawDescription

以下代码位于名为 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";
	}
};

所有 GUI 对象都通过唯一标识符存储在对象映射中。

typedef std::map<long, CGUIObject*> GUIObjectMap;

函数对象与 for_each 算法一起使用,将对象绘制到 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);
	}
};

一个上下文类具有指向活动对象映射和已删除对象映射的静态成员。在演示项目中,上下文成员在视图创建时被初始化。

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;

一组辅助函数用于创建撤销和重做单元。

HRESULT CreateUndoUnit(long id, IOleUndoUnit** ppUU);
HRESULT CreateRedoUnit(long id, IOleUndoUnit** ppUU);
HRESULT CreateGroupUnit(IOleParentUndoUnit** ppPUU);

以下是撤销单元的实现。

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;

IOleUndoUnitImpl_Do 方法是从 IOleUndoUnitImpl 类提供的 Do 方法调用的。在这里,我们只需要在活动映射中找到对象,将其移除,然后将其添加到已删除映射中。我们应该返回 S_OK 表示成功,或 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;
	}

如果上面的操作成功,我们会得到一个可以重做我们刚刚撤销的操作的撤销单元。这个单元被添加到撤销管理器中,撤销管理器会将其放入重做堆栈。这里我们只调用辅助函数。

	HRESULT IOleUndoUnitImpl_CreateUndoUnit(IOleUndoUnit** ppUU)
	{
		HRESULT hr = CreateRedoUnit(m_id, ppUU);
		return SUCCEEDED(hr) ? S_OK : E_ABORT;
	}

我们还需要实现 GetDescription 方法。

	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;
	}
};

重做单元以相同的方式实现,不同之处在于它反转了活动对象映射和已删除对象映射的用途。此外,我们只需要一对撤销和重做单元,因为所有图形对象都具有相同的接口。

在演示项目中,还通过使用 IOleParentUndoUnit 接口将一组动作嵌套在一起,从而产生分组的概念,这样它们就可以被视为一个单一的动作。

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()

我们需要实现 IOleParentUndoUnitImpl_CreateParentUndoUnit 方法。在这里,我们使用辅助函数,它实际上只是创建了同一个类的新实例。

	HRESULT IOleParentUndoUnitImpl_CreateParentUndoUnit(IOleParentUndoUnit** ppPUU)
	{
		HRESULT hr = CreateGroupUnit(ppPUU);
		return SUCCEEDED(hr) ? S_OK : E_ABORT;
	}

同样,我们也需要实现 GetDescription 方法。

	STDMETHOD(GetDescription)(BSTR* pBstr)
	{
		*pBstr = ::SysAllocString(L"Create group");
		return S_OK;
	}
};

View

剩下要做的就是将图形类集成到视图中。

构造函数初始化一个用于唯一 ID 的计数器,并且图形上下文类使用指向活动映射和已删除映射的指针进行初始化。

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;

OnPaint 方法中,DrawFunctor 类与 for_each 算法一起使用,将显示映射中的所有对象绘制出来。

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;
}

OnLButtonUP 方法根据当前 m_id 计数器的值调用 CreateGuiObjectCreateGroupObject

这里,一个新图形对象被添加到显示映射中。还创建了一个 CUndoUnit 并将其添加到撤销管理器,撤销管理器将其放入撤销堆栈。

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);
		}
	}
}

此方法创建一个新的 CGroupUnit,它实现了 IOleParentUndoUnit 接口。当调用撤销管理器接口上的 Open 方法并传入此父单元时,后续对撤销管理器上 Add 的调用会将 IOleUndoUnit 放入此父单元中。创建了几个图形对象后,我们关闭父单元,将其提交到撤销堆栈。

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++;
}

这结束了演示项目。请注意,此示例不提供任何内存管理。创建的图形对象永远不会被删除。其目的是仅展示 IOleUndoUnitIOleParentUndoUnit 如何与撤销管理器一起使用,为实现嵌套撤销和重做操作提供一个载体。


附录

有关这些接口的更多信息可以在 MSDN 在线库中找到。

此处关于 IOleUndoManagerIOleUndoUnitIOleParentUndoUnit 接口的描述摘自 MS Platform SDK 文档。

IOleUndoManager 方法描述
Open (IOleParentUndoUnit* pPUU) 打开一个新的父撤销单元,该单元成为其包含单元的撤销堆栈的一部分。
Close (IOleParentUndoUnit* pPUU, BOOL fCommit) 关闭指定的父撤销单元。
Add (IOleUndoUnit* pUU) 将一个简单的撤销单元添加到集合中。
GetOpenParentState (DWORD* pdwState) 返回关于最内层打开的父撤销单元的状态信息。
DiscardFrom (IOleUndoUnit* pUU) 指示撤销管理器丢弃指定的撤销单元以及撤销或重做堆栈上它之下的所有撤销单元。
UndoTo (IOleUndoUnit* pUU) 指示撤销管理器执行撤销堆栈上的操作,直到并包括指定的撤销单元。
RedoTo (IOleUndoUnit* pUU) 指示撤销管理器执行重做堆栈上的撤销操作,直到并包括指定的撤销单元。
EnumUndoable (IEnumOleUndoUnits** ppEnum) 创建一个枚举器对象,调用者可以使用该对象迭代撤销堆栈上的一系列顶层撤销单元。
EnumRedoable (IEnumOleUndoUnits** ppEnum) 创建一个枚举器对象,调用者可以使用该对象迭代重做堆栈上的一系列顶层撤销单元。
GetLastUndoDescription (BSTR* pBstr) 返回位于撤销堆栈顶部的顶层撤销单元的描述。
GetLastRedoDescription (BSTR* pBstr) 返回位于重做堆栈顶部的顶层撤销单元的描述。
Enable (BOOL fEnable) 启用或禁用撤销管理器。
IOleUndoUnit 方法描述
Open (IOleParentUndoUnit* pPUU) 打开一个新的父撤销单元,该单元成为其包含单元的撤销堆栈的一部分。
Do (IOleUndoManager* pUndoManager) 指示撤销单元执行其操作。
GetDescription (BSTR* pBstr) 返回一个描述撤销单元的字符串,该字符串可用于撤销或重做用户界面。
GetUnitType (CLSID* pClsid, LONG* plID) 返回撤销单元的 CLSID 和类型标识符。
OnNextAdd () 通知集合中的最后一个撤销单元已添加新单元。
IOleParentUndoUnit 方法描述
Open (IOleParentUndoUnit* pPUU) 打开一个新的父撤销单元,该单元成为包含单元的撤销堆栈的一部分。
Close (IOleParentUndoUnit* pPUU, BOOL fCommit) 关闭最近打开的父撤销单元。
Add (IOleUndoUnit* pUU) 将一个简单的撤销单元添加到集合中。
FindUnit (IOleUndoUnit* pUU) 指示指定的单元是否是此撤销单元或其子单元的子单元,即指定的单元是否是此父单元层次结构的一部分。
GetParentState (DWORD* pdwState) 返回关于最内层打开的父撤销单元的状态信息。
撤销管理器 - CodeProject - 代码之家
© . All rights reserved.