应用篇_撤销(Undo)和重做(Redo)的C++自动化实现(5)---处理多类型的对象以及命令管理

本文介绍如何使用C++实现撤销和重做功能,重点在于处理多类型的对象以及命令的集中管理。通过基本命令类、复合操作类和命令中心类,简化了撤销/重做机制的编码,适用于不同数据类型的处理。文章提供了实际例子,展示如何使用这些概念来实现撤销和重做操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作者: 熊春雷
网站: http://www.autodev.net
Blog: http://blog.csdn.net/pandaxcl
EMail: pandaxcl@163.com
QQ: 56637059
版本: 0.01 于2007/09/25
目标: 所有C++爱好者
版权: 本文的版权归熊春雷所有

Warning

  1. 本文由熊春雷所写,绝对保证原创,在此特别严肃声明。

  2. 绝对不能容忍他人说本文为他所写以及其他的侵权行为。一旦发现,一定 尽本人最大的能力以法律的形式严追到底,决不妥协。

  3. 引用本文,要保证本文的完整性,不可以删除此处的声明,并且务必注明出处。

Tip

  1. 本文编写的所有代码可以用于任何用途(包括商业用途)。

  2. 用于商业用途的需要在最后发布的软件中声明借鉴了本文的思想。具体事 宜可以协商解决,(代码决不收取任何费用)。

  3. 其他事项可以和我联系,包括技术讨论等等:)或者直接登陆网站论坛: http://www.autodev.net

Note

  1. 本文受到了《C++设计新思维》和《产生式编程》两本书的影响,同时也查阅了大 量的资料,从Loki库和Boost库中也吸收了不少营养,特此感谢之。

  2. 本文由于处于原创阶段,难免会出现各种各样的错误。代码出现错误的可能性非常 小(本来想说为零的),因为文档和代码是严格同步的,这是由VST文本的include 所保证的,代码都是测试成功之后才发布的。

  3. 本文所编写的代码,经过了VC2005编译器和g++编译器的测试,并且都通过了。

  4. 本文还没有彻底完成,算是一个初级版本,未来还将继续完善。暂时发布出来是为 了预知读者群有多少,读者越多,我的成就感越强,写作的时候也会更有动力:)

  5. 本文还会继续完善,欢迎各位读者的批评指正,也接受各种各样的建议,在权衡之 后以决定是否加入本书。

  6. 本书还没有最终完成,还会不断的进行完善,更新之后的内容将会发表于我的 网站我的博客。所以还需要读者多多关心本文的进展:)

Warning

  • 本文的有效篇幅仅仅局限于基础篇,之后的文档还仅仅只是一种创意记录,

  • 不保证正确性,特此提醒。

Contents

处理多类型的对象以及命令管理

目前为止,已经成功的将三个基本操作封装为三个基本命令,同时也将一个复合操作封装 成了一个复合命令,另外也给出了简单的使用代码;从代码中可以看出,撤销和重做的过 程还是比较晦涩,用户为了表达撤销和重做的过程还需要编写很多的额外代码,而且这种 代码也没有很直接的表达撤销和重做的思想。

最为重要的一点是:在上面的使用例子中,所有的命令是分散的,缺乏一个集中管理的地 方,从而导致了编码的繁琐和不规范!

为了更好的表达撤销和重做机制,并且更加简化客户端代码的书写,在此将会对前面给出 的代码进行更深入的封装,让客户端编写的代码尽可能的少,同时也能够更加直接的表达 撤销和重做的意思。

