撤销管理器






4.92/5 (9投票s)
2001 年 9 月 5 日
9分钟阅读

123046

3369
一篇关于管理撤销和重做操作的文章
引言
Microsoft 在 OLE 框架中设计了一套用于撤销管理的接口。本文档介绍了我的这些接口的实现类以及它们如何在演示项目中使用的。
尽管演示应用程序中提供了简单的撤销和重做操作管理,但其目的是展示 IOleUndoManager
、IOleUndoUnit
和 IOleParentUndoUnit
接口的用法。
主接口 IOleUndoManager
是为容器应用程序设计的。控件可以通过 IServiceProvider
接口,使用 GUID SID_SOleUndoManager
来获取撤销管理器的指针。通过将撤销单元添加到宿主撤销管理器中,控件可以参与集中式的撤销管理。
撤销单元可以通过父撤销单元进行嵌套。这使得可以将复杂的多个操作组合在一起,以便最终用户可以用一个命令撤销和重做它们。撤销管理器只会将顶层撤销单元作为一个整体来调用。
宿主应用程序决定了撤销管理器的范围。范围可以是文档级别的,为每个文档提供一个撤销管理器。
上次更新以来的更改
- 修复了在使用非
NULL
参数时COleUndoManagerImpl
的UndoTo
和RedoTo
实现中的错误 - 更新了演示项目,添加了“撤销到”和“重做到”菜单项
类概述
以下类实现了 Ole 撤销相关的接口。
以下类是一个辅助类。
COleUndoManagerImpl 管理撤销和重做堆栈 IOleUndoUnitImpl 撤销单元的基本实现 IOleParentUndoUnitImpl 管理子撤销单元
CComClassID CComCoClass
的替代,用于实现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
成员。
演示项目

演示项目是使用 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();
在撤销和重做命令的事件处理程序中。调用撤销管理器方法 UndoTo
或 RedoTo
,并传递一个 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
,该类定义了两个虚拟函数:Draw
和 Description
。
以下代码位于名为 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
计数器的值调用 CreateGuiObject
或 CreateGroupObject
。
这里,一个新图形对象被添加到显示映射中。还创建了一个 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++; }
这结束了演示项目。请注意,此示例不提供任何内存管理。创建的图形对象永远不会被删除。其目的是仅展示 IOleUndoUnit
和 IOleParentUndoUnit
如何与撤销管理器一起使用,为实现嵌套撤销和重做操作提供一个载体。
附录
有关这些接口的更多信息可以在 MSDN 在线库中找到。
- 关于 IOleUndoManger
- 关于 IOleUndoUnit
- 关于 IOleParentUndoUnit
此处关于 IOleUndoManager
、IOleUndoUnit
和 IOleParentUndoUnit
接口的描述摘自 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) | 返回关于最内层打开的父撤销单元的状态信息。 |