FreeCAD源码分析:Undo/Redo实现原理

Undo/RedoCAx软件中常见的操作功能,其实现方法也相对比较成熟,本文对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.

Command Pattern

Ref. from
REFACTORING GURU

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.

Memento Pattern

 Ref. from
REFACTORING GURU

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.

Observer Pattern

 Ref. from
 REFACTORING GURU

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.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片