交换函数swap功能的讨论
在撤销和重做的实现过程中,总会出现各种各样的特殊情况和特殊技巧来实现撤销和重做 功能,由于本系列文档实现的是采用了一般化的方法实现了撤销和重做框架!所以就要思 考这样的问题:采用取巧的方法是否一定比这里通用的方法在时间和空间上面高效呢?本 文就是通过讨论一个基本的交换函数来作为类比,虽然不是很有说服力的解释,但是也说 明了不少的问题!
下面是经典的交换函数的两种实现方式:
采用中间变量的交换过程:
int a = 10; int b = 20; // 需要第三个变量进行中转的交换过程 int t;// 中转变量 t = a;// 执行之后 a= 10 b= 20 t= 10 a = b;// 执行之后 a= 20 b= 20 b = t;// 执行之后 a= 20 b= 10
这就类似于有一杯橙汁(a)和一杯牛奶(b),要想把两个杯子中的饮料进行置换,就必须借 助于第三支空杯子(t)。交换的过程如下:
1. 把橙汁(a)导入空杯子(t)中
2. 把牛奶(b)导入盛橙汁的杯子(a)中
3. 把空杯子(t)中的橙汁倒入盛牛奶的杯子(a)中
在上面的讨论过程中,假设了三个杯子的大小一样,其实这种假设是不必要的!特别需要 注意的是,这里的这个过程可以很容易的实现逆操作!
下面再来看看采用另外的一种假设的交换过程:
int a = 10; int b = 20; // 不需要第三个变量进行中转的交换过程 a = a+b;// 右边a=10 b=20 执行之后 a= 30 b=20 b = a-b;// 右边a=30 b=20 执行之后 a= 30 b=10 a = a-b;// 右边a=30 b=10 执行之后 a= 20 b=10
从上面的交换过程可以看出,并没有采用第三个变量,而是直接采用了两个变量的自身! 这其实就是假设了:装橙汁和装牛奶的两个杯子都最多只装了一半!交换过程如下:
1. 将牛奶倒入盛橙汁的杯子中
2. 从1中的混合溶液中萃取出橙汁,放入盛牛奶的杯子中
上面的这个萃取过程是可不是那么容易实现的,这是由物理特性和化学特性决定 的!但是把上面的牛奶和橙汁换成水和汽油,那么实现起来就容易多了!当然是牛奶换成 水,橙汁换成汽油啦:)不过还要注意的是,上面的那个数学过程是可以实现可逆操 作的,但是后面的那个物理过程却很难实现逆操作!
从上面的两种交换过程可以看出:虽然可以采用一些特定的技巧(例如第二种交换方法) 实现不采用第三个量实现交换,但是这种交换过程的限制却太多了,不容易推广使用!此 外这种非常有技巧的交换过程的逆操作的实现也是非常困难的,不可能有比较通用的方法 实现逆操作的!
鉴于此,第一种方法虽然采用了第三个变量实现交换,但是这种方法有着下面的种种优势:
1. 交换过程可以非常容易的实现
2. 交换过程的逆操作也可以很容易的实现
3. 采用第三个变量实现交换的过程并不比其他的特定技巧实现的交换过程的方法占用 的资源多!
关于第3点,可以这样解释:第一种交换方法所用的三个杯子可以是一样大,而第二种交换 方法的两个杯子,先倒出牛奶的杯子可以只有装橙汁的杯子的容积的一半大小!如果装牛 奶的杯子和第一种方法中的杯子大小一致,那么两种方法的杯子的容积是一样的!由此可 见,采用特定的方法并不一定省资源:(
有了上面的讨论,可以看出,第一种交换方法是一种非常好的通用的交换方法:)撤销和 重做的本质就是一种交换过程,下面的内容都是围绕这种方法来讨论的:)
撤销和重做原理
首先讨论一下针对整型变量v的撤销和重做功能的原理实现:
int v = 5;// v的当前值 v = 8;//(1)现在需要将v的值修改成为8 v = 5;//(2)撤销将v的值修改成为8的操作 v = 8;//(3)重做将v的值修改成为8的操作
在上面的代码中可以看出:这是一个典型的修改操作的撤销和重做的原理实现, 其中:
1. 步骤(1)中的8是修改参数
2. 步骤(2)中的5是v在执行修改操作之前的原始数据
3. 步骤(3)中的8是执行重做操作需要的修改参数备份,必须在执行修改操 作之前备份之
从而可以总结出实现撤销和重做所必须遵守的规范如下:
1. 为了实现撤销操作必须在修改对象之前备份原始数据
2. 为了实现重做操作必须在修改对象的时候备份修改参数
有了这两条规范就可以保证任何操作(到目前为止我还没有发现不能用这种方式实现 撤销和重做的操作)都可以通过这两条规则实现撤销和重做的能力了。
好了,到目前为止,有了实现撤销和重做方案的通用规则,并且这种规则的空间和时间效 率都非常好,我们该讨论一下三个基本操作和一个复合操作了。至于为 什么只有三种基本操作和一个复合操作,这是我在编写这个撤销和重做框架的过程中慢慢 积累起来的,至于严格的证明,恐怕不是我现在可以处理的了:(
这个三个基本操作是:
a. 创建操作
b. 修改操作
c. 删除操作
一个复合操作是:
d. 复合操作
一共是四个操作,其中复合操作可以是三个基本操作的组合,也可以是三个基本操作 和复合操作的任意组合,也就是说:复合操作里面还有子复合操作,这种嵌套可以达到任 意的层次。这里面的组合就是千变万化的了!
好了,说了这么多,是该看看具体的代码是如何实现的了。值得说明的是:为了代码 的正确性和可读性,在实现的过程中尽量避免非常复杂的C++指针操作问题,当然对于避 免不了的指针问题,也要尽可能的使其简单;取而代之的是尽量使用STL中的容器和算法 来实现需要的功能。
在上面的修改操作的原理实现中,我们首先就已经拥有了一个整型变量v,所以执行修改操 作的必要条件就是保证该变量已经存在(没有变量,如何修改啊!!!),但是这里 的变量v从分配到释放的过程没有考虑,因此下面来讨论分配和 释放两个过程对应的创建操作和删除操作:
int*ptr = new int(5);// 创建操作 delete ptr;ptr = NULL;// 删除操作
可以看出,对象的创建过程就是创建操作,而对象的释放过程就是删除操作 。在此其实还有另外一种考虑:如何表示存在和消失的概念?目前 我只能想到用另外一个对象来记录这种变化,也就是说:对象的存在和消失 的状态,对象自身是不可以保存的,必须通过另外的对象(这里采用的是整型指针) 来保存!而这另外一个对象来保存其他对象的存在和消失的状态,实际上也是一 种修改过程,只不过这种修改过程不需要实现撤销和重做功能,也就是说实现撤销和重做 能力需要借助于不需要撤销和重做的基础才能实现!这是不是很矛盾,其实这是普遍的规 律,量子信息的通信还是需要借助于传统通讯基础才能实现:)
事实上,除了用指针对象来表示对象的存在和消失状态外,也可以用STL容器来表示对象的 存在和消失状态,而后者就是在本文中采用的方案!当容器中存在对象的时候,可以从STL容器中索引出相应的对象;当容器中没有指定索引的对象的时候,就表 示该对象已经被释放了!
Tip
· 事实上,只要能够表示存在和消失的概念的C++量都可以在这里作为
· 另外一个对象。
这里强调了索引,其实这里的索引包含了两层意思:
名字
· STL容器中可以存放多个同类型对象,为了识别不同的对象,就需要为每个对象取一个
· 独一无二的名字,当然这里的独一无二仅仅只限于该STL容器范围
动作
· 为STL容器中的多个对象取独一无二名字的目的就是为了能够在修改的时候有提取指定
· 对象的能力
在编写GUI程序的过程中,经常用对话框来实现对一批对象的操作,而且这种批量操作的撤 销操作和重做操作也希望能够一步执行所有子操作的撤销和重做操作,否则行为就会极其 怪异。这种批量操作不同于上面的修改操作、创建操作以及删除操 作,而是独立于这三种基本操作之外的操作。简单点说就是一种能够组合基本操作和 批量操作的操作。这种操作就是前面所说的复合操作。
现在对前面的讨论进行总结如下:
修改操作
1. 为了实现撤销操作必须在对对象修改之前保存原始信息备份
2. 为了实现重做操作必须在修改对象的时候保存修改信息备份
创建操作 删除操作
1. 必须通过对象(v)之外的其他对象(ptr)来反映该对象的存在和消失 状态
2. 创建操作和删除操作互为逆操作,可以用来互相实现对方的撤销操作
3. 本质上讲,创建操作和删除操作的撤销和重做操作实际上就是针对该对象之外的对 象(ptr)执行的修改操作,因此也必须要满足修改操作的信息备份要求
复合操作
1. 必须能够组合前面讨论的修改操作、创建操作和删除操作 三种基本操作
2. 当然复合操作也要能够组合其他的子复合操作
为了使得讨论更加形象,在这里特别提供一个对象类:
// 对象类 class Object { public: Object():_member(0){} Object(int m):_member(m){} private: int _member; };
有了这些讨论,我们就可以实现撤销和重做基本结构的原理实现了:
创建操作的原理实现:
// 首先创建两个Object对象,为了避免指针的出现,采用了标识号的方法 {// 创建操作的撤销和重做 typedef std::map<int,Object> Container;// 容器类 Container C;// 必须用这个容器来表示对象的存在状态,并且可以根据标识符得到对象 int id1=1,id2=2;// 两个对象的标识号创建参数保存于此 Object obj(10);// 对象创建参数保存于此 C.insert(std::make_pair(id1,obj));// 创建标识号为id1的对象 C.insert(std::make_pair(id2,obj));// 创建标识号为id2的对象 // 创建操作的撤销非常容易实现 C.erase(id1);// 创建标识号为id1的对象的撤销操作 C.erase(id2);// 创建标识号为id2的对象的撤销操作 // 创建操作的重做操作也非常容易实现,不过需要备份的创建参数 C.insert(std::make_pair(id1,obj));// 重做创建标识号为id1的对象 C.insert(std::make_pair(id2,obj));// 重做创建标识号为id2的对象 }
修改操作的原理实现:
{// 修改操作的撤销和重做 typedef std::map<int,Object> Container;// 容器类 Container C;// 必须用这个容器来表示对象的存在状态,并且可以根据标识符得到对象 // 当然如果需要修改操作的话,对象就必须一定已经存在了 ///////////////////////////////////////////////////////// int id1=1,id2=2;// 两个对象的标识号创建参数保存于此 Object obj(10);// 对象创建参数保存于此 C.insert(std::make_pair(id1,obj));// 创建标识号为id1的对象 C.insert(std::make_pair(id2,obj));// 创建标识号为id2的对象 ///////////////////////////////////////////////////////// // 下面才能够开始实现修改操作 // 为了能够实现撤销操作而必须保存的信息 Object OBK1 = C[id1];// 备份标识号为id1的对象的原始信息 Object OBK2 = C[id2];// 备份标识号为id2的对象的原始信息 // 为了能够实现重做操作而必须保存的信息 Object OM1(20);// 备份标识号为id1的对象的修改参数信息 Object OM2(50);// 备份标识号为id2的对象的修改参数信息 // 开始对对象实现修改操作 C[id1] = OM1;// 修改标识号为id1的对象 C[id2] = OM2;// 修改标识号为id2的对象 // 开始对对象实现撤销操作 C[id1] = OBK1;// 撤销修改标识号为id1的对象 C[id2] = OBK2;// 撤销修改标识号为id2的对象 // 开始对对象实现重做操作 C[id1] = OM1;// 重做修改标识号为id1的对象 C[id2] = OM2;// 重做修改标识号为id2的对象 }
删除操作的原理实现:
{// 删除操作的撤销和重做 typedef std::map<int,Object> Container;// 容器类 Container C;// 必须用这个容器来表示对象的存在状态,并且可以根据标识符得到对象 // 当然如果需要删除操作的话,对象就必须一定已经存在了 ///////////////////////////////////////////////////////// int id1=1,id2=2;// 两个对象的标识号创建参数保存于此 Object obj(10);// 对象创建参数保存于此 C.insert(std::make_pair(id1,obj));// 创建标识号为id1的对象 C.insert(std::make_pair(id2,obj));// 创建标识号为id2的对象 ///////////////////////////////////////////////////////// // 下面才能够开始实现删除操作 // 为了能够实现撤销操作而必须保存的信息 Object OBK1 = C[id1];// 备份标识号为id1的对象的原始信息 Object OBK2 = C[id2];// 备份标识号为id2的对象的原始信息 // 开始对对象实现删除操作 C.erase(id1);// 删除标识号为id1的对象 C.erase(id2);// 删除标识号为id2的对象 // 开始对对象实现撤销操作 C.insert(std::make_pair(id1,OBK1));// 创建标识号为id1的对象 C.insert(std::make_pair(id2,OBK2));// 创建标识号为id2的对象 // 开始对对象实现重做操作 C.erase(id1);// 重做删除标识号为id1的对象 C.erase(id2);// 重做删除标识号为id2的对象 }
复合操作的原理实现:
{// 复合操作 // 由于复合操作需要对三个基本操作进行保存,也需要对子复合操作 // 进行保存,在这里不方便给出,所以在后续的章节对这三个基本操 // 作进行了封装之后才能够讨论。 }
从上面的三种操作的原理实现可以看出,为了实现三个基本操作的撤销和重做,都必须在 执行操作之前进行必要的数据备份,为撤销和重做提供必要的数据。
此外,表达对象存在和消失的状态是通过容器类(Container)来实现的!容器类的状态变 化是不可以撤销和重做的,这也正是体现了前面所提到的新功能必须借助于现有的基础才 能实现的思想。此外用整数作为区别同类型对象的标识符!采用整数作为标识符是本框架 在最初的时候采用的方案。但是后来发现:采用指针作为标识符比整数作标识符更加 直接、方便且高效!这在后面的代码中将会直接反映出来:-)
复合操作没能实现的原因已经在代码里面说明了:-)
撤销和重做的基本架构
在上一节中,已经对三种基本操作的撤销和重做的原理实现进行了细致的分析,并给出了 示例代码,但是并没能够给出复合操作的示例代码。这里面有着比较深刻的原因,具体来 说就是:复合操作里面保存的是三个基本操作以及子复合操作的任意组合,这里面本身就 存在一个将三个基本操作和复合操作对象化的概念,在三个基本操作实现对象化 之前是不能够讨论复合操作的!既然需要对三个基本操作进行对象化,同时还应该看到: 复合操作可以保存其它的复合操作,这说明复合操作也需要对象化!既然三个基本操作和 一个复合操作都需要对象化,并且这些对象都可以保存到复合操作里面,那么在对三个基 本操作的对象化的同时就应该考虑复合操作的对象化问题!
现在就来讨论一下三个基本操作和一个复合操作的对象化问题,不过在讨论这个问题之前 必须先回答下面的几个问题:
1. 三个基本操作和一个复合操作的组合过程需要撤销和重做功能吗?
2. 三个基本操作和一个复合操作怎样保存在复合操作里面?
3. 撤销和重做功能在应用程序里面是怎么使用的?
对于问题1可以肯定的说是不需要撤销和重做功能的,否则就会成为“蛋鸡问题”!究竟是 先有蛋还是先有鸡呢?我想这不应该是我回答的问题吧:-)
对于问题2,C++里面有比较完美的方案,那就是C++天生具备的多态能力,所以问题2的答 案就是通过保存操作类的基类指针指向不同类型的对象实例的方法。
对于问题3就比较麻烦了,不过还是有比较简单的方案的:应用程序中保存一个可撤销操作 的命令队列,同时也要保存一个可重做操作的命令队列;每当执行一个操作的时 候,在执行完毕之后将这个命令保存到可撤销操作命令队列尾部,并且清空可重做操 作命令队列;每当执行撤销操作的时候,执行这个可撤销操作的反执行操作,执行完 毕之后将这个命令从可撤销操作队列尾部移到可重做操作队列尾部;对于重做操作则是执 行可重做操作队列中的操作的执行操作,执行完毕之后将这个操作的命令从可重做操作队 列尾部移到可撤销操作队列尾部。
为了以后讨论的方便,在此引进“命令”概念:
命令
· 将操作编码为C++类之后就成了命令类,命令类创建的对象就是命令
有了以上问题的讨论便可以得出如下的概念:
1. 命令的操作对象都是另外存在的一个容器(对象存在和消失的状态由其保存),这 个容器必须满足:
a. 用标识号区别容器中的同类型的不同对象,而且这种绑定是在程序运行期间是 终生绑定的!
b. 能够根据标识号得到对象
2. 必须有一个命令基类,这样才可以将所有的命令保存到复合命令中;同时 命令基类必须提供接口,以提供执行操作和反执行操作。
3. 复合命令实际上是一个命令队列,执行操作就是顺序调用子命令的执行操 作,而反执行操作就是逆序调用子命令的反执行操作。
关于标识符与对象的绑定是终生绑定的问题根源在于:当标识符绑定的 对象在执行删除操作之后,该标识符又和别的对象进行了绑定,那么当这个删除操作撤销 的时候就会出问题!因为一个标识符不可以用来标识两个对象,否则如何能够根据这个标 识符来得到绑定的对象呢?
有了这些讨论,现在可以得出标识符应该具备的功能列表了:
1. 在程序的运行期间的任何时刻,一个标识符都要能够且只能够索引出一个对象
2. 只有当标识符被完全废止的时候才能够重新使用这个标识符
3. 所谓的标识符完全废止,是指在程序运行到废止该标识号之后的任何时候 都不存在调用该标识号索引相应对象的代码
在前面的原理实现中,给出的是采用int类型的标识符。关于标识符的产生,采用的是预先 分配的方式(直接给出了id1和id2)。这对于固定的对象代码当然没有问题,但是要想使 得标识号能够用到动态生成的对象上面,就必须提供一个标识号自动生成的函数了!
经过深入的考虑(大约两年多时间)之后,发现:采用指针作为标识符比整数作标识 符更加直接、方便且高效,具体原因如下:
1. 在程序的运行周期内,程序分配的指针值(整数值)是绝对不会重复的!当然这也 是要满足一定条件的,在讨论后面的容器类的时候会讨论到这一点。这就解决了标 识符自动生成的问题:-)
2. 标识符要求能够根据标识符索引得到绑定的对象!关于这一点C++指针天生就具备 这种能力,根本就不需要额外的编码实现!从而采用指针作为标识符使得C++实现 更加自然
3. 要想在一个对象内部引用其它对象,采用指针是一个更加直接的方式!试想一下, 如果一个对象内部放置了一个整数用来索引其它的对象,这会多么的别扭啊!此外 用整数标识其它类型的对象把对象的类型弄丢了!这在C++中应该尽力避免,因为 C++的高效很大一部分在于类型系统的编译期优化
有了上面关于标识符的详细讨论之后,在后面的章节中将会采用指针作为标识符来实现这 个撤销和重做框架。
好了现在终于可以给出三个基本命令和一个复合命令的示例代码了:
命令基类command(提供两个公共的接口:redo和undo)的实现 如下:
struct command { virtual ~command(){}// 避免内存泄漏 virtual void redo()=0;// 重做操作,兼任执行操作 virtual void undo()=0;// 撤销操作 };
Note
· 将操作概念映射到具体的C++类,我将这种映射得到的C++类命名为操作
· 类,而操作类定义的对象就是命令,而这种将操作定义为命令
· 的过程就是对象化。
组合操作类的复合操作类:
class batch // 复合命令 : public command // 是一种命令 , public std::list<command*> // 用来记录子命令的命令队列 { public: virtual~batch()// 当复合命令被释放的时候,所有的子命令也要释放 { this->free(); } void record(command*pCmd)// 记录子命令,子命令必须通过new创建 { this->push_back(pCmd); } public: virtual void redo()// 执行操作和重做操作 { std::for_each(begin(),end(),std::mem_fun(&command::redo)); } virtual void undo()// 撤销操作 { std::for_each(rbegin(),rend(),std::mem_fun(&command::undo)); } void free()// 用复合命令实现命令中心的时候,需要这个函数 { std::for_each(this->begin(),this->end(),kill());// 释放子命令对象 this->clear();// 释放子命令对象指针 } };
其中的仿函数kill的实现如下:
struct kill// 给STL使用的仿函数 { template<class T> void operator()(T*ptr) { delete ptr;// delete自动判断ptr是否为NULL } };
这里的kill纯粹是为了使得编码更加简洁。
将撤销和重做的基本架构模组化
虽然有了上面的命令基类、复合操作类就可以实现任意的撤销和重做功能的程序了 ,但是很明显,还需要编写大量的派生自命令基类的各种各样的操作类。那么有没 有办法减轻或者消除这种负担呢?
在前面讨论过程中,我们已经假定任何操作都可以采用三种基本操作和一个复合 操作来实现,那么撤销和重做的架构的自动化就要以这个假定为基础了。
在前面讨论三个基本操作的时候,我们知道对象的存在状态必须通过另外的对象来表现, 因此需要提供一个始终存在的对象来表示。在这里就采用和STL类似的容器类来表 示这种状态:
template <class T> class container :public std::set<T*> { public: typedef T object_type; typedef std::set<T*> pointers_type; typedef std::list<T> objects_type; typedef std::map<T*,int> unused_type; protected: objects_type _objects;// 所有的对象都保存在这里 unused_type _unused;// 第二个参数是对应的变量被命令引用的数量 public: // 下面两个函数是在命令的创建和删除的时候调用的 // 配合_unused就是一个受管理的引用计数智能指针 // 只是引用该对象的“智能指针”只能是命令 void increase(T*id){ _unused[id]++; } void decrease(T*id){ _unused[id]--; } // 获得一个可以使用的对象空间,当没有对象可以回收的时候就创建一个 virtual T*generate(T* used = NULL) { T* ptr = used; typename unused_type::iterator it; // 如果_unused中有引用计数为零的对象,直接返回该指针 for( it = _unused.begin(); it != _unused.end(); ++it ) { if(0 == it->second && used != it->first) { ptr = it->first; break; } } // 如果_unused中没有引用计数为零的对象,在_vector中新建一个对象 // 并返回该新建对象的指针 if( used == ptr ) { _objects.push_back(T());// 调用默认构造函数的情况 ptr = &_objects.back(); } return ptr; } // 得到某个指针被命令引用的次数,不存在的指针引用计数为零 typename unused_type::size_type used(T**pID) { return (_unused.find(*pID)==_unused.end())?0:_unused[*pID]; } public: virtual void create(T*id) { this->insert(id); } virtual void modify(T*id,const T&v) { *id = v; } virtual void remove(T*id) { this->erase(id); } };
此容器类已经采用了指针作为标识符!把这里的代码和前面的原理实现中的容器类(STL的 map容器)进行一下对比便可以发现非常类似,但也有很多不同。这是因为这个容器类考虑 了前面谈到的标识符所必须具备的功能!下面逐一的进行解释:
标识符自动创建
· 在程序的运行周期内,程序分配的指针值(整数值)是绝对不会重复的!
1. 在上面的容器类里面,container向外提供的是set概念,也就是说容器里面的标识 符(指针)是不允许重复的!
2. 既然标识符是指针,那么必然有管理指针指向的对象的容器,在这里是_objects。 最初使用的是STL的vector类,但是经过使用发现,采用vector不能满足需求,相 反采用list却比较能够满足需求!因为这个容器仅仅只是保存对象,对这些对象只 执行一次添加操作,添加的同时把新添加的对象地址值取出,作为标识符。除此之 外,不需要任何其他的操作了。
3. 对象的分配是通过_objects进行管理的。这样可以降低复杂度!基本分配原理就是 :首先看看_unused里面有没有引用计数为零的还没有被释放的闲置对象 。有就直接利用之,反之则在_objects中追加一个,同时将追加的对象地 址取出返回!
4. 既然有对象分配,自然也要有资源的回收。上面的容器类的实现中,_unused容器 是一个map容器,作用是和increase()和decrease()函数配合使用,构成一个引用 计数的“智能指针”概念!特别需要注意的是,这里的“智能指针”就是我们讨论 的三个基本命令对象!
索引对象
· C++指针天生就可以索引到对象:-)
嵌入到其他对象
· 在其他的对象里面可以直接编写对象指针代码,这样就可以嵌入到其他的对象里面了
· !这一点在处理复杂对象的时候特别有用(当然要配合自动化编程的高级技术才可以
· )。
create()modify() remove()
· 分别被三个基本命令调用,这样就可以比较对称的处理三个基本操作了:)撤销和重
· 做的能力全部来自于三个基本命令类对必要数据的备份:)当然也可以采用其他专用
· 的方法可以避免一些数据的备份,或许可以节省一些时间和空间上的开销,但是通常
· 来说,这样是得不偿失的!即使采取各种各样的技巧,也未必真的可以减少时间或空
· 间上的开销,见前面的swap函数的讨论。
used()
· 因为对象的指针是作为标识符的,同时也可以直接操作对象!既然可以直接操作对象
· ,那么就可以在撤销和重做库的外部保存该指针变量,该指针变量有可能是动态分配
· 的就会出现释放的问题,所以在此提供一个查询指针的命令引用计数的函数。当该函
· 数返回0的时候,就表示没有命令引用该指针,也就表示可以安全删除该指针变量:)
· 特别注意不是删除指针指向的对象,而是指针变量而已,指针所指向的对象
· 并没有被删除,该对象由容器类管理。
有了上面的容器类之后,就可以来编写属于该容器类的三个基本操作类了:
创建操作类:
template<class Container> class create :public command { void init() { // 下面的generate函数主要是在第一次执行创建命令的时候起作用 _ID = _C.generate(_ID);// 如果_ID已经被使用了,则创建新的对象空间 _C.increase(_ID);// 容器类的引用计数增一 } public: typedef typename Container::object_type T; create(Container&C,T*&ID):_C(C),_ID(ID) { init(); } create(Container&C,T*&ID,const T&O):_C(C),_ID(ID) { init(); *_ID = O; } virtual~create() { _C.decrease(_ID);// 容器类的引用计数减一 } void redo() { _C.create(_ID); } void undo() { _C.remove(_ID); } private: create(){} Container &_C; T*& _ID; };
修改操作类:
template<class Container> class modify:public command { public: typedef typename Container::object_type T; public: modify(Container&C,T*ID,const T&O):_C(C),_ID(ID),_O(O) { _OB = *_ID;// 备份信息 _C.increase(_ID); } virtual~modify() { _C.decrease(_ID); } void redo() { _C.modify(_ID,_O); } void undo() { _C.modify(_ID,_OB); } private: bool _backuped;// 是否已经进行过备份啦,避免重复备份的问题 modify(){} Container &_C; T*_ID; T _O ;// 修改参数 T _OB;// 修改之前的对象 };
删除操作类(因为delete是C++操作符,所以选择了remove):
template<class Container> class remove:public command { typedef typename Container::object_type T; public: remove(Container&C,T*&ID):_C(C),_ID(ID),_IDB(ID) { _C.increase(_ID); } virtual~remove() { _C.decrease(_ID); } void redo() { _C.remove(_ID); _ID = NULL;// 标识号为NULL表示已经被删除了 } void undo() { _ID = _IDB;// 撤销了删除操作,标识号应该复原 _C.create(_ID); } private: remove(){} Container &_C ; T*&_ID; T* _IDB; };
有了前面的这些讨论就可以用最少的编码付出得到非常强大的撤销和重做功能。下面是一 个使用中的实际例子:
在测试代码中使用的对象:
//对象类 class Object { public: Object():_member(0){} Object(int m):_member(m){} //这个函数是前面的命令中所必须的操作符 Object&operator=(const Object&o) { if(&o!=this) { _member = o._member; } return *this; } private: int _member; //下面的这个函数仅仅是为了方便显示对象信息而重载的操作符 template <class Stream> friend Stream&operator << (Stream&s,Object&o) { s << o._member; return s; } }; //为了能够判断操作的正确性需要输出容器的信息 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; }
测试代码:
创建命令的撤销和重做示例:
{//模拟创建操作的撤销和重做 typedef undo::raw::container<Object> CONTAINER; typedef undo::raw::create<CONTAINER> CREATE; CONTAINER::object_type* id1 = NULL; CONTAINER::object_type* id2 = NULL; CONTAINER C; CREATE*pCmd1 = new CREATE(C,id1); CREATE*pCmd2 = new CREATE(C,id2); display("创建CREATE命令之后:",C); pCmd1->redo();//模拟创建标识号为id1的Object对象 pCmd2->redo();//模拟创建标识号为id2的Object对象 display("执行CREATE命令之后:",C); pCmd1->undo();//模拟撤销创建标识号为id1的Object对象 pCmd2->undo();//模拟撤销创建标识号为id2的Object对象 display("撤销CREATE命令之后:",C); pCmd1->redo();//模拟重做创建标识号为id1的Object对象 pCmd2->redo();//模拟重做创建标识号为id2的Object对象 display("重做CREATE命令之后:",C); delete pCmd1;delete pCmd2; }
测试结果:
创建CREATE命令之后:[0] 执行CREATE命令之后:[2] (00375600,0) (003756A8,0) 撤销CREATE命令之后:[0] 重做CREATE命令之后:[2] (00375600,0) (003756A8,0)
修改命令的撤销和重做示例:
{//模拟修改操作的撤销和重做 //在执行修改操作的时候,被修改的对象当然应该已经存在 //////////////////////////////////////////////////////////// typedef undo::raw::container<Object> CONTAINER; CONTAINER C; CONTAINER::object_type* id1 = NULL; CONTAINER::object_type* id2 = NULL; { typedef undo::raw::create<CONTAINER> CREATE; CREATE*pCmd1 = new CREATE(C,id1); CREATE*pCmd2 = new CREATE(C,id2); pCmd1->redo();//模拟创建标识号为id1的Object对象 pCmd2->redo();//模拟创建标识号为id2的Object对象 delete pCmd1;delete pCmd2; } //////////////////////////////////////////////////////////// //下面才可以进行修改操作的撤销和重做模拟了 typedef undo::raw::modify<CONTAINER> MODIFY; MODIFY*pCmd1 = new MODIFY(C,id1,Object(20)); MODIFY*pCmd2 = new MODIFY(C,id2,Object(50)); display("创建MODIFY命令之后:",C); pCmd1->redo();//模拟修改标识号为id1的Object对象 pCmd2->redo();//模拟修改标识号为id2的Object对象 display("执行MODIFY命令之后:",C); pCmd1->undo();//模拟撤销修改标识号为id1的Object对象 pCmd2->undo();//模拟撤销修改标识号为id2的Object对象 display("撤销MODIFY命令之后:",C); pCmd1->redo();//模拟重做修改标识号为id1的Object对象 pCmd2->redo();//模拟重做修改标识号为id2的Object对象 display("重做MODIFY命令之后:",C); delete pCmd1;delete pCmd2; }
测试结果:
创建MODIFY命令之后:[2] (003755D0,0) (00375690,0) 执行MODIFY命令之后:[2] (003755D0,50) (00375690,20) 撤销MODIFY命令之后:[2] (003755D0,0) (00375690,0) 重做MODIFY命令之后:[2] (003755D0,50) (00375690,20)
删除命令的撤销和重做示例:
{//模拟删除操作的撤销和重做 //在执行删除操作的时候,被删除的对象当然应该已经存在 //////////////////////////////////////////////////////////// typedef undo::raw::container<Object> CONTAINER; CONTAINER C; CONTAINER::object_type* id1 = NULL; CONTAINER::object_type* id2 = NULL; { typedef undo::raw::create<CONTAINER> CREATE; CREATE*pCmd1 = new CREATE(C,id1); CREATE*pCmd2 = new CREATE(C,id2); pCmd1->redo();//模拟创建标识号为id1的Object对象 pCmd2->redo();//模拟创建标识号为id2的Object对象 delete pCmd1;delete pCmd2; } //////////////////////////////////////////////////////////// //下面才可以进行删除操作的撤销和重做模拟了 typedef undo::raw::remove<CONTAINER> REMOVE; REMOVE*pCmd1 = new REMOVE(C,id1); REMOVE*pCmd2 = new REMOVE(C,id2); display("创建REMOVE命令之后:",C); pCmd1->redo();//模拟删除标识号为id1的Object对象 pCmd2->redo();//模拟删除标识号为id2的Object对象 display("执行REMOVE命令之后:",C); pCmd1->undo();//模拟撤销删除标识号为id1的Object对象 pCmd2->undo();//模拟撤销删除标识号为id2的Object对象 display("撤销REMOVE命令之后:",C); pCmd1->redo();//模拟重做删除标识号为id1的Object对象 pCmd2->redo();//模拟重做删除标识号为id2的Object对象 display("重做REMOVE命令之后:",C); delete pCmd1;delete pCmd2; }
测试结果:
创建REMOVE命令之后:[2] (003755E8,0) (003756A8,0) 执行REMOVE命令之后:[0] 撤销REMOVE命令之后:[2] (003755E8,0) (003756A8,0) 重做REMOVE命令之后:[0]
复合命令的撤销和重做示例:
{//模拟复合操作的撤销和重做 typedef undo::raw::container<Object> CONTAINER; typedef undo::raw::create<CONTAINER> CREATE; typedef undo::raw::batch BATCH; CONTAINER::object_type* id1 = NULL; CONTAINER::object_type* id2 = NULL; CONTAINER C; BATCH *pMCmd = new BATCH(); pMCmd->record(new CREATE(C,id1)); pMCmd->record(new CREATE(C,id2)); display("创建BATCH命令之后:",C); pMCmd->redo();//模拟执行复合命令 display("执行BATCH命令之后:",C); pMCmd->undo();//模拟撤销复合命令 display("撤销BATCH命令之后:",C); pMCmd->redo();//模拟重做复合命令 display("重做BATCH命令之后:",C); delete pMCmd; }
测试结果:
创建BATCH命令之后:[0] 执行BATCH命令之后:[2] (003755D0,0) (003756D8,0) 撤销BATCH命令之后:[0] 重做BATCH命令之后:[2] (003755D0,0) (003756D8,0)
从上面的代码中,可以看出三个基本操作对象化之后成为三个基本命令,而一个 复合操作则对象化为一个复合命令。从上面的示例代码中已经可以看出使用这种 方式的撤销和重做机制需要遵守:
1. 必须用标识号来区分同种类型的不同对象
2. 对象必须是可拷贝的(在三个基本命令中都需要)
3. 所有命令对象的创建都必须使用new操作符的方式创建(这是为了方便管理而提出 的)
4. 标识号绝对不允许重复;关于这一点其实可以很容易弄错的,所以在这里特别解释 一下。所谓的标识号绝对不允许重复是指:任意的标识号在应用程序中最多只能使 用一次,记住了,是在应用程序运行的整个生命周期中只能够使用一次(暂时用这 么强的语气,在一定的条件下,这种要求可以降低),就像GUID不允许重复一样。 有这种要求的原因,一方面是为了是代码更加趋于简单、可靠;另一方面的原因是 为了程序的运行效率。关于这一点会在后续的文章中详细解释。表面上看GUID就可 以满足这里的要求,但是我并不采用它,因为GUID相关的函数是Windows平台所特 有的,这样就限制了本文所介绍的撤销和重做机制的平台无关性;另一方面是因为 GUID占用16个字节的空间,对于上面的示例代码来说,标识号类型的尺寸比对象的 尺寸还大,显然很不划算,关于这一点和字符编码的方案有些类似,Unicode编码 方式就可以节省大量的ASCII编码所表示的信息,同时也可以表示无穷多的字符编 码,在后续的章节中就会给出一个特别定制的标识符类,以达到我们的要求
处理多类型的对象以及命令管理
目前为止,已经成功的将三个基本操作封装为三个基本命令,同时也将一个复合操作封装 成了一个复合命令,另外也给出了简单的使用代码;从代码中可以看出,撤销和重做的过 程还是比较晦涩,用户为了表达撤销和重做的过程还需要编写很多的额外代码,而且这种 代码也没有很直接的表达撤销和重做的思想。
最为重要的一点是:在上面的使用例子中,所有的命令是分散的,缺乏一个集中管理的地 方,从而导致了编码的繁琐和不规范!
为了更好的表达撤销和重做机制,并且更加简化客户端代码的书写,在此将会对前面给出 的代码进行更深入的封装,让客户端编写的代码尽可能的少,同时也能够更加直接的表达 撤销和重做的意思。
总的说来,在此要达到下列目标:
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容器允许容纳的最大 元素数量。由此可见,真正意义上的无限次数并不存在,只要这个数足够大就可以了。另 外计算机的存储容量再大,也还是有限的。所以可以这么说:在本文中已经成功的实现了 无限次的撤销和重做方案。
有了前面的这些基础性的代码之后,我们就可以处理大量的撤销和重做操作了:)但是通 常的情况下,我们还是希望能够往command里面添加一些额外的信息。
例如:为了在图形用户界面应用程序里面显示还可以撤销的命令队列列表以及可以重做的 命令列表,这两种情况下都需要给每一个命令添加一个额外的名称信息,所以我们将前面 的command代码修改为下面的basic_command代码:
class DefaultExtraData{};//默认的额外信息
template<class ExtraData=DefaultExtraData>
struct basic_command :public ExtraData// 扩展了command类
{
virtual ~basic_command(){}//避免内存泄漏
virtual void redo()=0;//重做操作,兼任执行操作
virtual void undo()=0;//撤销操作
};
template<>struct basic_command<DefaultExtraData>
{
virtual ~basic_command(){}//避免内存泄漏
virtual void redo()=0;//重做操作,兼任执行操作
virtual void undo()=0;//撤销操作
};
特别注意上面的代码,为了避免空基类,采用了模板特化的静态选择机制 ,这样就可以根据模板参数选择合适的command类。
从上面的代码还可以看出,提供一个ExtraData给basic_command就可以完全的改变前面提 供的command的结构!可以增加属性,也可以增加方法。总之所有可以添加的信息都可以, 现在给出一个示例:
struct MyExtraData
{
std::stringname;// 命令的名字
};
有了这个额外的数据之后,就可以采用这一系列的basic_nx模板类来使得命令类可以添加 额外的属性信息了;)当然仅仅限于basic_command模板类及其派生类才有这个功能, command仍然没有这个功能!
除了上面的给命令添加额外的信息需求之外,还有一个比较常见的问题会在应用 中出现:容器类中盛放的对象类型的默认构造函数不能调用的时候,前面的 container类就无能为力了!所以在此也要给出一种处理这种情况的能力!除此之外,前面 还假定:该容器类里面的所有对象的修改都是通过对象类的赋值运算符实现的, 这在大多数情况下是可以接受的,但是这样毕竟还是一个限制,而且这是不必要的!
可以处理非默认构造函数创建对象的容器类:
template<class T,class Config=basic_config<T>>class basic_container:public std::set<T*>
{
public:
typedef T object_type;
typedef std::list<T> objects_type;
typedef std::map<T*,int> unused_type;
typedef typename Config::create_type create_type;
typedef typename Config::modify_type modify_type;
private:
objects_type_objects;// 所有的对象都保存在这里
unused_type _unused;// 第二个参数是对应的变量被命令引用的数量
public:
// 得到标识号的命令引用计数
typename unused_type::mapped_type used(T**pID)
{
return _unused.find(*pID)==_unused.end()?0:_unused[*pID];
}
// 下面两个函数是在命令的创建和删除的时候调用的
// 配合_unused就是一个受管理的引用计数智能指针
// 只是引用该对象的“智能指针”只能是命令
void increase(T*ID){ _unused[ID]++; }
void decrease(T*ID){ _unused[ID]--; }
// 获得一个可以使用的对象空间,当没有对象可以回收的时候就创建一个
T*generate(T*used = NULL)
{
T* ptr = used;
typename unused_type::iteratorit;
// 如果_unused中有引用计数为零的对象,直接返回该指针
for( it = _unused.begin(); it !=_unused.end(); ++it )
{
if(0 == it->second && used != it->first)
{
ptr = it->first;
break;
}
}
// 如果_unused中没有引用计数为零的对象,在_objects中新建一个对象
// 并返回该新建对象的指针
if( used == ptr )
{
// Create避免了某些对象不可以通过默认构造函数创建的问题
_objects.push_back(create_type()());//调用Create仿函数创建对象
ptr = &_objects.back();
}
return ptr;
}
public:
void create(T*ID)
{
this->insert(ID);
}
void modify(T*ID,const T&v)
{
modify_type()(*ID,v);// 通过指定的修改仿函数实现修改过程
}
void remove(T*ID)
{
this->erase(ID);
}
};
容器类增加了一个配置(basic_config)参数,配置参数中主要是创建仿函数和修改仿函数 !分别解决下面的问题:
1. 创建仿函数(create)。主要用来处理那些默认构造函数不是public属性的类 ,创建这些对象就只能通过其他的构造函数了!
2. 修改仿函数(modify)。主要用来处理那些不可以通过赋值运算符修改对象属 性的类,修改这些对象就只能通过其他的方法了!
有了上面的两个模板参数之后就可以使得basic_container类具有极大的可扩展性和处理能 力。下面是容器类需要的配置类:
template<class T>class basic_config
{
protected:
struct Create
{
const T&operator()()
{
static const T O;//默认构造函数
return O;
}
};
struct Modify
{
void operator()(T&lhs,const T&rhs)
{
lhs = rhs;// 默认采用赋值运算符实现修改
}
};
public:
typedef Create create_type;
typedef Modify modify_type;
};
有了上面的这种容器类之后就可以处理创建命令特别常用的一种功能:创建对象的时候能 够提供创建参数,在创建对象的时候就对对象进行修改,只是这种修改是不可以撤销的! 可以添加额外信息并且可以提供创建参数的创建命令:
template<class Container,class Command>class basic_create :public Command
{
void initialize()
{
// 下面的generate函数主要是在第一次执行创建命令的时候起作用
_ID = _C.generate(_ID);// 如果_ID已经被使用了,则创建新的对象空间
_C.increase(_ID);// 容器类的引用计数增一
}
typedef typename Container::object_type T;
typedef typename Container::modify_type M;
public:
basic_create(Container&C,T*&ID):_C(C),_ID(ID)
{
initialize();
}
// 经常的时候还需要在创建的时候就赋予初始值
basic_create(Container&C,T*&ID,const T&O):_C(C),_ID(ID)//,_O(O)
{
initialize(); M()(*_ID , O);// 创建的时候直接赋初始值
}
virtual~basic_create()
{
_C.decrease(_ID);// 容器类的引用计数减一
}
void redo()
{
_C.create(_ID);
}
void undo()
{
_C.remove(_ID);
}
private:
basic_create(){}
Container &_C;
T*& _ID;
};
可以添加额外信息的修改命令:
template<class Container,class Command>class basic_modify :public Command
{
typedef typename Container::object_type T;
typedef typename Container::modify_type M;
public:
basic_modify(Container&C,T*ID,const T&O):_C(C),_ID(ID),_O(O)
{
M()(_OB , *_ID);// 备份信息
_C.increase(_ID);
}
virtual~basic_modify()
{
_C.decrease(_ID);
}
void redo()
{
_C.modify(_ID,_O);
}
void undo()
{
_C.modify(_ID,_OB);
}
private:
basic_modify(){}
Container &_C;
T*_ID;
T _O ;// 修改参数
T _OB;// 修改之前的对象备份
};
可以添加额外信息的删除命令:
template<class Container,class Command>class basic_remove :public Command
{
typedef typename Container::object_type T;
public:
basic_remove(Container&C,T*&ID):_C(C),_ID(ID),_IDB(ID)
{
_C.increase(_ID);
}
virtual~basic_remove()
{
_C.decrease(_ID);
}
void redo()
{
_C.remove(_ID);
_ID = NULL;//标识号为NULL表示已经被删除了
}
void undo()
{
_ID = _IDB;// 撤销了删除操作,标识号应该复原
_C.create(_ID);
}
private:
basic_remove(){}
Container&_C ;
T*&_ID;
T* _IDB;
};
下面是三个基本命令的扩展代码的测试用例:
typedef undo::basic_container<std::string>CT;
typedef undo::basic_command<MyExtraData>Command;
typedef undo::basic_create<CT,Command>Create;
typedef undo::basic_modify<CT,Command>Modify;
typedef undo::basic_remove<CT,Command>Remove;
CT c;
CT::object_type* PANDAXCL = NULL;
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),0);
CreatecreatePandaxcl(c,PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_EQUAL(c.size(),0);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),1);
// 在执行其它的命令之前必须保证这个对象已经存在
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,NULL);
createPandaxcl.redo();
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,NULL);
// 如果上面的命令不执行的话,后面的对这个对象的修改和移除操作都会失败
// 调用了上面的create命令后可以进行检查了
AUTOCXX_ASSERT_EQUAL(c.size(),1);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),1);
// 尝试create命令的撤销
createPandaxcl.undo();// 调用create命令的撤销操作
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,NULL);
// 调用了上面的create命令的撤销命令之后可以进行检查了
AUTOCXX_ASSERT_EQUAL(c.size(),0);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),1);
// 尝试create命令的重做操作
createPandaxcl.redo();// 调用create命令的重做操作
// 调用了上面的create命令的撤销命令之后可以进行检查了
AUTOCXX_ASSERT_EQUAL(c.size(),1);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),1);
// 现在来尝试一下modify命令,特别注意:使用modify和remove命令的时候
// 必须保证相应的对象已经存在了,否则结果就是未知的:)
// 下面的这个修改命令将ID为PANDAXCL的对象修改为大写形式
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),1);
Modify modifyPandaxcl(c,PANDAXCL,std::string("PANDAXCL"));
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),2);
// 调用modify的执行操作(又称重做操作)
modifyPandaxcl.redo();// 相当于执行操作
// 调用了上面的modify命令的执行命令之后可以进行检查了
AUTOCXX_ASSERT_EQUAL(c.size(),1);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),2);
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("PANDAXCL"));
// 调用modify的撤销操作
modifyPandaxcl.undo();
// 调用了上面的modify命令的执行命令之后可以进行检查了
AUTOCXX_ASSERT_EQUAL(c.size(),1);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),2);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string("PANDAXCL"));
// 调用modify的重做操作(又称执行操作)
modifyPandaxcl.redo();
// 调用了上面的modify命令的重做命令之后可以进行检查了
AUTOCXX_ASSERT_EQUAL(c.size(),1);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),2);
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("PANDAXCL"));
// 再次调用modify的撤销操作
modifyPandaxcl.undo();
// 调用了上面的modify命令的执行命令之后可以进行检查了
AUTOCXX_ASSERT_EQUAL(c.size(),1);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),2);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string("PANDAXCL"));
// 下面来尝试一下remove操作,同modify操作一样,执行remove操作的时候必须
// 保证指定ID的对象已经存在了!
// 下面的这个remove删除ID为PANDAXCL的对象
Remove removePandaxcl(c,PANDAXCL);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),3);
// 调用remove命令的执行操作
removePandaxcl.redo();
// 调用remove命令的执行操作之后来核实一下我们想要的结果
AUTOCXX_ASSERT_EQUAL(c.size(),0);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),0);
// 调用remove命令的撤销操作
removePandaxcl.undo();
// 调用remove命令的撤销操作之后来核实一下我们想要的结果
AUTOCXX_ASSERT_EQUAL(c.size(),1);
AUTOCXX_ASSERT_EQUAL(c.used(&PANDAXCL),3);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_NOT_EQUAL(*PANDAXCL,std::string("PANDAXCL"));
// 同样你也可以对一个命令执行很多次,但是需要特别注意上面的几个顺序问题
// 1. create命令可以在对象不存在的时候创建,但是不能创建相同ID的对象两
// 次及其以上
// 2. modify命令必须在指定ID存在的时候进行操作,否则程序运行意义未知
// 3. remove命令必须在指定ID存在的时候进行操作,否则程序运行意义未知
// 为了对上面的三种情况进行保证,才出现了undo里面的其它的几个模版类
可以添加额外信息的复合命令:
template<class Command>class basic_batch// 复合命令
:public Command// 是一种命令
,public std::list<Command*>// 用来记录子命令的命令队列
{
typedef Command command_type;
public:
virtual~basic_batch()//当复合命令被释放的时候,所有的子命令也要释放
{
this->free();
}
void record(command_type*pCmd)//记录子命令,子命令必须通过new创建
{
this->push_back(pCmd);
}
public:
virtual void redo()//执行操作和重做操作
{
std::for_each(this->begin(),this->end(),std::mem_fun(&command_type::redo));
}
virtual void undo()//撤销操作
{
std::for_each(this->rbegin(),this->rend(),std::mem_fun(&command_type::undo));
}
void free()//用复合命令实现命令中心的时候,需要这个函数
{
std::for_each(this->begin(),this->end(),kill());//释放子命令对象
this->clear();//释放子命令对象指针
}
};
下面是复合命令的扩展代码的测试用例:
typedef undo::basic_command<MyExtraData>Command;
typedef undo::basic_container<std::string>CT;
typedef undo::basic_create<CT,Command>Create;
typedef undo::basic_modify<CT,Command>Modify;
typedef undo::basic_remove<CT,Command>Remove;
CT c;
CT::object_type* PANDAXCL = NULL;
CT::object_type*QQ = NULL;
CT::object_type*BLOG = NULL;
CT::object_type*EMAIL = NULL;
CT::object_type*NETWORK = NULL;
// 这里进行batch命令的测试
undo::basic_batch<Command>bat;
// 首先让这个batch类记录多个(5个)命令,可以是三个基本的命令,也可以是batch命令
// 特别注意这里的命令都必须是通过new操作符得到的对象,否则结果未知
bat.record(new Create(c,PANDAXCL));
bat.record(new Create(c,QQ));
bat.record(new Create(c,BLOG));
bat.record(new Create(c,EMAIL));
bat.record(new Create(c,NETWORK));
// 先检查初始状态
AUTOCXX_ASSERT_EQUAL(c.size(),0);
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(QQ ,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(BLOG ,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(EMAIL ,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(NETWORK ,NULL);
// 执行这个batch命令的执行操作,具体意义视其记录的命令而定
bat.redo();// 执行操作
AUTOCXX_ASSERT_NOT_EQUAL(PANDAXCL,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(QQ ,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(BLOG ,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(EMAIL ,NULL);
AUTOCXX_ASSERT_NOT_EQUAL(NETWORK ,NULL);
////////////////////////////////////////////////////////////////////////////////
// 为了测试的方便,在此从外部修改对象状态
*PANDAXCL = std::string("熊春雷");
*QQ = std::string("56637059");
*BLOG =std::string("http://blog.csdn.net/pandaxcl");
*EMAIL =std::string("pandaxcl@163.com");
*NETWORK = std::string("http://www.autodev.net");
////////////////////////////////////////////////////////////////////////////////
// 检查状态
AUTOCXX_ASSERT_EQUAL(c.size(),5);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_EQUAL(*QQ ,std::string("56637059"));
AUTOCXX_ASSERT_EQUAL(*BLOG ,std::string("http://blog.csdn.net/pandaxcl"));
AUTOCXX_ASSERT_EQUAL(*EMAIL ,std::string("pandaxcl@163.com"));
AUTOCXX_ASSERT_EQUAL(*NETWORK,std::string("http://www.autodev.net"));
// 执行这个batch命令的撤销操作,具体意义视其记录的命令而定
bat.undo();
// 检查状态
AUTOCXX_ASSERT_EQUAL(c.size(),0);
// 执行这个batch命令的重做操作,具体意义视其记录的命令而定
bat.redo();// 表示重做操作而不是执行操作
// 检查状态
AUTOCXX_ASSERT_EQUAL(c.size(),5);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_EQUAL(*QQ ,std::string("56637059"));
AUTOCXX_ASSERT_EQUAL(*BLOG ,std::string("http://blog.csdn.net/pandaxcl"));
AUTOCXX_ASSERT_EQUAL(*EMAIL ,std::string("pandaxcl@163.com"));
AUTOCXX_ASSERT_EQUAL(*NETWORK,std::string("http://www.autodev.net"));
// 其实batch命令里面可以使用create,modify,remove和batch一共4种子命令
// 为了测试,先把bat这个batch命令里面记录的所有子命令删除
bat.free();
// 重新记录新的命令
bat.record(new Modify(c,PANDAXCL,std::string("PANDAXCL")));
bat.record(new Remove(c,QQ));
// 为了在batch里面记录子batch命令,需要额外声明一个变量
// 假想第一次创建了一个错误的电子邮箱地址然后再进行修改的过程
undo::basic_batch<Command> *childbat = new undo::basic_batch<Command>();
childbat->record(new Modify(c,NETWORK,std::string("http://www.autocxx.net")));
childbat->record(new Modify(c,NETWORK,std::string("http://www.autodev.net")));
// 最后在父batch命令里面记录这个子batch命令,注意:这个子batch命令也是new出来的
bat.record(childbat);
// 执行这个重新记录的batch命令的执行操作,具体意义视其记录的命令而定
bat.redo();// true表示执行操作
// 检查状态
AUTOCXX_ASSERT_EQUAL(c.size(),4);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("PANDAXCL"));
//AUTOCXX_ASSERT_EQUAL(*QQ,std::string("56637059"));
AUTOCXX_ASSERT_EQUAL(*BLOG,std::string("http://blog.csdn.net/pandaxcl"));
AUTOCXX_ASSERT_EQUAL(*EMAIL,std::string("pandaxcl@163.com"));
AUTOCXX_ASSERT_EQUAL(*NETWORK,std::string("http://www.autodev.net"));
// 执行这个batch命令的撤销操作,具体意义视其记录的命令而定
bat.undo();// false表示撤销操作
// 检查状态
AUTOCXX_ASSERT_EQUAL(c.size(),5);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("熊春雷"));
AUTOCXX_ASSERT_EQUAL(*QQ,std::string("56637059"));
AUTOCXX_ASSERT_EQUAL(*BLOG,std::string("http://blog.csdn.net/pandaxcl"));
AUTOCXX_ASSERT_EQUAL(*EMAIL,std::string("pandaxcl@163.com"));
AUTOCXX_ASSERT_EQUAL(*NETWORK,std::string("http://www.autodev.net"));
// 执行这个batch命令的重做操作,具体意义视其记录的命令而定
bat.redo();// true表示重做操作
// 检查状态
AUTOCXX_ASSERT_EQUAL(c.size(),4);
AUTOCXX_ASSERT_EQUAL(*PANDAXCL,std::string("PANDAXCL"));
//AUTOCXX_ASSERT_EQUAL(*QQ,std::string("56637059"));
AUTOCXX_ASSERT_EQUAL(*BLOG,std::string("http://blog.csdn.net/pandaxcl"));
AUTOCXX_ASSERT_EQUAL(*EMAIL,std::string("pandaxcl@163.com"));
AUTOCXX_ASSERT_EQUAL(*NETWORK,std::string("http://www.autodev.net"));
可以添加额外信息的命令中心:
template<class Command>class basic_undo_bat:public basic_batch<Command>{};
template<class Command>class basic_redo_bat:public basic_batch<Command>{};
template<class Command>class basic_center
:public basic_undo_bat<Command>
,public basic_redo_bat<Command>//命令中心,管理两种命令队列
{
private:
size_t _limit;
public:
typedef Command command_type;
typedef basic_batch <Command> batch_type;
typedef basic_redo_bat<Command>redo_type;
typedef basic_undo_bat<Command>undo_type;
typedef undo_type history_type;
basic_center():_limit(100)//默认最大撤销操作步骤总数为100步,一般的应用已经足够
{
}
virtual~basic_center()
{
}
public:
size_t limit()//获取撤销命令队列的最大命令数量
{
return _limit;
}
void limit(size_t Limit)//设置撤销命令队列的最大命令数量
{
_limit=Limit;
relimit();
}
void relimit()//检查撤销命令队列是否已满,剔除过期命令
{
typedef std::list<command_type*>QT;
if(_limit >=undo_type::size())return;
while(_limit <undo_type::size())
{
command_type *pCmd = 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、execute和stop三个函数实现复合命令的自动创建,只要保证如下
// 的固定格式即可:
// record();// 复合命令开始
// execute(命令1);//子命令
// execute(命令2);
// record();// 子复合命令开始
// ... ...
// stop();// 子复合命令结束
// ... ...
// execute(命令n);
// stop();// 复合命令结束
// 这种组合方式可以任意的嵌套,从而实现无级撤销
void record(command_type*pCmd=NULL)
{
history_type::record(pCmd);
redo_type::free();
}
// 下面的execute也可以单独使用,不需要record和stop的配合
void execute(command_type*pCmd)
{
assert(pCmd!=NULL);
pCmd->redo();
record(pCmd);
relimit();
}
void stop()
{
history_type &H=static_cast<history_type&>(*this);
typename history_type::iteratorresult=H.begin();
typename history_type::reverse_iteratorrit;
rit = std::find(H.rbegin(),H.rend(),static_cast<command_type*>(NULL));
std::advance(result,std::distance(rit,H.rend())-1);
if(result!=H.end())
{
typename history_type::difference_typediff;
diff = std::distance(result,H.end());
if(diff==1 || diff==2) {
H.erase(result);
}else{
batch_type*pBat=new batch_type();
pBat->splice(pBat->begin(),H,result,H.end());
pBat->remove(static_cast<command_type*>(NULL));//list
record(pBat);
}
}
}
};
下面是命令中心的扩展代码的基本状态的测试用例:
typedef undo::basic_command<MyExtraData>Command;
typedef undo::basic_container<std::string>CT;
typedef undo::basic_create<CT,Command>Create;
typedef undo::basic_modify<CT,Command>Modify;
typedef undo::basic_remove<CT,Command>Remove;
CT c;// 注意这里的容器类仍然是和撤销和重做概念是分离的,实际应用的过程可能是整合
// 在一起的,命令中心对象和容器对象是同一个对象,通过多重派生得到
// 在上面的代码中没有涉及到和一般的应用程序相关的类似概念,那么center模版类的出现这是
// 给出了这个非常近似的概念
undo::basic_center<Command>cc;// 和容器类的名称都及其类似,当然可以整合了,这里不这样做
CT::object_type* PANDAXCL = NULL;
CT::object_type*QQ = NULL;
CT::object_type*BLOG = NULL;
CT::object_type*EMAIL = NULL;
CT::object_type*NETWORK = NULL;
// 下面用center命令中心的观点来重新实现上面的功能
// 注意center对象的execute已经不可以直接使用了,为的就是下面的这个概念:
// 每一个操作都是立即执行的,这和一般的应用程序里面的操作概念非常类似
AUTOCXX_ASSERT_EQUAL(c.size(),0);
cc.execute(new Create(c,PANDAXCL));
AUTOCXX_ASSERT_EQUAL(c.size(),1);
cc.execute(new Create(c,QQ));
AUTOCXX_ASSERT_EQUAL(c.size(),2);
cc.execute(new Create(c,BLOG));
AUTOCXX_ASSERT_EQUAL(c.size(),3);
cc.execute(new Create(c,EMAIL));
AUTOCXX_ASSERT_EQUAL(c.size(),4);
// 撤销一步操作
cc.undo();
AUTOCXX_ASSERT_EQUAL(c.size(),3);
// 重做一步操作
cc.redo();
AUTOCXX_ASSERT_EQUAL(c.size(),4);
// 撤销多步操作
cc.undo(3);
AUTOCXX_ASSERT_EQUAL(c.size(),1);
// 重做多步操作
cc.redo(3);
AUTOCXX_ASSERT_EQUAL(c.size(),4);
//撤销多步操作(如果撤销的步骤数量比实际的可撤销数量多,多余的部分没有任何效果)
cc.undo(100);//注意这里的实际可撤销步骤只有4步
AUTOCXX_ASSERT_EQUAL(c.size(),0);
// 重做多步操作
cc.redo(200);
AUTOCXX_ASSERT_EQUAL(c.size(),4);
// 撤销多步操作
cc.undo(3);
AUTOCXX_ASSERT_EQUAL(c.size(),1);
// 重做操作的步骤数量和先前撤销操作的数量不等
cc.redo(1);
AUTOCXX_ASSERT_EQUAL(c.size(),2);
// 再次全部撤销
cc.undo(100);
AUTOCXX_ASSERT_EQUAL(c.size(),0);
// 再次全部重做
cc.redo(100);
AUTOCXX_ASSERT_EQUAL(c.size(),4);
// 当出现连续进行撤销操作的时候
cc.undo(1);
AUTOCXX_ASSERT_EQUAL(c.size(),3);
cc.undo(1);
AUTOCXX_ASSERT_EQUAL(c.size(),2);
cc.undo(1);
AUTOCXX_ASSERT_EQUAL(c.size(),1);
cc.redo(3);
AUTOCXX_ASSERT_EQUAL(c.size(),4);
// 当出现进行撤销操作的时候又开始了新命令的时候,就会把先前已经存在的已
// 经撤销过的可重做操作清除也就是说这里存在一个不可撤销过程,实际上相当
// 于我们做事的时候,刚开始做错了,恢复原始状态之后马上进行重新选择的过
// 程,当然不需要记录之前我们做错过的事情:(
cc.undo(2);
AUTOCXX_ASSERT_EQUAL(c.size(),2);
// 这个时候又开始了一个新的命令
cc.execute(new Create(c,NETWORK));
AUTOCXX_ASSERT_EQUAL(c.size(),3);
// 在这种情况下考虑重做的过程
cc.redo(100);//实际上什么也没有做,因为之前缓存的可重做队列被清空了:(
AUTOCXX_ASSERT_EQUAL(c.size(),3);
下面是命令中心的扩展代码的模拟复合命令的测试用例:
typedef undo::basic_command<MyExtraData>Command;
typedef undo::basic_container<std::string>CT;
typedef undo::basic_create<CT,Command>Create;
typedef undo::basic_modify<CT,Command>Modify;
typedef undo::basic_remove<CT,Command>Remove;
CT c;// 容器对象,每个对象必须放在一个同类型的容器中
undo::basic_center<Command>cc;// 命令中心
CT::object_type* PANDAXCL = NULL;
CT::object_type*QQ = NULL;
CT::object_type*BLOG = NULL;
CT::object_type*EMAIL = NULL;
CT::object_type*NETWORK = NULL;
// 现在来考虑一下之前考虑过的batch命令在center中是如何表示的
AUTOCXX_ASSERT_EQUAL(c.size(),0);
cc.record();
cc.execute(new Create(c,PANDAXCL));
cc.execute(new Create(c,EMAIL));
cc.execute(new Create(c,QQ));
cc.execute(new Create(c,BLOG));
cc.execute(new Create(c,NETWORK));
cc.stop();
AUTOCXX_ASSERT_EQUAL(c.size(),5);
// 撤销一步,实际上就是把上面的三个子命令组成的一个batch命令撤销了
cc.undo();
AUTOCXX_ASSERT_EQUAL(c.size(),0);
// 重做一步,实际上就是把上面的三个子命令组成的一个batch命令重做了
cc.redo();
AUTOCXX_ASSERT_EQUAL(c.size(),5);
// 上面的记录过程其实还可以嵌套子记录过程,相当于前面的例子中的子batch命令一样
cc.record();
// 没有任何命令也是可以接受的
cc.stop();
下面是命令中心的扩展代码的模拟子复合命令的测试用例:
typedef undo::basic_command<MyExtraData>Command;
typedef undo::basic_container<std::string>CT;
typedef undo::basic_create<CT,Command>Create;
typedef undo::basic_modify<CT,Command>Modify;
typedef undo::basic_remove<CT,Command>Remove;
CT c;// 容器对象,每个对象必须放在一个同类型的容器中
undo::basic_center<Command>cc;// 命令中心
CT::object_type* PANDAXCL = NULL;
CT::object_type*QQ = NULL;
CT::object_type*BLOG = NULL;
CT::object_type*EMAIL = NULL;
CT::object_type*NETWORK = NULL;
// 现在来考虑一下之前考虑过的batch命令在center中是如何表示的
AUTOCXX_ASSERT_EQUAL(c.size(),0);
cc.record();
{
cc.execute(new Create(c,PANDAXCL));
// 上面的记录过程其实还可以嵌套子记录过程,相当于前面的例子中的子batch命令一样
cc.record();
{
cc.execute(new Create(c,EMAIL));
cc.record();
{
cc.execute(new Create(c,QQ));
cc.execute(new Create(c,BLOG));
cc.execute(new Create(c,NETWORK));
}
cc.stop();
}
cc.stop();
}
cc.stop();
AUTOCXX_ASSERT_EQUAL(c.size(),5);
// 撤销一步,实际上就是把上面的三个子命令组成的一个batch命令撤销了
cc.undo();
AUTOCXX_ASSERT_EQUAL(c.size(),0);
// 重做一步,实际上就是把上面的三个子命令组成的一个batch命令重做了
cc.redo();
AUTOCXX_ASSERT_EQUAL(c.size(),5);
// 由上述测试可以看出,只要center的record函数和stop函数是配对的那么就可以无限制的
// 进行嵌套!当然,如果一个batch命令只有一个子命令的话就没有必要啦:)
// 注意加深嵌套层级就意味着性能损失,虽然它数量少的时候微不足道,但是一旦数量庞大
// 之后就很明显啦:)
好了,到此为止已经成功的完成了命令的扩展,使用方法并没有多大不同:)可以放心使 用了:)