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.
1.2 FreeCAD属性系统
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::TransactionalObject
FreeCAD中,整套代码最为基础的类其实只有两个,一个是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<const App::DocumentObject*>(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<const Property*, PropData> _PropChangeMap;
std::string _NameInDocument;
};
需要指出的是,App::TransactionObject通过枚举值New、Del来标记对象创建、对象删除,并记录对象名称;而将属性存储在App::TransactionObject::_PropChangeMap中。
2.3 App::Transaction
App::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<const TransactionalObject*, TransactionObject*> Info;
bmi::multi_index_container<
Info,
bmi::indexed_by<
bmi::sequenced<>,
bmi::hashed_unique<
bmi::member<Info, const TransactionalObject*, &Info::first>
>
>
> _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 = "<empty>";
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 " << getName() << " -> " << activeDoc->getName());
activeDoc->_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<Transaction*> mUndoTransactions;
std::map<int,Transaction*> mUndoMap;
std::list<Transaction*> mRedoTransactions;
std::map<int,Transaction*> mRedoMap;
// pointer to the python class
Py::Object DocumentPythonObject;
struct DocumentP* d;
std::string oldLabel;
std::string myName;
};
}
2.5 Gui::Command
Gui::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.
暂无评论内容