因工作的需要实现一个功能,就是用C#编程实现OpenXML填充Word模板文档生成新文档,做这个的目的就是在交付软件产品给客户的时候,需要编写数据库的设计说明书,而数据库的设计说明书每个公司的有自己的格式,有自己的页眉设置等等,来看一看Word模板文档的目录结构吧。
在模板文档中,物理设计处放的是整个数据库里面所有的表的设计结构,(当然我是编写一个工作只导出选中的表才能生成,因为有些表不能放到设计文档中去。),在【物理设计】栏处,我预留了一些标记符,主要是方便通过编程的方式找到该段落,然后通过程序的方式替换掉内容即可,如预留的标记符为【$物理设计$】,如下图所示:
至于标记怎么留,看你喜好,随便整,无所谓,它就相当于一个占位符而已。因此我们需要做的准备工作就是准备好你的Word模板,然后设置好标记(占位符)即可。接下来进入主题。
OpenXML填充Word模板文档生成新文档
本文中的代码和工具的开发都是使用VS2019,开始之前需要通过 nuget 添加 DocumentFormat.OpenXml的引用。
代码中添加如下的命名空间:
using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing;
准备好了之后,我们就开始编写核心代码,先是将word模板文件给打开。
打开word模板
打开word模板直接调用WordprocessingDocument对象的open方法即可,实践中,最好是在内存流(MemoryStream)中打开,这样做的好处是不会破坏你原始的word模板文件,如果你调用的直接打开文件版本的那个open方法,当程序执行完毕,模板文件会自动保存,下次你使用时,程序会出错,因为丢失了你预留的标记。
byte[] byteArray = File.ReadAllBytes("template.docx"); using (var stream = new MemoryStream()) { stream.Write(byteArray, 0, byteArray.Length); using (WordprocessingDocument doc = WordprocessingDocument.Open(stream, true)) { } }
查找标记段落
在内存流中打开模板文件后,要查找我们标记的占位符段落,只需要从根元素的后代元素里面解析出包含我们设置的占位字符的段落(Paragraph)即可。如下代码所示:
var p = doc.MainDocumentPart.RootElement.Descendants<Paragraph>().FirstOrDefault(x => x.InnerText == "$物理设计$"); if (p != null) { var text = p.Descendants<Text>().FirstOrDefault(); text.Text = $"表标号: {exportList.First().Name}"; }
上面的代码将【$物理设计$】重新进行了替换。替换为了我们要导出的第一张表的名称。最终的效果图会放到后面。
插入新段落
真实的文档非常的复杂,需要插入新的段落,如果直接调用 Append方法,则会追加到文档末尾,非我想要的,来看下如何做的。
var tableName = p.Clone() as Paragraph; tableName.Descendants<Text>().FirstOrDefault().Text = $"表名: {exportList.First().TableDesc}"; doc.MainDocumentPart.Document.Body.InsertAfter(tableName, p);
插入新段落正是通过上面的第3行代码实现,它有2个参数,第一个是你要插入的word文档元素,不一定局限于段落,第2个参数是引用的word文档元素,表示插入在该元素的后面,InsertAfter命名已经说明了一切。
OpenXML生成word表格
表格才是本文的主体,下面的代码就是用OpenXML提供的API来创建一个Word表格。
var table = new Table(new TableProperties( new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct }, new TableBorders( new TopBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new BottomBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new LeftBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new RightBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new InsideHorizontalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new InsideVerticalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 } ) ));
上面只是一个空表格,还没有行和列,下面代码就是为表格添加行和列的内容。
TableRow tr = new TableRow(); TableCell tc1 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text($"字段标号")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc1); TableCell tc2 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text("字段名称")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc2); TableCell tc3 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text("字段类型")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc3); TableCell tc4 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text("字段长度")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc4); table.Append(tr);
OpenXML填充Word模板文档生成新文档
在实现导出功能时,通常会是多张表,在真实项目中,有上百张的表需要导出,最后依次将这些表插入到Word的占位标记处即可。
doc.MainDocumentPart.Document.Body.InsertAfter(table, 引用的word元素);
完整核心代码
byte[] byteArray = File.ReadAllBytes("template.docx"); using (var stream = new MemoryStream()) { stream.Write(byteArray, 0, byteArray.Length); using (WordprocessingDocument doc = WordprocessingDocument.Open(stream, true)) { var p = doc.MainDocumentPart.RootElement.Descendants<Paragraph>().FirstOrDefault(x => x.InnerText == "$物理设计$"); if (p != null) { var text = p.Descendants<Text>().FirstOrDefault(); text.Text = $"表标号: {exportList.First().Name}"; } var tableName = p.Clone() as Paragraph; tableName.Descendants<Text>().FirstOrDefault().Text = $"表名: {exportList.First().TableDesc}"; doc.MainDocumentPart.Document.Body.InsertAfter(tableName, p); Table t = null; for (int i = 0; i < exportList.Count(); i++) { var dbTable = exportList.ElementAt(i); var subP1 = p.Clone() as Paragraph; subP1.Descendants<Text>().FirstOrDefault().Text = $"表标号: {dbTable.Name}"; var subP2 = p.Clone() as Paragraph; subP2.Descendants<Text>().FirstOrDefault().Text = $"表名: {dbTable.TableDesc}"; if (i > 0) { doc.MainDocumentPart.Document.Body.InsertAfter(subP1, t); doc.MainDocumentPart.Document.Body.InsertAfter(subP2, subP1); } var table = new Table(new TableProperties( new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct }, new TableBorders( new TopBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new BottomBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new LeftBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new RightBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new InsideHorizontalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 }, new InsideVerticalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 1 } ) )); TableRow tr = new TableRow(); TableCell tc1 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text($"字段标号")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc1); TableCell tc2 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text("字段名称")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc2); TableCell tc3 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text("字段类型")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc3); TableCell tc4 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }, new Shading { Fill = "DBE5F1" }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text("字段长度")) { RunProperties = new RunProperties(new Bold(), new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc4); table.Append(tr); foreach (var item in dbTable.Fields) { tr = new TableRow(); tc1 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text(item.FieldName)) { RunProperties = new RunProperties(new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc1); tc1 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Left }), new Run(new Text(item.FieldDesc)) { RunProperties = new RunProperties(new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc1); tc1 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text(item.DbTypeChinese)) { RunProperties = new RunProperties(new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc1); tc1 = new TableCell( new TableCellProperties(new TableCellVerticalAlignment() { Val = TableVerticalAlignmentValues.Center }), new Paragraph(new ParagraphProperties(new Justification() { Val = JustificationValues.Center }), new Run(new Text(item.DBLength.Trim("()".ToCharArray()))) { RunProperties = new RunProperties(new FontSize { Val = "24" }, new RunFonts { Ascii = "宋体", HighAnsi = "宋体", EastAsia = "宋体" }) })); tr.Append(tc1); table.Append(tr); } if (i == 0) doc.MainDocumentPart.Document.Body.InsertAfter(table, tableName); else doc.MainDocumentPart.Document.Body.InsertAfter(table, subP2); t = table; } doc.SaveAs(saveFileDialog1.FileName).Dispose(); Alert("导出完毕,请手动更新该文档的目录,操作方式:选中目录->鼠标右键->更新域,保存即可!", callback: () => { System.Diagnostics.Process.Start(saveFileDialog1.FileName); }); } }
导出效果图
下图就是导出的word文档效果。有点缺陷就是文档的目录需要手动更新一下。当然你可以写一个word宏来实现打开word文档自动更新,但这些内容我就没有去研究了。
Openxml参考地址:https://docs.microsoft.com/en-us/office/open-xml/open-xml-sdk