使用C# OpenXml实现Word多表格列表按表重启编号并保留格式
需求与问题
现有一份含多表格的Word文档,表格为两列结构,Column2包含超链接、截图、嵌套表格、项目符号列表等内容。需要编写基于DocumentFormat.OpenXml的C#代码,读取Column2内容并写入另一份Word文档的编号列表中,且每个表格的编号列表需重新启动。
当前实现存在矛盾:
- 实现编号列表时无法按表格重启编号
- 实现重启编号时单元格原有格式(如超链接、图片、嵌套表格等)丢失
以下是能实现重启编号但格式丢失的代码:
public static void createDoc(string Filename, string opfolder) { string b = DateTime.Now.ToString("ddMMyyyyhhmmsstt"); b = opfolder + b + ".docx"; byte[] byteArray = File.ReadAllBytes(Filename); using (MemoryStream memoryStream = new MemoryStream()) { memoryStream.Write(byteArray, 0, byteArray.Length); using (WordprocessingDocument indoc = WordprocessingDocument.Open(memoryStream, true)) using (WordprocessingDocument opdoc = WordprocessingDocument.Create(b, WordprocessingDocumentType.Document)) { MainDocumentPart inMainPart = indoc.MainDocumentPart; Body inbody = inMainPart.Document.Body; MainDocumentPart opMainPart = opdoc.AddMainDocumentPart(); opMainPart.Document = new Document(); Body outputBody = new Body(); IEnumerable<Table> tables = inbody.Descendants<Table>(); NumberingDefinitionsPart numberingpart = opMainPart.AddNewPart<NumberingDefinitionsPart>(); numberingpart.Numbering = new Numbering(); Numbering numbering = numberingpart.Numbering; int listindex = 1; int columnIndex = 1; foreach (Table table in tables) { var columnCells = table.Elements<TableRow>() .Select(row => row.Elements<TableCell>().ElementAtOrDefault(columnIndex)) .Where(cell => cell != null); foreach (var cell in columnCells) { if (cell.InnerText.ToLower().StartsWith("column2")) { continue; } Paragraph newParagraph = new Paragraph(); newParagraph.ParagraphProperties = new ParagraphProperties(new NumberingProperties( new NumberingLevelReference() { Val = 0 }, new NumberingId() { Val = listindex })); foreach (Paragraph p in cell.Descendants<Paragraph>()) { foreach (var run in p.Elements<Run>()) { Run newrun = (Run)run.CloneNode(true); newParagraph.Append(newrun); } outputBody.Append(newParagraph); } } listindex++; } opMainPart.Document.Body = outputBody; opMainPart.Document.Save(); } } }
解决方案:同时保留格式与按表格重启编号
核心改进点:
- 为每个表格创建独立的编号定义实例,确保编号重启
- 直接克隆单元格内的所有内容元素(而非仅Run),完整保留格式、超链接、图片、嵌套表格等
- 为段落添加编号属性时,合并原段落的格式属性
using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using System; using System.IO; using System.Linq; public static void CreateFormattedNumberedDoc(string inputPath, string outputFolder) { // 生成带时间戳的输出文件名 string outputFileName = Path.Combine(outputFolder, $"{DateTime.Now:ddMMyyyyhhmmsstt}.docx"); byte[] inputBytes = File.ReadAllBytes(inputPath); using (MemoryStream inputStream = new MemoryStream(inputBytes)) using (WordprocessingDocument inputDoc = WordprocessingDocument.Open(inputStream, true)) using (WordprocessingDocument outputDoc = WordprocessingDocument.Create(outputFileName, WordprocessingDocumentType.Document)) { MainDocumentPart inputMainPart = inputDoc.MainDocumentPart; Body inputBody = inputMainPart.Document.Body; // 初始化输出文档主部件 MainDocumentPart outputMainPart = outputDoc.AddMainDocumentPart(); outputMainPart.Document = new Document(); Body outputBody = new Body(); // 添加编号定义部件并配置基础编号样式 NumberingDefinitionsPart numberingPart = outputMainPart.AddNewPart<NumberingDefinitionsPart>(); Numbering numbering = new Numbering(); numberingPart.Numbering = numbering; // 定义通用的十进制编号样式(1, 2, 3...) AbstractNum abstractNum = new AbstractNum() { AbstractNumberId = 1 }; abstractNum.AppendChild(new MultiLevelType() { Val = MultiLevelValues.HybridMultilevel }); abstractNum.AppendChild(new Level() { LevelIndex = 0, StartNumberingValue = new StartNumberingValue() { Val = 1 }, NumberFormat = new NumberFormat() { Val = NumberFormatValues.Decimal }, LevelText = new LevelText() { Val = "%1." }, LevelJustification = new LevelJustification() { Val = LevelJustificationValues.Left }, ParagraphProperties = new ParagraphProperties(new Indentation() { Left = "720", Hanging = "360" }) }); numbering.AppendChild(abstractNum); int listIdCounter = 1; int columnIndex = 1; // Column2的索引 foreach (Table table in inputBody.Descendants<Table>()) { // 为当前表格创建独立的编号实例,确保编号从1重启 NumberingInstance numInstance = new NumberingInstance() { NumberID = listIdCounter }; numInstance.AppendChild(new AbstractNumId() { Val = 1 }); numbering.AppendChild(numInstance); // 获取当前表格的Column2单元格,跳过表头 var columnCells = table.Elements<TableRow>() .Select(row => row.Elements<TableCell>().ElementAtOrDefault(columnIndex)) .Where(cell => cell != null && !cell.InnerText.Trim().Equals("Column2", StringComparison.OrdinalIgnoreCase)); foreach (TableCell cell in columnCells) { // 遍历单元格内所有子元素(段落、嵌套表格、图片等) foreach (OpenXmlElement element in cell.ChildElements) { // 克隆原元素,完整保留格式 OpenXmlElement clonedElement = element.CloneNode(true); // 如果是段落,添加编号属性并保留原段落格式 if (clonedElement is Paragraph para) { ParagraphProperties paraProps = para.ParagraphProperties ?? new ParagraphProperties(); // 添加编号属性 paraProps.AppendChild(new NumberingProperties( new NumberingLevelReference() { Val = 0 }, new NumberingId() { Val = listIdCounter })); para.ParagraphProperties = paraProps; } outputBody.AppendChild(clonedElement); } } listIdCounter++; } outputMainPart.Document.Body = outputBody; outputMainPart.Document.Save(); } }
关键说明
- 编号重启:每个表格对应一个独立的
NumberingInstance,关联同一个基础编号样式,确保每个表格的编号从1开始计数。 - 格式完整保留:直接克隆单元格内的所有
OpenXmlElement,包括段落、嵌套表格、图片、超链接等元素,不丢失任何格式信息。 - 属性合并:为段落添加编号属性时,保留原段落的格式属性(如缩进、字体样式等),避免覆盖原有格式。
内容的提问来源于stack exchange,提问作者Bubbles
相关产品推荐
相关产品推荐