总的说来,在此要达到下列目标:

  1. 处理更多的对象类型,前面只是处理了一种类型(Object)。从本质上来说,处理 多个数据类型和处理一个数据类型是一样的,但是对于方便使用该撤销和重做框架 来说,给出一个示例将会是读者更加直接的了解到如何使用该框架的方便的实现自 己的撤销和重做能力。另外从方便使用的角度来说,本章中必须给出一个惯例,并 且在这个惯例的基础上,实现撤销和重做功能。实际上在本文中介绍的撤销和重做 框架还天生具备了序列化能力,这可是一个非常不错的副产品哦:)关于序列化的原 理,可以参见本人的其它相关文档。

  2. 撤销和重做过程在代码中表达得更加直观。从前面的示例代码中可以看出,撤销和 重做的功能虽然可以实现了,但是也要注意撤销和重做的表现并不直观,直接看到 代码,阅读代码的人并不能够很容易的了解代码的意义;所以,在此还要会改进这 种代码,使得阅读代码就像阅读文档一样,使代码更直接的表达撤销和重做的意义 。

  3. 实现任意次的撤销和重做。前面的撤销和重做次数默认是无限的,虽然默认的情况 下是无限次的撤销和重做,但是客户端有时候可能因为种种原因而不想使撤销和重 做的步骤太多,因而在此还要给出如何实现有限次的撤销和重做方案。

下面依次解决所提出的三个问题。对于问题(1)文中将会给出两个比较直接的数据类型:矩 形类(Rectangle)和圆形类(Circle)。对于问题(2)文中将会更进一步的封装前面的使用方 式,最后一个问题,文中将会给出一个使用惯例,是可以封装成为头文件的,这在后面讨 论。

有了上面的命令基类复合操作类,要想使用撤销和重做功能,还需要 一个命令中心类 ,来集中管理各种命令,而这里的复合操作类已经具备了 一定的命令管理功能了,因此,命令中心类采用了复合操作类来实现:

 class undo_bat:public batch{};// 撤销命令队列
 class redo_bat:public batch{};// 重做命令队列
 class center:public undo_bat,public redo_bat// 命令中心,管理两种命令队列
 {
 private:
     size_t _limit;
 public:
     typedef redo_bat redo_type;
     typedef undo_bat undo_type;
     typedef undo_type  history_type;
     center():_limit(100)// 默认最大撤销操作步骤总数为100步,一般的应用已经足够
     {
     }
     virtual~center()
     {
         undo_type::free();
         redo_type::free();
     }
 public:
     size_t limit()// 获取撤销命令队列的最大命令数量
     {
         return _limit;
     }
     void limit(size_t Limit)// 设置撤销命令队列的最大命令数量
     {
         _limit=Limit;
         relimit();
     }
     void relimit()// 检查撤销命令队列是否已满,剔除过期命令
     {
         typedef std::list<command*> QT;
         if(_limit >= undo_type::size())return;
         while(_limit < undo_type::size())
         {
             command *pCmd = static_cast<command*>(undo_type::front());
             undo_type::pop_front();
             assert( NULL != pCmd );
             delete pCmd;// 命令被释放了
         }
     }

 public:
     bool undoable()
     {
         return !undo_type::empty();
     }
     bool redoable()
     {
         return !redo_type::empty();
     }
     void undo()// 撤销
     {
         if (!undo_type::empty()) {
             undo_type::back()->undo();
             redo_type::push_back(undo_type::back());
             undo_type::pop_back();
         }
     }
     void redo()// 执行或者重做
     {
         if (!redo_type::empty()) {
             redo_type::back()->redo();
             undo_type::push_back(redo_type::back());
             redo_type::pop_back();
         }
     }
     void undo(int number)// 一次撤销很多步命令
     {
         for(int i=0;i<number;i++) {
             if(undo_type::empty())break;
             else undo();
         }
     }
     void redo(int number)// 一次重做很多命令
     {
         for(int i=0;i<number;i++) {
             if(redo_type::empty())break;
             else redo();
         }
     }
     // 下面的record、exec和stop三个函数实现复合命令的自动创建,只要保证如下
     // 的固定格式即可:
     // record();// 复合命令开始
     //     exec(命令1);// 子命令
     //     exec(命令2);
     //     record();// 子复合命令开始
     //         ... ...
     //     stop();// 子复合命令结束
     //     ... ...
     //     exec(命令n);
     // stop();// 复合命令结束
     // 这种组合方式可以任意的嵌套,从而实现无级撤销
     void record(command*pCmd=NULL)
     {
         history_type::record(pCmd);
         redo_type::free();
     }
     // 下面的execute也可以单独使用,不需要record和stop的配合
     void execute(command*pCmd)
     {
         assert(pCmd!=NULL);
         pCmd->redo();
         record(pCmd);
         relimit();
     }
     void stop()
     {
         history_type &H=static_cast<history_type&>(*this);
         history_type::iterator result=H.begin();
         history_type::reverse_iterator rit;
         rit = std::find(H.rbegin(),H.rend(),static_cast<command*>(NULL));
         std::advance(result,std::distance(rit,H.rend())-1);
         if(result!=H.end())
         {
             history_type::difference_type diff;
             diff = std::distance(result,H.end());
             if(diff==1 || diff==2) {
                 H.erase(result);
             }else{
                 batch*pBat=new batch();
                 pBat->splice(pBat->begin(),H,result,H.end());
                 pBat->remove(static_cast<command*>(NULL));//list
                 record(pBat);
             }
         }
     }
 };

