应用篇_撤销(Undo)和重做(Redo)的C++自动化实现(4)---将撤销和重做的基本架构模组化

作者: 熊春雷
网站: 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

将撤销和重做的基本架构模组化

虽然有了上面的命令基类、复合操作类就可以实现任意的撤销和重做功能的程序了 ,但是很明显,还需要编写大量的派生自命令基类的各种各样的操作类。那么有没 有办法减轻或者消除这种负担呢?

在前面讨论过程中,我们已经假定任何操作都可以采用三种基本操作和一个复合 操作来实现,那么撤销和重做的架构的自动化就要以这个假定为基础了。

在前面讨论三个基本操作的时候,我们知道对象的存在状态必须通过另外的对象来表现, 因此需要提供一个始终存在的对象来表示。在这里就采用和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编码所表示的信息,同时也可以表示无穷多的字符编 码,在后续的章节中就会给出一个特别定制的标识符类,以达到我们的要求。

实现QTreeWidget和QTableView支持撤销,需要用到Qt提供的QUndoStack类。QUndoStack是一个用于实现撤销操作的类,它可以在操作之前存储操作状态,并在需要时进行恢复。下面是一个简单的示例代码: ```cpp #include <QApplication> #include <QUndoStack> #include <QUndoCommand> #include <QTreeView> #include <QTableView> #include <QStandardItemModel> class TreeModelCommand: public QUndoCommand { public: TreeModelCommand(QTreeView* treeView, QStandardItemModel* model, int row, QStandardItem* item, QUndoCommand* parent = nullptr): QUndoCommand(parent), m_treeView(treeView), m_model(model), m_row(row), m_item(item) { } void undo() override { m_model->removeRow(m_row); m_treeView->setCurrentIndex(QModelIndex()); } void redo() override { m_model->insertRow(m_row, m_item); m_treeView->setCurrentIndex(m_model->index(m_row, 0)); } private: QTreeView* m_treeView; QStandardItemModel* m_model; int m_row; QStandardItem* m_item; }; class TableModelCommand: public QUndoCommand { public: TableModelCommand(QTableView* tableView, QStandardItemModel* model, int row, int column, const QVariant& data, QUndoCommand* parent = nullptr): QUndoCommand(parent), m_tableView(tableView), m_model(model), m_row(row), m_column(column), m_data(data) { } void undo() override { m_model->setData(m_model->index(m_row, m_column), m_oldData); m_tableView->setCurrentIndex(QModelIndex()); } void redo() override { m_oldData = m_model->data(m_model->index(m_row, m_column)); m_model->setData(m_model->index(m_row, m_column), m_data); m_tableView->setCurrentIndex(m_model->index(m_row, m_column)); } private: QTableView* m_tableView; QStandardItemModel* m_model; int m_row; int m_column; QVariant m_data; QVariant m_oldData; }; int main(int argc, char *argv[]) { QApplication a(argc, argv); QUndoStack undoStack; QTreeView treeView; QStandardItemModel treeModel; treeView.setModel(&treeModel); QTableView tableView; QStandardItemModel tableModel; tableView.setModel(&tableModel); QObject::connect(&treeModel, &QStandardItemModel::itemChanged, [&undoStack, &treeView, &treeModel](QStandardItem* item) { auto command = new TreeModelCommand(&treeView, &treeModel, item->row(), item); undoStack.push(command); }); QObject::connect(&tableModel, &QStandardItemModel::dataChanged, [&undoStack, &tableView, &tableModel](const QModelIndex& index, const QVariant& data, const QVariant& oldData) { auto command = new TableModelCommand(&tableView, &tableModel, index.row(), index.column(), data); undoStack.push(command); }); treeModel.appendRow(new QStandardItem("Item 1")); treeModel.appendRow(new QStandardItem("Item 2")); tableModel.setItem(0, 0, new QStandardItem("1")); tableModel.setItem(0, 1, new QStandardItem("One")); tableModel.setItem(1, 0, new QStandardItem("2")); tableModel.setItem(1, 1, new QStandardItem("Two")); treeView.show(); tableView.show(); return a.exec(); } ``` 在上面的示例代码中,我们定义了两个QUndoCommand子类分别用于实现QTreeWidget和QTableView的撤销操作。这两个类都继承自QUndoCommand类,并实现undo()和redo()方法,这两个方法分别用于撤销当前操作。在这两个类的构造函数中,我们保存了操作所需的数据以便于undo()和redo()方法的实现。 在main()函数中,我们创建了一个QUndoStack对象来存储所有操作,并将其与QTreeWidget和QTableView的数据模型的信号连接起来。当QStandardItemModel的itemChanged信号被触发时,我们创建一个TreeModelCommand对象并将其压入undoStack中。当QStandardItemModel的dataChanged信号被触发时,我们创建一个TableModelCommand对象并将其压入undoStack中。 最后,我们通过调用show()方法来显示QTreeWidget和QTableView,并通过调用QApplication的exec()方法启动应用程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值