【答客問】C# 在 Word 找到圖案插入文字
3 |
讀者 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,我學到一些知識:
- 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。
- 修改前後差在 <wps:wsp> 下多了 <wps:txbx> 內含 <w:txbxContent>,裡面則是 <w:p> Paragraph、<w:Run>,這部分我就認識了;而 <v:shape> 下則是增加 <v:textbox>,裡面的 <w:txbxContent> 一樣包了 Paragraph、Run。
- <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 這也是好東西說