FreeCAD源码分析:Undo/Redo实现原理 – 源码巴士 |
您所在的位置:网站首页 › freecad源码解析 › FreeCAD源码分析:Undo/Redo实现原理 – 源码巴士 |
Undo/Redo是CAx软件中常见的操作功能,其实现方法也相对比较成熟,本文对FreeCAD Transaction机制进行深入分析,一方面是为了深化对FreeCAD代码的理解,学习其设计思路,领略其设计模式的使用范式;另一方面则考虑到Undo/Redo功能的普遍性,旨在阐述Undo/Redo的实现原理,希望对从事国产CAx软件开发的朋友有所帮助。 注1:限于笔者研究水平,难免有理解不当,欢迎批评指正。 注2:文章内容会不定期更新,欢迎交流讨论。 一、预修知识1.1 设计模式Undo/Redo经典实现是采用Command、Memento等设计模式。GoF、Alexander Shvets等已经就Command、Memento等相关设计模式进行了经典阐述,这里不再赘述,仅简要罗列其技术要点。 Command模式将请求封装成了对象,提供了命令响应的统一接口。 Command is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request’s execution, and support undoable operations. ![]() Memento模式在不违反封装的前提下,提供了对象状态记录与恢复的功能。 Memento is a behavioral design pattern that lets you save and restore the previous state of an object without revealing the details of its implementation. ![]() Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing. ![]() FreeCAD基于Observer模式,实现了App::Property类。按照GoF's Observer模式,Property作为Subject,而App::PropertyContainer则是Observer。 二、代码分析2.1 App::TransactionalObjectFreeCAD中,整套代码最为基础的类其实只有两个,一个是App::DocumentObject,另一个则是Gui::ViewProvider,而这两个类均派生于App::TransactionalObject。 App::TransactionalObject类比较简单,主要功能是采用Memento模式实现了对象属性快照的功能。 namespace App { class Document; class TransactionObject; /** Base class of transactional objects */ class AppExport TransactionalObject : public App::ExtensionContainer { PROPERTY_HEADER(App::TransactionalObject); public: /// Constructor TransactionalObject(void); virtual ~TransactionalObject(); virtual bool isAttachedToDocument() const; virtual const char* detachFromDocument(); protected: void onBeforeChangeProperty(Document *doc, const Property *prop); }; } //namespace App每当修改属性数据时,便会自动调用onBeforeChangeProperty()函数,而该函数的主要作用就是存储对象相关属性,以便后续执行Undo时,可以完成对象状态的恢复。 //Property.cpp void Property::aboutToSetValue(void) { if (father) father->onBeforeChange(this); } //Document.cpp void DocumentObject::onBeforeChange(const Property* prop) { // Store current name in oldLabel, to be able to easily retrieve old name of document object later // when renaming expressions. if (prop == &Label) oldLabel = Label.getStrValue(); if (_pDoc) onBeforeChangeProperty(_pDoc, prop); signalBeforeChange(*this,*prop); } //TransactionalObject.cpp void TransactionalObject::onBeforeChangeProperty(Document *doc, const Property *prop) { doc->onBeforeChangeProperty(this, prop); } //Document.cpp void Document::onBeforeChangeProperty(const TransactionalObject *Who, const Property *What) { if(Who->isDerivedFrom(App::DocumentObject::getClassTypeId())) signalBeforeChangeObject(*static_cast(Who), *What); if(!d->rollback && !_IsRelabeling) { _checkTransaction(0,What,__LINE__); if (d->activeUndoTransaction) d->activeUndoTransaction->addObjectChange(Who,What); } }2.2 App::TransactionObject由于一个Transaction通常由多个子操作构成,对应于子操作,App::TransactionObject则是用于记录对象创建、对象删除、属性修改等操作历史。 /** Represents an entry for an object in a Transaction */ class AppExport TransactionObject : public Base::Persistence { TYPESYSTEM_HEADER(); public: /// Construction TransactionObject(); /// Destruction virtual ~TransactionObject(); virtual void applyNew(Document &Doc, TransactionalObject *pcObj); virtual void applyDel(Document &Doc, TransactionalObject *pcObj); virtual void applyChn(Document &Doc, TransactionalObject *pcObj, bool Forward); void setProperty(const Property* pcProp); void addOrRemoveProperty(const Property* pcProp, bool add); virtual unsigned int getMemSize (void) const; virtual void Save (Base::Writer &writer) const; /// This method is used to restore properties from an XML document. virtual void Restore(Base::XMLReader &reader); friend class Transaction; protected: enum Status {New,Del,Chn} status; struct PropData : DynamicProperty::PropData { Base::Type propertyType; }; std::unordered_map _PropChangeMap; std::string _NameInDocument; };需要指出的是,App::TransactionObject通过枚举值New、Del来标记对象创建、对象删除,并记录对象名称;而将属性存储在App::TransactionObject::_PropChangeMap中。 2.3 App::TransactionApp::Transaction正是用于记录一次Transaction中对同一文档的修改,其中可能设计对多个对象的修改。 /** Represents a atomic transaction of the document */ class AppExport Transaction : public Base::Persistence { TYPESYSTEM_HEADER(); public: /** Construction * * @param id: transaction id. If zero, then it will be generated * automatically as a monotonically increasing index across the entire * application. User can pass in a transaction id to group multiple * transactions from different document, so that they can be undo/redo * together. */ Transaction(int id = 0); /// Construction virtual ~Transaction(); /// apply the content to the document void apply(Document &Doc,bool forward); // the utf-8 name of the transaction std::string Name; virtual unsigned int getMemSize (void) const; virtual void Save (Base::Writer &writer) const; /// This method is used to restore properties from an XML document. virtual void Restore(Base::XMLReader &reader); /// Return the transaction ID int getID(void) const; /// Generate a new unique transaction ID static int getNewID(void); static int getLastID(void); /// Returns true if the transaction list is empty; otherwise returns false. bool isEmpty() const; /// check if this object is used in a transaction bool hasObject(const TransactionalObject *Obj) const; void addOrRemoveProperty(TransactionalObject *Obj, const Property* pcProp, bool add); void addObjectNew(TransactionalObject *Obj); void addObjectDel(const TransactionalObject *Obj); void addObjectChange(const TransactionalObject *Obj, const Property *Prop); private: int transID; typedef std::pair Info; bmi::multi_index_container< Info, bmi::indexed_by< bmi::sequenced, bmi::hashed_unique< bmi::member > > > _Objects; };实际使用中,每个Document中都分别定义了自己的App::Transaction对象,因此记录的也就是对本文档对象的修改。 //Document.cpp int Document::_openTransaction(const char* name, int id) { if(isPerformingTransaction() || d->committing) { if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) FC_WARN("Cannot open transaction while transacting"); return 0; } if (d->iUndoMode) { if(id && mUndoMap.find(id)!=mUndoMap.end()) throw Base::RuntimeError("invalid transaction id"); if (d->activeUndoTransaction) _commitTransaction(true); _clearRedos(); d->activeUndoTransaction = new Transaction(id); if (!name) name = ""; d->activeUndoTransaction->Name = name; mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; id = d->activeUndoTransaction->getID(); signalOpenTransaction(*this, name); auto &app = GetApplication(); auto activeDoc = app.getActiveDocument(); if(activeDoc && activeDoc!=this && !activeDoc->hasPendingTransaction()) { std::string aname("-> "); aname += d->activeUndoTransaction->Name; FC_LOG("auto transaction " _openTransaction(aname.c_str(),id); } return id; } return 0; }2.4 App::Document按照GoF Memento模式,App::Document实际上扮演的是Caretaker的角色,App::Document提供了Undo/Redo堆栈, namespace App { class Document { ... private: // # Data Member of the document +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ std::list mUndoTransactions; std::map mUndoMap; std::list mRedoTransactions; std::map mRedoMap; // pointer to the python class Py::Object DocumentPythonObject; struct DocumentP* d; std::string oldLabel; std::string myName; }; }2.5 Gui::CommandGui::Command对应的就是GoF Command模式中的Command,提供了命令响应的接口,同时提供了Transaction创建的调用接口。 /// Open a new Undo transaction on the active document static void openCommand(const char* sName=0); /// Commit the Undo transaction on the active document static void commitCommand(void); /// Abort the Undo transaction on the active document static void abortCommand(void);参考资料Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides. Design Patterns:elements of reusable object-oriented software. Addison Wesley, 1994. Alexander Shvets. Dive into Design Patterns. |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |