作者: | 熊春雷 |
网站: | http://www.autodev.net |
Blog: | http://blog.csdn.net/pandaxcl |
EMail: | pandaxcl@163.com |
QQ: | 56637059 |
版本: | 0.01 于2007/09/25 |
目标: | 所有C++爱好者 |
版权: | 本文的版权归熊春雷所有 |
Warning
-
本文由熊春雷所写,绝对保证原创,在此特别严肃声明。
-
绝对不能容忍他人说本文为他所写以及其他的侵权行为。一旦发现,一定 尽本人最大的能力以法律的形式严追到底,决不妥协。
-
引用本文,要保证本文的完整性,不可以删除此处的声明,并且务必注明出处。
Tip
-
本文编写的所有代码可以用于任何用途(包括商业用途)。
-
用于商业用途的需要在最后发布的软件中声明借鉴了本文的思想。具体事 宜可以协商解决,(代码决不收取任何费用)。
-
其他事项可以和我联系,包括技术讨论等等:)或者直接登陆网站论坛: http://www.autodev.net
Note
-
本文受到了《C++设计新思维》和《产生式编程》两本书的影响,同时也查阅了大 量的资料,从Loki库和Boost库中也吸收了不少营养,特此感谢之。
-
本文由于处于原创阶段,难免会出现各种各样的错误。代码出现错误的可能性非常 小(本来想说为零的),因为文档和代码是严格同步的,这是由VST文本的include 所保证的,代码都是测试成功之后才发布的。
-
本文所编写的代码,经过了VC2005编译器和g++编译器的测试,并且都通过了。
-
本文还没有彻底完成,算是一个初级版本,未来还将继续完善。暂时发布出来是为 了预知读者群有多少,读者越多,我的成就感越强,写作的时候也会更有动力:)
-
本文还会继续完善,欢迎各位读者的批评指正,也接受各种各样的建议,在权衡之 后以决定是否加入本书。
-
本书还没有最终完成,还会不断的进行完善,更新之后的内容将会发表于我的 网站或我的博客。所以还需要读者多多关心本文的进展:)
Warning
-
本文的有效篇幅仅仅局限于基础篇,之后的文档还仅仅只是一种创意记录,
-
不保证正确性,特此提醒。
Contents
撤销和重做原理
首先讨论一下针对整型变量v的撤销和重做功能的原理实现:
int v = 5;// v的当前值
v = 8;//(1)现在需要将v的值修改成为8
v = 5;//(2)撤销将v的值修改成为8的操作
v = 8;//(3)重做将v的值修改成为8的操作
在上面的代码中可以看出:这是一个典型的修改操作的撤销和重做的原理实现, 其中:
-
步骤(1)中的8是修改参数
-
步骤(2)中的5是v在执行修改操作之前的原始数据
-
步骤(3)中的8是执行重做操作需要的修改参数备份,必须在执行修改操 作之前备份之
从而可以总结出实现撤销和重做所必须遵守的规范如下:
-
为了实现撤销操作必须在修改对象之前备份原始数据
-
为了实现重做操作必须在修改对象的时候备份修改参数
有了这两条规范就可以保证任何操作(到目前为止我还没有发现不能用这种方式实现 撤销和重做的操作)都可以通过这两条规则实现撤销和重做的能力了。
好了,到目前为止,有了实现撤销和重做方案的通用规则,并且这种规则的空间和时间效 率都非常好,我们该讨论一下三个基本操作和一个复合操作了。至于为 什么只有三种基本操作和一个复合操作,这是我在编写这个撤销和重做框架的过程中慢慢 积累起来的,至于严格的证明,恐怕不是我现在可以处理的了:(
这个三个基本操作是:
-
创建操作
-
修改操作
-
删除操作
一个复合操作是:
-
复合操作
一共是四个操作,其中复合操作可以是三个基本操作的组合,也可以是三个基本操作 和复合操作的任意组合,也就是说:复合操作里面还有子复合操作,这种嵌套可以达到任 意的层次。这里面的组合就是千变万化的了!
好了,说了这么多,是该看看具体的代码是如何实现的了。值得说明的是:为了代码 的正确性和可读性,在实现的过程中尽量避免非常复杂的C++指针操作问题,当然对于避 免不了的指针问题,也要尽可能的使其简单;取而代之的是尽量使用STL中的容器和算法 来实现需要的功能。
在上面的修改操作的原理实现中,我们首先就已经拥有了一个整型变量v,所以执行修改操 作的必要条件就是保证该变量已经存在(没有变量,如何修改啊!!!),但是这里 的变量v从分配到释放的过程没有考虑,因此下面来讨论分配和 释放两个过程对应的创建操作和删除操作:
int*ptr = new int(5);// 创建操作
delete ptr;ptr = NULL;// 删除操作
可以看出,对象的创建过程就是创建操作,而对象的释放过程就是删除操作 。在此其实还有另外一种考虑:如何表示存在和消失的概念?目前 我只能想到用另外一个对象来记录这种变化,也就是说:对象的存在和消失 的状态,对象自身是不可以保存的,必须通过另外的对象(这里采用的是整型指针) 来保存!而这另外一个对象来保存其他对象的存在和消失的状态,实际上也是一 种修改过程,只不过这种修改过程不需要实现撤销和重做功能,也就是说实现撤销和重做 能力需要借助于不需要撤销和重做的基础才能实现!这是不是很矛盾,其实这是普遍的规 律,量子信息的通信还是需要借助于传统通讯基础才能实现:)
事实上,除了用指针对象来表示对象的存在和消失状态外,也可以用STL容器来表示对象的 存在和消失状态,而后者就是在本文中采用的方案!当容器中存在对象的时候,可以从STL 容器中索引出相应的对象;当容器中没有指定索引的对象的时候,就表 示该对象已经被释放了!
Tip
-
事实上,只要能够表示存在和消失的概念的C++量都可以在这里作为
-
另外一个对象。
这里强调了索引,其实这里的索引包含了两层意思:
名字
-
STL容器中可以存放多个同类型对象,为了识别不同的对象,就需要为每个对象取一个
-
独一无二的名字,当然这里的独一无二仅仅只限于该STL容器范围
动作
-
为STL容器中的多个对象取独一无二名字的目的就是为了能够在修改的时候有提取指定
-
对象的能力
在编写GUI程序的过程中,经常用对话框来实现对一批对象的操作,而且这种批量操作的撤 销操作和重做操作也希望能够一步执行所有子操作的撤销和重做操作,否则行为就会极其 怪异。这种批量操作不同于上面的修改操作、创建操作以及删除操 作,而是独立于这三种基本操作之外的操作。简单点说就是一种能够组合基本操作和 批量操作的操作。这种操作就是前面所说的复合操作。
现在对前面的讨论进行总结如下:
修改操作
-
为了实现撤销操作必须在对对象修改之前保存原始信息备份
-
为了实现重做操作必须在修改对象的时候保存修改信息备份
创建操作 删除操作
-
必须通过对象(v)之外的其他对象(ptr)来反映该对象的存在和消失 状态
-
创建操作和删除操作互为逆操作,可以用来互相实现对方的撤销操作
-
本质上讲,创建操作和删除操作的撤销和重做操作实际上就是针对该对象之外的对 象(ptr)执行的修改操作,因此也必须要满足修改操作的信息备份要求
复合操作
-
必须能够组合前面讨论的修改操作、创建操作和删除操作 三种基本操作
-
当然复合操作也要能够组合其他的子复合操作
为了使得讨论更加形象,在这里特别提供一个对象类:
// 对象类
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)来实现的!容器类的状态变 化是不可以撤销和重做的,这也正是体现了前面所提到的新功能必须借助于现有的基础才 能实现的思想。此外用整数作为区别同类型对象的标识符!采用整数作为标识符是本框架 在最初的时候采用的方案。但是后来发现:采用指针作为标识符比整数作标识符更加 直接、方便且高效!这在后面的代码中将会直接反映出来:-)
复合操作没能实现的原因已经在代码里面说明了:-)