可以看出,通过三个基本操作类、一个复合操作类、容器类以及命令中心类就可以实现任 意的撤销和重做功能了!而且再也没有必要编写派生自命令基类的类,更不需要再实现 redo和undo虚函数了,从而极大的简化的命令编写!所有的其他命令都可以由这里的三个 基本命令和一个复合命令组合出来:)

来看一个使用中的实际例子:

 //矩形类
 class Rectangle
 {
 public:
     Rectangle():_x(0),_y(0),_width(20),_height(10){}
     Rectangle(int x,int y,int w,int h):_x(x),_y(y),_width(w),_height(h){}
 private:
     int _x,_y,_width,_height;
     //下面的函数仅仅是为了输出信息而准备的
     friend std::ostream&operator<<(std::ostream&s,Rectangle&o)
     {
         return s << "(" << o._x << "," << o._y << "," << o._width << "," << o._height << ")" ;
     }
 };
 //圆形类
 class Circle
 {
 public:
     Circle():_x(0),_y(0),_radius(20){}
     Circle(int x,int y,int r):_x(x),_y(y),_radius(r){}
 private:
     int _x,_y,_radius;
     //下面的函数仅仅是为了输出信息而准备的
     friend std::ostream&operator<<(std::ostream&s,Circle&o)
     {
         return s << "(" << o._x << "," << o._y << "," << o._radius << ")" ;
     }
 };
 //需要一个控制面板类
 class Control
     :public undo::raw::container<Rectangle>
     ,public undo::raw::container<Circle   >
     ,public undo::raw::center// center必须放置在最后
 {
 public:
     template<class T> void create( T*&ID )
     {
         typedef undo::raw::container<T> CT;
         undo::raw::center::execute(new undo::raw::create<CT>(*this,ID));
     }
     template<class T> void modify( T* ID, const T&O )
     {
         typedef undo::raw::container<T> CT;
         undo::raw::center::execute(new undo::raw::modify<CT>(*this,ID,O));
     }
     template<class T> void remove( T*&ID )
     {
         typedef undo::raw::container<T> CT;
         undo::raw::center::execute(new undo::raw::remove<CT>(*this,ID));
     }
 private:
     //为了能够判断操作的正确性需要输出容器的信息
     template <class Container> void display(const char*str,Container&c)
     {
         std::cout << str << "[" << c.size() << "] ";//输出提示信息,并输出容器中的元素数量
         typename Container::iterator it = c.begin();
         for(;it!=c.end();++it)
         {
             std::cout<<"("<<*it<<","<<**it<<") ";
         }
         std::cout << std::endl;
     }
 public:
     //下面的函数是输出该控制面板对象的相关信息的
     void display(const char*str)
     {
         typedef undo::raw::container<Rectangle> RC;
         typedef undo::raw::container<Circle   > CC;
         std::cout << "----------"<<str<<"----------" <<std::endl;
         std::cout << "Undo:[" << undo::raw::center::undo_type::size() << "] " ;
         std::cout << "Redo:[" << undo::raw::center::redo_type::size() << "] " ;
         std::cout << std::endl ;
         display("矩形容器:",static_cast<RC&>(*this));
         display("圆形容器:",static_cast<CC&>(*this));
         std::cout << "=========="<<str<<"==========" <<std::endl;
     }
 };

