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

一个简单、基于操作的撤销/重做框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.51/5 (37投票s)

2006 年 5 月 16 日

CPOL

5分钟阅读

viewsIcon

104850

downloadIcon

1953

如何使用一个简单、基于操作的撤销/重做框架

Demo Application Snapshot

引言

本文介绍了一种为应用程序添加撤销/重做支持(http://en.wikipedia.org/wiki/Undo)的简单方法。该框架能够处理各种场景。但是,我不能声称它能够处理所有可能的场景,也不能声称它是最聪明的框架。

需要基本的 C++ 和 STL 理解。要理解演示应用程序,还需要基本的 MFC 理解。

我使用了 Microsoft Visual C++ 和 Windows XP Professional。该框架可以轻松移植到其他操作系统。

操作方法

该框架基于一个简单的概念:操作(命令)对象。操作或命令对象封装了用户请求。与临时处理用户请求相比,操作具有许多优点(http://en.wikipedia.org/wiki/Command_pattern)。

包含内容

该框架封装在 kis 命名空间(http://en.wikipedia.org/wiki/KISS_Principle)中,包含以下内容:

  1. 一个操作接口,C_Action
  2. 一个操作执行器接口,C_ActionExecutor
  3. 一个默认操作执行器创建器,CreateActionExecutor_Default
  4. 一个智能指针模板,C_SharedPtrhttp://en.wikipedia.org/wiki/Smart_pointer

使用框架

要使用该框架,您必须遵循三个步骤:

  1. 设置框架。
  2. 编写操作。
  3. 使用操作。

设置框架

  1. 解压 KisWinBin.zip 文件。我使用的默认路径是 d:\。演示应用程序也使用此路径。
  2. 准备您的项目
    1. kis 路径添加到您的项目中。
    2. kis.lib 库添加到您的项目中。
    3. kis.dll 库添加到您的项目输出目录。
    4. #include “kis.h” 指令添加到您的 STDAFX.H 文件中。
  3. 在您的文档类中添加一个操作执行器成员。所有操作都通过操作执行器执行。提供了一个默认执行器。您可以编写自己的执行器,以利用特定情况。
    class C_Document
    {
    protected:
      kis::SP_ActionExecutor m_spActionExecutor;
      
    public:
      C_Document( /*...*/ )
      :
      m_spActionExecutor( kis::CreateActionExecutor_Default() )
      {
        // ...
      }
      // ...
    };

编写操作

  1. 操作类是派生C_Action 的类。C_Action 在头文件(kis_action.h)中有完整的文档记录,因此您应该能够轻松编写操作。
  2. 理想情况下,您应该为应用程序中的所有用户请求编写操作类;请求是否应该可撤销并不重要。
  3. 使用异常让客户端知道发生了错误。
  4. 确保执行是原子性的,这意味着它永远不会将目标留在无效状态。
  5. 使用操作创建器来创建操作对象;不要暴露派生操作类。
    //
    // interface file
    //
    SP_Action CreateAction_Clear ( C_Document* a_pTarget );
    
    //
    // implementation file
    //
    namespace
    {
    class C_Action_Clear : public C_Action
    {
    public:
      C_Action_Clear ( C_Document* a_pTarget )
    // ...
    };
    } // namespace
    
    SP_Action CreateAction_Clear ( C_Document* a_pTarget )
    {
      return SP_Action( new C_Action_Clear( a_pTarget ) );
    }

使用操作

操作不能直接执行。操作对象保护其大部分方法。要执行操作,您需要一个操作执行器对象。

  1. 执行操作
    kis::SP_Action spAction = CreateAction_Clear( this );
    // ...
    try
    {
      m_spActionExecutor->Execute( spAction );
    }
    catch ( /*exceptions threw by your action*/ )
    {
      // ...
    }
  2. 撤销最后执行的操作
    // ...
    m_spActionExecutor->Unexecute( 1 ); // un-executes last executed action
    // ...
  3. 重做最后未执行的操作
    // ...
    m_spActionExecutor->Reexecute( 2 ); // re-executes last two executed actions
    // ...

问题?

如何更改历史记录大小?

历史记录大小由两个方法管理:C_ActionExecutor::GetMaxBytesCountC_ActionExecutor::SetMaxBytesCount。历史记录空间消耗的速度取决于操作对象的大小。操作对象的大小由 C_Action::GetBytesCount 给出。

操作对象有多大?

这取决于。C_Action::GetBytesCount 方法响应 C_ActionExecutor 的问题“您消耗了多少内存?”。您派生的 C_Action 对象不应回答“零”,因为您的操作大小至少是 sizeof(C_Action)。答案越准确,对历史记录使用情况的估计就越好。

/*override*/ unsigned int C_DemoAction_InMemoryClearDrawing::GetBytesCount() const
{
  return sizeof( *this ) + m_pClearedDrawing->GetBytesCount();
}

通过使用临时文件可以保持历史记录的低内存使用。在这种情况下,分配的字节数会因流式传输对象的大小而减少。

/*override*/ unsigned int C_DemoAction_ClearDrawing::GetBytesCount() const
{
  return sizeof( *this ); // size of cleared drawing not added
}

/*override*/ bool C_DemoAction_ClearDrawing::Execute()
{
  m_pDoc->m_Drawing.Write( m_Stream );
  m_pDoc->m_Drawing.Delete();
  return true;
}

如何禁用/启用历史记录内存?

如果历史记录内存大小设置为零,则禁用历史记录。将其设置为大于零的值将启用它。

如何列出历史记录?

您可以使用 C_ActionExecutor 的方法 GetUnexecuteCountGetReexecuteCountGetUnexecuteNameGetReexecuteName 来迭代历史记录。

为什么我不能直接使用操作对象?

简而言之,该框架不应该这样工作。[有一个关于一群警察进行智商测试的笑话(真实故事?)测试包括一块带有形状奇特的孔的板子和相应的销子,应该插入孔中。测试结果是:1% 的警察非常聪明,99%......非常强壮。]

假设 ExecuteUnexecuteReexecute 方法是 public 的。一些客户端会忍不住编写类似这样的代码:

void C_Document::OnClear_WithPublicActionExecute()
{
  SP_Action spAction = CreateAction_Clear();
  spAction->Execute();
}

看起来没问题,直到您意识到客户端忘记将已执行的操作保存到历史记录中。他忘记了吗?(如果产品已经交付,这将非常尴尬。)他真的打算本地执行操作吗?很难说。

隐藏操作的 ExecuteUnexecuteReexecute 方法使框架不易出错。假设您对框架一无所知,并且想在一个已有的项目中添加一个“清空”命令处理程序。首先,您会尝试直接调用 Execute 方法。您很快就会发现 Execute 是受保护的。“为什么?我该如何执行操作?” 您会发现需要一个操作执行器。“我为什么需要它?……啊,撤销/重做!”

确实,有时可能需要执行一些不需要撤销的操作。在我看来,这是一个罕见的情况,可以通过本地操作执行器轻松解决。使用本地操作执行器可以清楚地表明开发者的意图是本地使用该操作。

如何本地执行一个或多个操作?

由于 execute 方法不是 public 的,因此您不能直接执行操作。为了执行它,您首先需要创建一个本地操作执行器,然后调用执行器的 execute 方法。

void C_Document::LocalActionExample()
{
  // the following line will generate an error: cannot access protected member
  // CreateAction_Clear( this )->Execute(); 

  kis::SP_ActionExecutor spActionExecutor = kis::CreateActionExecutor_Default();
  
  try
  {
    spActionExecutor->Execute( CreateAction_Clear( this ) );
    spActionExecutor->Execute( CreateAction_Insert( this, "abc" ) );
  }
  catch ( ... )
  {
    spActionExecutor->Unexecute( spActionExecutor->GetUnexecuteCount() ); // rollback
  }
}

演示应用程序

演示是一个简单的绘图应用程序。它实现了以下用户请求的操作:

  • 添加随机线段。此操作演示了如何创建一个不需要参数的操作。
  • 更改历史记录大小。此操作演示了如何:创建不可撤销的操作、通过对话框获取参数以及更改历史记录大小。
  • 添加线段/矩形/椭圆。这些操作演示了如何使用鼠标获取参数以及如何将新的图形对象添加到绘图中。
  • 清空整个绘图。此操作演示了如何保持历史记录内存使用量低。

虽然可以列出整个历史记录(就像 Microsoft Word 等应用程序所做的那样),但撤销/重做用户界面元素仅列出最近的操作。

历史

  • 2006 年 5 月 16 日:文章首次发布
  • 2006 年 6 月 14 日:“问题?”部分已更改
  • 2009 年 2 月 3 日:修复了一个小错误
  • 2013 年 2 月 16 日:拼写错误
© . All rights reserved.