- A+
使用 OpenXML 导出 Word
一、前言
最近公司需要导出Word,前端那边说实现不了那种花里胡哨的,样式不支持,没办法,只能我上了。
公司秉持着节约的优秀品质,我尝试了国内知名的开源插件,例如:Magicodes.IE
,NPOI
都无法实现要求。
想使用 Microsoft.Office.Interop.Word
的,发现不支持 linux
,我吐了呀。
最后磨蹭着开始了研究 OpenXML
的旅途。
导出还是有些问题的,插入图片后
Word
无法打开,WPS
可以。有哪位道友有解决方案的,帮忙提供下,感激不尽。
二、前提条件
Nuget包:DocumentFormat.OpenXml version:2.13.1
开发工具:VSCode/VS2019
三、创建 Word
3.1、创建一个简单的空白Word
首先创建一个项目,下面创建了一个名为 ExportController
的控制器
引用 DocumentFormat.OpenXml.Wordprocessing
命名空间来方便我们操作 Word
。
WordprocessingDocument.Create
的作用是用于创建一个 Word
。官方提供了多个重载,感兴趣的可以去研究下。
我使用的 WordprocessingDocument Create(string path, WordprocessingDocumentType type)
,WordprocessingDocumentType
提供了四个类型。
分别是:
- Document
Word文档
- MacroEnabledDocument
Word Macro-Enabled Document (*.docm)
- MacroEnabledTemplate
Word Macro-Enabled Template (*.dotm)
- Template
Word 模板 (*.dotx)
执行下方代码后一个简单的 Word
就导出成功了
using DocumentFormat.OpenXml.Wordprocessing; private readonly string _rootPath; public ExportController(IWebHostEnvironment environment) { _rootPath = environment?.WebRootPath; } public async Task<IActionResult> ExportWord() { string savePath = Path.Combine(_rootPath, "Word"); if (!Directory.Exists(savePath)) Directory.CreateDirectory(savePath); string fileName = $"{Guid.NewGuid()}.docx"; using var doc = WordprocessingDocument.Create(Path.Combine(savePath, fileName), WordprocessingDocumentType.Document); // 新增主文档部分 var main = doc.AddMainDocumentPart(); main.Document = new Document(); // 新增主体 var docBody = main.Document.AppendChild(new Body()); // 设置主体页面大小及方向 // PageSize 的 Width 和 Height 的单位并不是厘米,而是 二十分之一磅 // 1 厘米≈28.35磅 A4 纸的大小是 21cm x 29.7cm // Orient 只要是用于设置页面是横向还是纵向 // Portrait 纵向 // Landscape 横向 docBody.AppendChild(new SectionProperties(new PageSize { Width = 11907U, Height = 16839U, Orient = PageOrientationValues.Portrait })); return Ok(Path.Combine(savePath, fileName)); }
3.2 新增段落
下面是一个新增段落的扩展方法。
Paragraph
代表一个段落,每新增一个段落,都有段落属性 ParagraphProperties
。
在段落属性中设置字体的样式。Justification
是用于设置字体方向的,例如:居左,居右,居中,两端等
我这里查找了两个常用的,其他的可以自行设置。
如果想在段落中设置文字,RUN
属性必不可少,需要在 Paragraph
中新增子节点,RunProperties
也是一样,是属于 RUN
的属性设置。
RunProperties
必须是 Run
的第一个子元素,也就是说需要先添加 RunProperties
,后添加 Text
FontSize
是以半磅为单位的,若要设置文字为 12 磅大小,需要设置 FontSize
为 24
例如设置字体的文字种类,字体大小,字体颜色,字体加粗等。
至于换行,我没有找到其他相关文档,用循环段落处理了。
使用起来也比较简单
docBody.SetParagraph("博物馆藏品清单", fontSize: "36");
/// <summary> /// 设置段落属性 /// </summary> /// <param name="docBody">文档主体</param> /// <param name="text">文档段落文本</param> /// <param name="font">字体类型</param> /// <param name="fontSize">字体大小:以半磅为单位</param> /// <param name="color">字体颜色</param> /// <param name="justification">字体对齐方向:(0:左侧;2:居中)</param> /// <param name="isWrap">是否换行</param> /// <param name="wrapNum">换行数量</param> /// <param name="isBold">是否加粗</param> public static void SetParagraph(this Body docBody, string text, string font = "宋体", string fontSize = "36", string color = "#000000", int justification = 2, bool isWrap= true, int wrapNum = 1, bool isBold = false) { // 新增段落 var para = docBody.AppendChild(new Paragraph()); // 段落属性 var paragraphProperties = para.AppendChild(new ParagraphProperties()); paragraphProperties.Justification = paragraphProperties.AppendChild(new Justification() { Val = (JustificationValues)justification }); var run = para.AppendChild(new Run()); var runProperties = run.AppendChild(new RunProperties()); run.AppendChild(new Text(text)); runProperties.AppendChild(new RunFonts() { Ascii = font, HighAnsi = font, EastAsia = font }); // 设置自动大小为18磅,以半磅为单位 runProperties.AppendChild(new FontSize() { Val = fontSize }); // 设置字体颜色 runProperties.AppendChild(new Color() { Val = color }); // 设置字体加粗 runProperties.AppendChild(new Bold() { Val = new OnOffValue() { Value = isBold } }); if (isWrap) docBody.Wrap(wrapNum, fontSize); } /// <summary> /// 换行 /// </summary> /// <param name="docBody"></param> /// <param name="wrapNum"></param> /// <param name="fontSize"></param> public static void Wrap(this Body docBody, int wrapNum = 1, string fontSize = "20") { for (int i = 0; i < wrapNum; i++) { var para = docBody.AppendChild(new Paragraph()); var run = para.AppendChild(new Run()); var runProperties = run.AppendChild(new RunProperties()); run.AppendChild(new Text("")); // 设置自动大小为18磅,以半磅为单位 runProperties.AppendChild(new FontSize() { Val = fontSize }); } }
3.3 插入表格
段落解决了,接下来就是我们比较关心的表格和插入图片了,代码比较乏味,往下看。
比较麻烦的是每个单元格的内容都需要针对的处理。
下方是导出示例。
接下来我们需要实现的就是上图中的样子,代码也比较长,我就不多说,有需要的等会可以在文章底部获取Demo(有筛减)。
在处理表格之前,最好是把表格的单元格都先初始化出来,后面也比较好处理。
说一下比较关键的点:
-
横向合并单元格
HorizontalMerge
这个的作用主要是处理横向合并的。给HorizontalMerge
赋值时有Restart
和Continue
。Restart
是告诉单元格,我要开始合并了,你注意着点。Continue
是告诉单元格,从上一个开始,开始合并 -
纵向合并单元格
VerticalMerge
这个的作用主要是处理纵向合并的。和HorizontalMerge
赋值一样,VerticalMerge
赋值时有Restart
和Continue
。他们两的作用都是一样的。
-
单元格插入图片
插入图片有个注意点,设置图片大小,控制图片比例。我之前查找资料的时候看见的那个公式,一脸懵逼,怎么转换???
后面找到一个图片像素尺寸转换器,才将图片的比例给计算出来。1英寸=2.54厘米
下面用代码来说明:
initWidth
和initHeight
的单位是cm
,对于Word
中来说。通过
Image
获取到图片的高度和宽度。resolution
是图片的分辨率。原本是想使用
image.HorizontalResolution
来获取图片分辨率,在windows
运行时万事正常,一部署到linux
上导出Word
图片样式就失效了。通过在本地模拟
linux
环境,调试发现image.HorizontalResolution
获取到的值是 0!!!所以干脆定死了,一把辛酸泪。满足了我设定在
Word
中设置的大小后,公式就是像素/分辨率*2.54=厘米
int initWidth = 16, initHeight = 20, resolution = 96; if (!string.IsNullOrWhiteSpace(picAddress)) { // 假装picAddress是一个图片地址,是有值的。 string picAddress = ""; if (!string.IsNullOrWhiteSpace(picAddress)) { string picType = picAddress.Split('.').Last().ToLower(); picType = picType == "jpg" ? "jpeg" : picType; ImagePart imagePart = null; if (Enum.TryParse(picType, true, out ImagePartType imagePartType)) { imagePart = main.AddImagePart(imagePartType); } var fs = System.IO.File.Open(picAddress, FileMode.Open); imagePart?.FeedData(fs); System.Drawing.Image image = System.Drawing.Image.FromStream(fs); double width = image.Width; double height = image.Height; while (Math.Round(width / resolution * 2.54, 1) > initWidth || Math.Round(height / resolution * 2.54, 1) > initHeight) { width /= 2; height /= 2; } Run run = new(); run.AddImageToBodyTableCell(main.GetIdOfPart(imagePart), (long)(width / resolution * 2.54 * 360000), (long)(height / resolution * 2.54 * 360000)); docBody.AppendChild(new Paragraph(run)); image.Dispose(); await fs.DisposeAsync(); docBody.Wrap(15); } }
-
单元格高宽设置注意点
在初始化表格的时候需要注意一个值:
rowHeightType
rowHeightType
是用于设置单元格高度类型的,如果想自定义高度,那么就设置 1,设置的单元格高度就生效了,缺点就是:如果赋值的时候字符串超出单元格内容,那么就会隐藏掉多余的部分。想自动高度的,那么就设置 0,不过那样的话设置的高度就失效了,可以通过填充边距来让样式好看点,能解决上述的缺点(两者是相对的)。
TableCellVerticalAlignment
是用于设置单元格内字体的对齐方向的,左对齐,右对齐,居中对齐。基本上大部分能用到的都可以了,其他花里胡哨的在自个琢磨去吧。 -
单元格内换行
public static void CellWrap(this Run run, int wrapNum = 1) { for (int i = 0; i < wrapNum; i++) { run.AppendChild(new Break()); } }
/// <summary> /// 初始化表格 /// </summary> /// <param name="table"></param> /// <param name="row"></param> /// <param name="col"></param> /// <param name="rowHeightType">0:自动高度(设置高度无效,需在设置上下左右间距);1:自定义高度(设置此模式后内容超出会被隐藏);</param> /// <param name="borderColor">4F81BD</param> /// <param name="tableWidth"></param> /// <param name="rowHeight"></param> /// <param name="leftMargion"></param> /// <param name="rightMargin"></param> /// <param name="topMargion"></param> /// <param name="bottomMargion"></param> /// <param name="cellAlignmentMethod">单元格对齐方式(0:上对齐 1:居中 2:下对齐)</param> public static void InitTable(this Table table, int row, int col, int rowHeightType = 0, string borderColor = "000000", string tableWidth = "5000", uint rowHeight = 600,string leftMargion = "100", string rightMargin = "100", string topMargion = "0", string bottomMargion = "0", int cellAlignmentMethod = 1) { // 设置表格边框 var tabProps = table.AppendChild(new TableProperties() { TableBorders = new TableBorders() { TopBorder = new TopBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4, Color = borderColor }, BottomBorder = new BottomBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4, Color = borderColor }, LeftBorder = new LeftBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4, Color = borderColor }, RightBorder = new RightBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4, Color = borderColor }, InsideHorizontalBorder = new InsideHorizontalBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4, Color = borderColor }, InsideVerticalBorder = new InsideVerticalBorder { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 4, Color = borderColor } }, }); tabProps.AppendChild(new TableWidth { Width = tableWidth, Type = TableWidthUnitValues.Pct }); // 初始化表格 string width = (tableWidth.ObjToInt() / (double)col).ToString("0.0"); for (int i = 0; i < row; i++) { TableRow tabRow = table.AppendChild(new TableRow()); tabRow.AppendChild(new TableRowProperties(new TableRowHeight { Val = UInt32Value.FromUInt32(rowHeight), HeightType = (HeightRuleValues)rowHeightType })); for (int j = 0; j < col; j++) { TableCell tableCell = tabRow.AppendChild(new TableCell()); var cellPara = tableCell.AppendChild(new Paragraph()); cellPara.AppendChild(new ParagraphProperties()); var tableCellProps = tableCell.AppendChild(new TableCellProperties()); // 设置单元格字体水平居中,垂直居中 tableCellProps.AppendChild(new TableCellVerticalAlignment { Val = (TableVerticalAlignmentValues)cellAlignmentMethod }); tableCellProps.AppendChild(new Justification { Val = JustificationValues.Center }); // 单元格宽度 tableCellProps.AppendChild(new TableCellWidth { Width = width, Type = TableWidthUnitValues.Pct }); // 设置单元格上下左右间距 tableCellProps.AppendChild(new TableCellMargin() { LeftMargin = new LeftMargin { Width = leftMargion, Type = TableWidthUnitValues.Dxa }, RightMargin = new RightMargin { Width = rightMargin, Type = TableWidthUnitValues.Dxa }, TopMargin = new TopMargin { Width = topMargion, Type = TableWidthUnitValues.Dxa }, BottomMargin = new BottomMargin { Width = bottomMargion, Type = TableWidthUnitValues.Dxa } }); } } } /// <summary> /// 插入图片px/37.8*36=cm /// </summary> /// <param name="run"></param> /// <param name="relationshipId"></param> /// <param name="width">图片宽度</param> /// <param name="height">高度</param> public static void AddImageToBodyTableCell(this Run run, string relationshipId, long width = 990000L, long height = 792000L) { var element = new Drawing( new Inline( //new Extent() { Cx = 990000L, Cy = 792000L }, // 调节图片大小 new Extent() { Cx = width, Cy = height }, // 调节图片大小 new SimplePosition() { X = 0, Y = 0 }, new VerticalPosition() { RelativeFrom = VerticalRelativePositionValues.Paragraph }, new HorizontalPosition() { RelativeFrom = HorizontalRelativePositionValues.InsideMargin, PositionOffset = new PositionOffset("36") }, new EffectExtent() { LeftEdge = 0L, TopEdge = 0L, RightEdge = 0L, BottomEdge = 0L }, new DocProperties() { Id = 1U, Name = "Picture 1" }, new NonVisualGraphicFrameDrawingProperties( new A.GraphicFrameLocks() { NoChangeAspect = true }), new A.Graphic( new A.GraphicData( new PIC.Picture( new PIC.NonVisualPictureProperties( new PIC.NonVisualDrawingProperties() { Id = 0U, Name = "New Bitmap Image.jpg" }, new PIC.NonVisualPictureDrawingProperties()), new PIC.BlipFill( new A.Blip( new A.BlipExtensionList( new A.BlipExtension() { Uri = "{28A0092B-C50C-407E-A947-70E740481C1C}" }) ) { Embed = relationshipId, CompressionState = A.BlipCompressionValues.Print }, new A.Stretch( new A.FillRectangle())), new PIC.ShapeProperties( new A.Transform2D( new A.Offset() { X = 0L, Y = 0L }, new A.Extents() { Cx = width, Cy = height }), //与上面的对准 new A.PresetGeometry( new A.AdjustValueList() ) { Preset = A.ShapeTypeValues.Rectangle })) ) { Uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" }) ) { DistanceFromTop = 0U, DistanceFromBottom = 0U, DistanceFromLeft = 0U, DistanceFromRight = 0U, EditId = "50D07946" }); run.AppendChild(element); }
3.4 插入分页符
/// <summary> /// 创建分节符 /// </summary> /// <params>width</params> /// <params>height</params> /// <params>orient:1 横向;0:纵向</params> /// <returns></returns> public static void CreatePortraitSectionBreakParagraph(this Body docBody, uint width = 11906U, uint height = 16838U, int orient = 0) { Paragraph paragraph1 = new() { RsidParagraphAddition = "00052B73", RsidRunAdditionDefault = "00052B73" }; ParagraphProperties paragraphProperties1 = new(); SectionProperties sectionProperties1 = new() { RsidR = "00052B73" }; sectionProperties1.AppendChild(new PageSize() { Width = width, Height = height, Orient = (PageOrientationValues)orient }); sectionProperties1.AppendChild(new Columns() { Space = "425" }); sectionProperties1.AppendChild(new DocGrid() { Type = DocGridValues.Lines, LinePitch = 312 }); paragraphProperties1.Append(sectionProperties1); paragraph1.Append(paragraphProperties1); docBody.AddChild(paragraph1); }