使用文档节点树
使用文档节点树
本指南展示了如何直接通过节点树导航和操作文档结构——即 Document、Section、Paragraph、Run、Table 以及所有其他文档元素所基于的对象模型。文档中的每个元素都派生自 Node,能够容纳子元素的每个元素则派生自 CompositeNode,因此了解这两个基类即可解释如何处理那些没有专用辅助 API 覆盖的内容。
Node 和 CompositeNode 基类
Node 是文档树中所有对象的抽象基类。它公开了 NodeType(一个枚举值,用于标识该节点的类型)、ParentNode(所在的 CompositeNode)、Document(所属文档)以及用于在同一级别节点之间移动的 PreviousSibling/NextSibling。Node.IsComposite 报告给定节点是否可以包含子节点。CompositeNode 为容器元素扩展了 Node——FirstChild、LastChild 和 Count 描述了其直接子节点,HasChildNodes 报告是否存在子节点。CompositeNode.GetChildNodes(nodeType, isDeep) 检索给定 NodeType 的所有子节点,可选择搜索完整子树(isDeep),而不仅仅是直接子节点;CompositeNode.GetChild(nodeType, index, isDeep) 按索引检索单个子节点。
NodeType 枚举
NodeType 列举了文档模型定义的所有节点类型——结构类型,如 Document、Section、Body、Paragraph、Table、Row 和 Cell;内联内容类型,如 Run、Shape 和 Comment;以及字段机械类型,如 FieldStart、FieldSeparator 和 FieldEnd,它们标记 Word 字段在运行流中的边界和切换文本。将 NodeType 值传递给 GetChildNodes 或 GetChild 可将搜索范围限定为恰好该类型的节点,或者传入 NodeType.Any 以匹配所有节点类型。
遍历和修改树
CompositeNode.GetEnumerator() 让你可以遍历节点的直接子节点,CompositeNode 还实现了 SelectNodes(xpath) 和 SelectSingleNode(xpath),用于在子树中进行基于 XPath 的查询。要修改树结构,AppendChild(newChild) 和 PrependChild(newChild) 在子节点的开头或结尾添加节点,InsertBefore(newChild, refChild) 和 InsertAfter(newChild, refChild) 相对于已有子节点插入,RemoveChild(oldChild) 或 RemoveAllChildren() 删除节点。CompositeNode.IndexOf(child) 返回子节点的位置。在 Node 本身上,Clone(isCloneChildren) 创建一个副本——可选地深拷贝——并且 Remove() 将节点从其父节点分离。Node.NextPreOrder(rootNode) 和 PreviousPreOrder(rootNode) 按文档顺序遍历树,受限于给定的根节点,这是在不使用递归代码的情况下逐节点扫描整个子树的常用方式。
节点集合与变更通知
NodeCollection 是一个具体的、可变的节点集合,支持 Add(node)、Insert(index, node)、Remove(node)、RemoveAt(index)、Clear()、Contains(node)、IndexOf(node) 和 ToArray(),并且拥有一个 Count 属性。NodeList 和 NodeEnumerator 支持对查询结果进行读取式迭代,例如由 SelectNodes 返回的节点。要观察结构变化的实时发生,请实现 INodeChangingCallback——其 NodeInserting(args)、NodeInserted(args)、NodeRemoving(args) 和 NodeRemoved(args) 方法各自接收一个描述受影响的 Node、其 OldParent/NewParent 以及发生的 NodeChangingAction 的 NodeChangingArgs。
使用 DocumentVisitor 访问节点
为了以结构化方式处理每种节点类型而无需手动遍历树,继承 DocumentVisitor 并覆盖你关心的节点类型对应的 Visit*Start/Visit*End 方法——如 VisitParagraphStart/VisitParagraphEnd、VisitTableStart/VisitTableEnd、VisitRun 等——然后在子树根节点上调用 Node.Accept(visitor) 以执行。这是导出、转换或检查文档的标准模式,无需为每种使用场景编写自定义递归遍历。
提示与最佳实践
- 当你需要获取所有后代节点(不论类型)时,使用
NodeType.Any与GetChildNodes(NodeType.Any, true),而不是针对每种类型单独调用GetChildNodes。 - 相比手写递归下降,建议使用
Node.NextPreOrder(rootNode)/PreviousPreOrder(rootNode)对子树进行平面、非递归的扫描。 - 在需要对许多不同节点类型执行相同处理时,使用
DocumentVisitor而不是临时的遍历树代码。 - 在处理未知类型的节点时,在转换为
CompositeNode之前先检查Node.IsComposite。 - 当需要对其他代码(包括内置操作)进行的插入或删除作出响应时,注册一个
INodeChangingCallback,而不仅仅是对自己代码直接产生的更改作出响应。
常见问题
| 问题 | 原因 | 修复 |
|---|---|---|
将 Node 强制转换为 CompositeNode 会抛出 invalid-cast 错误 | 该节点是叶子类型(例如 Run 或 FieldStart),无法容纳子节点 | 首先检查 Node.IsComposite,或使用 NodeType 在转换前确认该节点是容器类型 |
GetChildNodes 仅返回直接子节点 | isDeep 被保留为 false(或在重载默认浅层时省略) | 传递 isDeep: true 以搜索整个子树 |
| 在遍历集合的同时修改它会抛出错误 | 在 foreach 中对 NodeCollection 或 GetChildNodes 的结果进行了节点的添加或删除 | 先使用 ToArray() 将要修改的节点实体化,然后在遍历数组的同时对树进行变更 |
DocumentVisitor 子类未对某些内容被调用 | 访问者通过 Accept() 在不包含目标内容的节点上被调用,或者相应的 Visit* 方法未被覆盖 | 在包含内容的祖先节点上调用 Accept(),并覆盖与正在处理的节点类型相关的每一对 Visit*Start/Visit*End |
FAQ
Node 与 CompositeNode 有何区别?
Node 是所有文档元素的基类。CompositeNode 继承自 Node,用于可以包含子元素的节点(例如 Body、Paragraph 和 Table);叶子元素(例如 Run)直接继承自 Node,且不能拥有子元素。
我该如何在文档中查找所有特定类型的节点?
在根节点(通常是 Document 本身)上调用 CompositeNode.GetChildNodes(nodeType, isDeep),传入您想要的 NodeType 并使用 isDeep: true 来搜索整个子树,而不仅仅是直接子节点。
在遍历时,如何安全地删除节点?
首先将要删除的节点收集到一个数组中——例如使用 NodeCollection.ToArray()——然后遍历该数组,对每个节点调用 Remove(),而不是在 foreach 正在迭代时修改实时集合。
什么是 FieldStart、FieldSeparator 和 FieldEnd 节点?
它们是标记嵌入在运行流中的 Word 字段机制的 NodeType 值:FieldStart 和 FieldEnd 绑定字段,FieldSeparator 将字段的代码与当前显示的结果分隔开来。
如何处理文档中的每个段落、表格和 run?
对 DocumentVisitor 进行子类化,为您需要的节点类型重写 VisitParagraphStart/VisitParagraphEnd、VisitTableStart/VisitTableEnd 和 VisitRun,然后在 Document(或任何子树根)上调用 Accept(visitor) 来运行它。
API Reference 摘要
| 类/方法 | 描述 |
|---|---|
Node | 文档树中每个节点的抽象基类 |
CompositeNode | 能够包含子节点的节点的基类 |
CompositeNode.GetChildNodes(nodeType, isDeep) | 检索给定类型的所有子节点,可选择在整个子树范围内进行 |
CompositeNode.AppendChild(newChild) / InsertBefore(newChild, refChild) | 将节点添加到容器的末尾,或在已存在的子节点之前/之后 |
NodeType | 枚举,用于标识节点的类型(Paragraph、Table、Run、FieldStart,等等) |
NodeCollection | 具体的、可变的 Node 对象集合 |
Node.Clone(isCloneChildren) | 复制节点,可选择性地包括其子节点 |
Node.NextPreOrder(rootNode) / PreviousPreOrder(rootNode) | 按文档顺序遍历子树 |
INodeChangingCallback | 当节点被插入或删除时,回调接口会收到通知 |
DocumentVisitor | 通过 Node.Accept(visitor) 进行结构化、基于类型分发的遍历的基类 |