从上面的Control代码可以看出,这里的Control就好比数据库,而container<Rectangle> 和container<Circle>就好比数据库中的表,而center就是DBMS了:-)

从而处理多种不同的数据类型就相当于增加多种不同的数据库表(container<T>),仅此 而已,根本就不必再用到额外的编码了。使用方式如下:

 //首先应用程序中必须有一个全局的控制面板类对象
 Control C;
 Rectangle *idr1=NULL,*idr2=NULL;//两个测试使用的矩形类对象标识号
 Circle    *idc1=NULL,*idc2=NULL;//两个测试使用的圆形类对象标识号
 C.display("控制面板中的初始状态");

 ////////////////////////////////////////////////////////////
 //模拟创建矩形和圆形过程
 C.create(idr1);
 C.create(idr2);
 C.create(idc1);
 C.create(idc2);
 C.display("创建了两个矩形对象和两个圆形对象");
 ////////////////////////////////////////////////////////////

 ////////////////////////////////////////////////////////////
 //模拟修改操作
 C.modify(idr1,Rectangle(1,1,5,10));
 C.modify(idr2,Rectangle(15,10,50,30));
 C.modify(idc1,Circle(15,10,50));
 C.modify(idc2,Circle(5,10,70));
 C.display("修改了两个矩形对象和两个圆形对象");
 ////////////////////////////////////////////////////////////

 ////////////////////////////////////////////////////////////
 //开始模拟撤销操作
 C.undo();//撤销1步
 C.display("撤销1步之后");
 C.undo(4);//再撤销4步
 C.display("撤销4步之后");
 C.redo();//重做一步
 C.display("重做1步之后");
 C.redo(4);//重做4步
 C.display("重做4步之后");
 ////////////////////////////////////////////////////////////

 ////////////////////////////////////////////////////////////
 //下面的代码演示了如何将多个命令合并成为一个复合命令的方法,
 //通过撤销和重做一步的方式演示了复合命令是否真的成为了一个
 //命令。将多个命令合并成为一个命令的方法如下:
 C.record();//记录开始
 C.remove(idr1);//删除标识号为idr1的矩形对象
 C.remove(idr2);//删除标识号为idr2的矩形对象
 C.remove(idc1);//删除标识号为idc1的圆形对象
 C.remove(idc2);//删除标识号为idc2的圆形对象
 C.stop();//记录结束
 C.display("执行复合命令");
 C.undo();//撤销1步
 C.display("撤销1步");
 C.redo();//重做一步
 C.display("重做1步");
 C.undo();//再撤销1步
 C.display("再撤销1步恢复原来的状态");
 ////////////////////////////////////////////////////////////

 ////////////////////////////////////////////////////////////
 //现在模拟一下如何使用限制次数的撤销和重做方法
 C.limit(5);//命令中最多只允许撤销和重做5次
 C.display("限制最大可撤销步骤为5次");
 C.undo();//撤销1步
 C.display("撤销1步之后");
 C.undo(4);//再撤销4步
 C.display("撤销4步之后");
 C.redo();//重做一步
 C.display("重做1步之后");
 C.redo(4);//重做4步
 C.display("重做4步之后");
 //在这里正好模拟一下当重做队列中还有命令的时候再执行新的命令的情况
 //到这里为止,重做队列中还有一个命令可以重做,但是没有重做。这样将
 //会直接添加新的命令到历史纪录的末尾,清除掉这个可以重做的命令,同
 //时清理过期的命令,保证命令历史记录不会超过指定的最大可撤销步骤数
 //量,在这里是5次。
 C.modify(idr2,Rectangle(150,100,500,300));
 C.display("重做队列中还有命令的时候再执行新的命令的情况");
 ////////////////////////////////////////////////////////////

