使用文档节点树

使用文档节点树

本指南展示了如何直接通过节点树导航和操作文档结构——即 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) 进行结构化、基于类型分发的遍历的基类

另请参阅

 中文