讀者 Nathan 在使用 Open XML SDK 在 Word 插入圖片提問:如何在 Word 文件找到第一個 Shape 並插入文字?

好久沒玩 OpenXML,適逢假日剛好拿來暖身,維持手感。

這類 OpenXML 問題有個特性,只要能用 Word 做出來就一定有解! 因為有個無敵解題技巧 - 用 Word 將文件修改成想要的樣子,分別儲存修改前後的檔案,用壓縮軟體解壓取出 word\document.xml (如下圖),比較取得 XML 修改前後差異,再設法寫出程式透過 OpenXML 修改 XML 調成修改後版本就一定會成功。就算對 OpenXML 物件模型一無所知,當成純 XML 硬幹,只要能拼出 XML 最終相符,一樣可以解決問題。

拿到題目,不囉嗦,依照上述 SOP 破解。(其實是沒有其他招式了,噗) 先做一個簡單的 Word,在文件插入資料庫圖案存成 Exercise216A.docx,在資料庫圖案加上 MSSQL 文字再存檔 Exercise216B.docx。

比較修改前後的 document.xml,我學到一些知識:

  1. Word 文件裡圖案相關 XML 會出現兩次,<mc:AlternateContent> 下有 <mc:Choice Requires="wps"> 及 <mc:Fallback>,前者內含 <m:drawing> <wps:wsp>,後者則是 <w:pict> <v:shape>。由於 wps Namespace 是 http://schemas.microsoft.com/office/word/2010/wordprocessingShape 而 v Namespace 為 urn:schemas-microsoft-com:vml,猜想 WordprocessingShape 屬 Microsoft Word 特有規格,當第三方軟體不支援就 Fallback 改用公開規格 VML。
  2. 修改前後差在 <wps:wsp> 下多了 <wps:txbx> 內含 <w:txbxContent>,裡面則是 <w:p> Paragraph、<w:Run>,這部分我就認識了;而 <v:shape> 下則是增加 <v:textbox>,裡面的 <w:txbxContent> 一樣包了 Paragraph、Run。
  3. <v:shape> 有個 o:gfxdata Attribute 是一段 "UEsDBBQABgAI..." Base64 編碼,在加入文字編碼也會改變,查了文件,它是 XML Shape 的 EncodedPackage,相關文件說明很少,要涵蓋到連同 o:gfxdata 也更新頗具挑戰性。我把目標放在「修改後的文件 Microsoft Word 可以開啟」,這段先略過不處理。

經過一個多小時的奮鬥,終於成功了。範例程式如下:

using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using System;
using System.IO;
using System.Linq;

namespace InsertTextToShape
{
    class Program
    {
        static void Main(string[] args)
        {
            var workFileName = $"D:\\Test-{DateTime.Now:HHmmss}.docx";
            File.Copy("D:\\Exercise216A.docx", workFileName);
            using (WordprocessingDocument document =
                WordprocessingDocument.Open(workFileName, true))
            {
                var mainPart = document.MainDocumentPart;

                //找到第一個 Shape <wps:wsp>
                //ref: https://stackoverflow.com/a/7820288/288936
                var wsp = mainPart.Document.Body
                    .Descendants<DocumentFormat.OpenXml.Office2010.Word.DrawingShape.WordprocessingShape>()
                    .FirstOrDefault();

                if (wsp != null)
                {
                    var textBox = new DocumentFormat.OpenXml.Office2010.Word.DrawingShape.TextBoxInfo2();
                    var alignCenterParagraphProp = new ParagraphProperties(new Justification()
                    {
                        Val = JustificationValues.Center
                    });
                    textBox.Append(
                        new TextBoxContent(
                            new Paragraph(
                                alignCenterParagraphProp,
                                new Run(
                                    new Text("MSSQL")
                                )
                            )
                        )
                    );
                    //實測,<wps:txbx>要放在<wps:bodyPr>前方才會顯示
                    wsp.InsertBefore(textBox, wsp.LastChild);
                }
                
                //找第一個 VML Shape <v:shape>
                //註: 未處理 EncodedPackage,第三方軟體開啟可能有問題
                //此部分屬 Fallback,若 docx 只供 Word 開啟,可以不處理
                var shape =
                    mainPart.Document.Body
                    .Descendants<DocumentFormat.OpenXml.Vml.Shape>()
                    .FirstOrDefault();

                if (shape != null)
                {
                    var textBox = new DocumentFormat.OpenXml.Vml.TextBox();
                    var alignCenterParagraphProp = new ParagraphProperties(new Justification()
                    {
                        Val = JustificationValues.Center
                    });
                    textBox.Append(
                        new TextBoxContent(
                            new Paragraph(
                                alignCenterParagraphProp,
                                new Run(
                                    new Text("MSSQL")
                                )
                            )
                        )
                    );
                    shape.Append(textBox);
                }
                document.Save();
            }
        }
    }
}

這次多學到一個新技巧,Document.Body.Descendants<T>() 可以找出文件裡所有指定型別元素,比用 XML 節點名稱易讀好寫,但要怎麼知道 <wps:wsp> 是 DocumentFormat.OpenXml.Office2010.Word.DrawingShape.WordprocessingShape? 你為什麼不問問神奇海螺找找官方文件呢?不得不讚嘆微軟的開發文件整理得完整易查,對開發者超佛心~

補充:標準 docx 格式中 Shape 需同時提供 <wps:wsp> 及 <v:shape>,本程式範例未完整修改 <v:shape>,使用 Word 以外第三方軟體開啟時可能會有問題,Workaround 是用 Word 開啟程式調整過的文件,隨便改個地方再儲存,Word 即會更新 <v:shape> 內容。

Example of using OpenXML to insert text to shape in Word.


Comments

# by Nathan

實在太感激你快速而詳細的解答了!!!! 我就差在 //實測,<wps:txbx>要放在<wps:bodyPr>前方才會顯示 這一步, 結果卡了大半天orz, 總之真的太感激了!謝謝你!!

# by ByTIM

真特別的功能,希望有天能用到!

# by 凱大

https://www.microsoft.com/en-us/download/details.aspx?id=30425 這也是好東西說

Post a comment