测试结果:

 ----------控制面板中的初始状态----------
 Undo:[0] Redo:[0]
 矩形容器:[0]
 圆形容器:[0]
 ==========控制面板中的初始状态==========
 ----------创建了两个矩形对象和两个圆形对象----------
 Undo:[4] Redo:[0]
 矩形容器:[2] (00375758,(0,0,20,10)) (003757B8,(0,0,20,10))
 圆形容器:[2] (00375818,(0,0,20)) (003758A8,(0,0,20))
 ==========创建了两个矩形对象和两个圆形对象==========
 ----------修改了两个矩形对象和两个圆形对象----------
 Undo:[8] Redo:[0]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(5,10,70))
 ==========修改了两个矩形对象和两个圆形对象==========
 ----------撤销1步之后----------
 Undo:[7] Redo:[1]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(0,0,20))
 ==========撤销1步之后==========
 ----------撤销4步之后----------
 Undo:[3] Redo:[5]
 矩形容器:[2] (00375758,(0,0,20,10)) (003757B8,(0,0,20,10))
 圆形容器:[1] (00375818,(0,0,20))
 ==========撤销4步之后==========
 ----------重做1步之后----------
 Undo:[4] Redo:[4]
 矩形容器:[2] (00375758,(0,0,20,10)) (003757B8,(0,0,20,10))
 圆形容器:[2] (00375818,(0,0,20)) (003758A8,(0,0,20))
 ==========重做1步之后==========
 ----------重做4步之后----------
 Undo:[8] Redo:[0]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(5,10,70))
 ==========重做4步之后==========
 ----------执行复合命令----------
 Undo:[9] Redo:[0]
 矩形容器:[0]
 圆形容器:[0]
 ==========执行复合命令==========
 ----------撤销1步----------
 Undo:[8] Redo:[1]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(5,10,70))
 ==========撤销1步==========
 ----------重做1步----------
 Undo:[9] Redo:[0]
 矩形容器:[0]
 圆形容器:[0]
 ==========重做1步==========
 ----------再撤销1步恢复原来的状态----------
 Undo:[8] Redo:[1]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(5,10,70))
 ==========再撤销1步恢复原来的状态==========
 ----------限制最大可撤销步骤为5次----------
 Undo:[5] Redo:[1]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(5,10,70))
 ==========限制最大可撤销步骤为5次==========
 ----------撤销1步之后----------
 Undo:[4] Redo:[2]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(0,0,20))
 ==========撤销1步之后==========
 ----------撤销4步之后----------
 Undo:[0] Redo:[6]
 矩形容器:[2] (00375758,(0,0,20,10)) (003757B8,(0,0,20,10))
 圆形容器:[1] (00375818,(0,0,20))
 ==========撤销4步之后==========
 ----------重做1步之后----------
 Undo:[1] Redo:[5]
 矩形容器:[2] (00375758,(0,0,20,10)) (003757B8,(0,0,20,10))
 圆形容器:[2] (00375818,(0,0,20)) (003758A8,(0,0,20))
 ==========重做1步之后==========
 ----------重做4步之后----------
 Undo:[5] Redo:[1]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(15,10,50,30))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(5,10,70))
 ==========重做4步之后==========
 ----------重做队列中还有命令的时候再执行新的命令的情况----------
 Undo:[5] Redo:[0]
 矩形容器:[2] (00375758,(1,1,5,10)) (003757B8,(150,100,500,300))
 圆形容器:[2] (00375818,(15,10,50)) (003758A8,(5,10,70))
 ==========重做队列中还有命令的时候再执行新的命令的情况==========

从上面的示例代码中可以看出,表达撤销和重做的方式比上一节直接多了,如果将本节的 center类也放到头文件中,那么使用这个撤销和重做框架将会容易得多。从运行的结果看 ,已经达到了本节开始提出的前两个问题,至于第三个问题所提出的任意次实际上也实现 了,虽然表面上看只是实现了有限次的撤销和重做方案,但是实际上这已经足够我们目前 的所有的应用了,不是我不想实现真正的任意次数的撤销和重做方案,而是STL的list容器 限制了,我们知道list容器里面的max_size()函数可以得到一个list容器允许容纳的最大 元素数量。由此可见,真正意义上的无限次数并不存在,只要这个数足够大就可以了。另 外计算机的存储容量再大,也还是有限的。所以可以这么说:在本文中已经成功的实现了 无限次的撤销和重做方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值