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和CommitCommandAbortCommand保证了一系列操作要么全部成功并记录,要么全部被撤销,使数据保持一致。 -
基于变更集: 整个机制不是简单地保存整个文档的快照,而是高效地记录和应用“变更集” (
TDF_Delta)。 -
仅限 TDF 数据: 此机制只对
TDF_Data内部的修改有效。任何外部副作用,如写入文件、更新UI、打印日志等,都不会被记录,也无法通过Undo或AbortCommand回滚。 -
堆栈模型:
Undo和Redo的实现是经典的数据结构——双堆栈模型,一个用于撤销,一个用于重做。


