net5 使用 Openxml 導出 Word


使用 OpenXML 導出 Word

一、前言

最近公司需要導出Word,前端那邊說實現不了那種花里胡哨的,樣式不支持,沒辦法,只能我上了。

公司秉持着節約的優秀品質,我嘗試了國內知名的開源插件,例如:Magicodes.IENPOI 都無法實現要求。
想使用 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 提供了四個類型。

分別是:

傳送門

  1. Document Word文檔
  2. MacroEnabledDocument Word Macro-Enabled Document (*.docm)
  3. MacroEnabledTemplate Word Macro-Enabled Template (*.dotm)
  4. 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 插入表格

段落解決了,接下來就是我們比較關心的表格和插入圖片了,代碼比較乏味,往下看。

比較麻煩的是每個單元格的內容都需要針對的處理。

下方是導出示例。

導出word

接下來我們需要實現的就是上圖中的樣子,代碼也比較長,我就不多說,有需要的等會可以在文章底部獲取Demo(有篩減)。

在處理表格之前,最好是把表格的單元格都先初始化出來,后面也比較好處理。

說一下比較關鍵的點:

  1. 橫向合並單元格

    HorizontalMerge 這個的作用主要是處理橫向合並的。給 HorizontalMerge 賦值時有 RestartContinue

    Restart 是告訴單元格,我要開始合並了,你注意着點。

    Continue 是告訴單元格,從上一個開始,開始合並

  2. 縱向合並單元格

    VerticalMerge 這個的作用主要是處理縱向合並的。和 HorizontalMerge 賦值一樣, VerticalMerge 賦值時有 RestartContinue

    他們兩的作用都是一樣的。

  3. 單元格插入圖片

    插入圖片有個注意點,設置圖片大小,控制圖片比例。我之前查找資料的時候看見的那個公式,一臉懵逼,怎么轉換???

    公式
    后面找到一個圖片像素尺寸轉換器,才將圖片的比例給計算出來。1英寸=2.54厘米

    下面用代碼來說明:

    initWidthinitHeight 的單位是 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);
    }
}
  1. 單元格高寬設置注意點

    在初始化表格的時候需要注意一個值:rowHeightType

    rowHeightType 是用於設置單元格高度類型的,如果想自定義高度,那么就設置 1,設置的單元格高度就生效了,缺點就是:如果賦值的時候字符串超出單元格內容,那么就會隱藏掉多余的部分。

    想自動高度的,那么就設置 0,不過那樣的話設置的高度就失效了,可以通過填充邊距來讓樣式好看點,能解決上述的缺點(兩者是相對的)。

    TableCellVerticalAlignment 是用於設置單元格內字體的對齊方向的,左對齊,右對齊,居中對齊。基本上大部分能用到的都可以了,其他花里胡哨的在自個琢磨去吧。

  2. 單元格內換行

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);
}

4、參考鏈接

blackwood-cliff

5、獲取Demo

github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM