TDocStd_Document 的事务和撤销/重做(Undo/Redo)机制_cpp document 事务
我们来深入探讨一下 TDocStd_Document
的事务和撤销/重做(Undo/Redo)机制。这套机制是 Open CASCADE (OCCT) 应用框架的核心,保证了数据操作的原子性和可恢复性。其核心思想类似于数据库的事务和版本控制系统(如 Git)。每一次提交的操作都被记录成一个“变更集”,你可以应用这个“变更集”的反向操作来撤销它。
## 核心组件
这套机制主要由以下几个关键类协同工作:
-
TDocStd_Document
: 扮演着管理器 (Manager) 的角色。它持有Undo
和Redo
的列表,并提供了OpenCommand
、CommitCommand
、AbortCommand
、Undo
、Redo
等公共接口来协调整个过程。 -
TDF_Data
: 这是文档的核心数据容器。所有的几何形状、属性、元数据等都以标签(TDF_Label
)和属性(TDF_Attribute
)的形式存储在这里。事务机制只关心对TDF_Data
的修改。 -
TDF_Transaction
: 这是一个临时的变更记录器。当你调用OpenCommand
时,文档会创建一个TDF_Transaction
并将其附加到TDF_Data
上。从此刻起,任何对TDF_Data
的修改(增、删、改标签或属性)都会被这个事务对象实时捕捉。 -
TDF_Delta
: 这是一个变更集 (Changeset)。当事务被提交(CommitCommand
)时,TDF_Transaction
会将它记录的所有变更打包成一个不可变的TDF_Delta
对象。这个对象包含了从事务开始到结束之间所有数据变化的完整信息。 -
TDF_DeltaList
: 这是一个TDF_Delta
对象的列表(实际上是堆栈)。TDocStd_Document
内部维护着两个这样的列表:myUndos
和myRedos
。
## 工作流程 ⚙️
下面我们通过一个完整的操作流程来理解这些组件是如何协作的。
1. 开启事务 (OpenCommand
)
doc->OpenCommand();
- 动作:
TDocStd_Document
创建一个内部的TDF_Transaction
对象 (myUndoTransaction
)。 - 目的: 告诉文档:“准备好,我要开始做一系列修改了,请开始记录。”
- 状态: 此时,任何对
TDF_Data
的操作都会被myUndoTransaction
记录下来。
2. 执行数据修改
// 假设这里执行了一些操作,比如创建一个形状并给它命名TDF_Label shapeLabel = ...;TNaming_Builder(shapeLabel).Generated(aBox);TDataStd_Name::Set(shapeLabel, \"MyBox\");
-
动作: 这些代码修改了
TDF_Data
,例如添加了新的标签和TNaming_NamedShape
、TDataStd_Name
属性。 -
后台:
myUndoTransaction
悄悄地记录了这些变更,比如:“在标签 X 添加了属性 Y”、“修改了属性 Z 的值”等。
3. 结束事务(两种可能)
路径 A:提交事务 (CommitCommand
) ✅
doc->CommitCommand();
TDocStd_Document
命令myUndoTransaction
将其记录的所有变更打包成一个TDF_Delta
对象。- 这个新创建的
TDF_Delta
被压入myUndos
堆栈的顶部。 - 清空
myRedos
堆栈。这是非常关键的一步,因为一个新的操作会使之前的“重做”历史失效(与大多数软件的逻辑一致)。 myUndoTransaction
被销毁,事务结束。
路径 B:中止事务 (AbortCommand
) ❌
doc->AbortCommand();
TDocStd_Document
命令myUndoTransaction
执行反向操作。myUndoTransaction
会遍历它记录的所有变更,并一个一个地撤销它们,从而将TDF_Data
恢复到OpenCommand
被调用前的原始状态。myUndos
和myRedos
堆栈保持不变。myUndoTransaction
被销毁,事务结束。
4. 撤销操作 (Undo
) ↩️
doc->Undo();
- 检查
myUndos
堆栈是否为空。如果为空,则什么也不做。 - 从
myUndos
堆栈顶部取出(Pop)最近的一个TDF_Delta
。 - 对
TDF_Data
应用这个TDF_Delta
的反向操作,使数据恢复到这个变更集被应用之前的状态。 - 将这个
TDF_Delta
压入myRedos
堆栈的顶部,以备将来重做。
5. 重做操作 (Redo
) ↪️
doc->Redo();
- 检查
myRedos
堆栈是否为空。如果为空,则什么也不做。 - 从
myRedos
堆栈顶部取出(Pop)最近的一个TDF_Delta
。 - 对
TDF_Data
正向应用这个TDF_Delta
,重新执行之前的操作。 - 将这个
TDF_Delta
压回myUndos
堆栈的顶部。
## 图解流程
下面是一个简化的图示,展示了 TDF_Delta
在 myUndos
和 myRedos
堆栈之间的移动:
初始状态:myUndos: []myRedos: []--> OpenCommand()--> (修改 A)--> CommitCommand()状态 1:myUndos: [Delta_A]myRedos: []--> OpenCommand()--> (修改 B)--> CommitCommand()状态 2:myUndos: [Delta_B, Delta_A]myRedos: []--> Undo()状态 3:myUndos: [Delta_A]myRedos: [Delta_B]--> Undo()状态 4:myUndos: []myRedos: [Delta_A, Delta_B]--> Redo()状态 5 (同状态 3):myUndos: [Delta_A]myRedos: [Delta_B]--> OpenCommand()--> (修改 C)--> CommitCommand()状态 6 (Redo历史被清空):myUndos: [Delta_C, Delta_A]myRedos: []
## 总结
-
原子性:
OpenCommand
和CommitCommand
AbortCommand
保证了一系列操作要么全部成功并记录,要么全部被撤销,使数据保持一致。 -
基于变更集: 整个机制不是简单地保存整个文档的快照,而是高效地记录和应用“变更集” (
TDF_Delta
)。 -
仅限 TDF 数据: 此机制只对
TDF_Data
内部的修改有效。任何外部副作用,如写入文件、更新UI、打印日志等,都不会被记录,也无法通过Undo
或AbortCommand
回滚。 -
堆栈模型:
Undo
和Redo
的实现是经典的数据结构——双堆栈模型,一个用于撤销,一个用于重做。