原文:http://blog.csdn.net/francislaw/article/details/7568317
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
一、OpenXml簡介
利用C#生成Word文檔並非一定要利用OpenXml技術,至少可以使用微軟提供的Office相關組件來編程,不過對於Office2007(確切的說是Word、Excel和PowerPoint2007)及以上版本,微軟提供了這些信息組織的另外一種思路:OpenXml技術。
OpenXml是微軟office2007及之后版本里,對Office信息內容(Word、Excel和PowerPoint)的一種組織方式,當你創建一個Word2007文檔:XXX.docx后,它實際上是一組符合一定規范的格式化xml文件,如果你把后綴名更改為XXX.zip,或者,直接用7-zip類似的解壓軟件解壓后,你會得到一系列的xml文件。這些xml文件有用來描述類容的,有用來描述形式的,還有用來自我描述(描述XXX.docx這一組文檔本身的)。撇開其他一切,這樣的一系列xml文件來實現一個Word文檔(Excel和PowerPoint都很相似)至少可以實現兩點優點:內容和表現形式的分離,文檔的自我描述。這不管對於信息內容、表現形式的管理,還是對於文檔的擴展性,都提供了比較大的便利。Ok,優點啥的,那是微軟的事,我們還是比較關注OpenXml對於我們一般開發者有何便利,尤其對於我們通過編程去操作Word文檔(以下皆以Word為例,其他與此思路一致)。
二、OpenXmlSDK的由來
其實,最容易想到的思路就是:既然Word文檔時一系列的格式化的xml文件,那我直接按照這些xml文件的規范,去通過C#寫xml文件,按照OpenXml的標准去生成節點,節點名、節點屬性、值、內容等啥的。先不說這些xml文件格式如何苛刻,內容有多繁瑣,我們創建這些xml文檔有啥用呢?這樣不過是生成了一些xml文件,難道把這些文件的父文件夾后綴更改為.docx,就自動變成word文檔了嗎?
當然不會。。這些xml文件並非是孤立的。當用Office工具創建文檔時,它會自動生成相應的xml文件,並組織成一個包(即正如我們所看到的一個.docx文件。)。對於我們用編程來實現時,肯定是先去生成某個東西A,對應於所有xml文件所組成的包,然后利用A,去生成A里的各種內部部件,即對應於包里的各個xml文件。如此以來,我們所生成的xml文件才會相互關聯起來,且最終確實可以組合成一個Word文檔。
既然都想到先去創建一個A對應於文件包,創建A里的內部部件去對應xml文件。對應、對應,何必要我們自己去生成xml文件呢?畢竟我們想要的是生成word文檔,生成word文檔里的段落、文本、樣式、表格等等。所以,在我們編程時,如果可以直接像一般處理那樣,直接去生成我們所創建的東西,比如一個段落P,而不用關心實際的xml文件表示這個P需要怎樣的結構,需要哪些屬性等等,甚至不用關系我們要生成哪些xml文件,這些xml文件如何組織等等。對我們編程而言,我們只是通過某些C#類、方法,生成了一些文檔、一些段落、一些樣式,而這些東西如何轉變成OpenXml文件,轉換成怎么樣的Xml文件,這些如果都能自動完成,我們豈不是要輕松很多。
沒錯,OpenXmlSDK就是來完成這樣的工作:它為我們提供了一系列的API,通過這些API,我們可以直接去描述我們想要生成的東西:文檔、段落、表格、樣式等等。這些API根據OpenXml的標准,去生成對應的xml文件,並組織對應的xml文件,最終生成以OpenXml的形式組織內容的Word文檔。
單純從開發引用類庫而言,OpenXMLSDKv2.msi這個3.8M的文件足矣,安裝它之后,把相應的dll文件添加到項目中即可。但如果你想更輕松的開發,比如,想看看一個Word文檔轉換為怎樣的xml文件了,甚至想把這些OpenXml轉換為對應的C#代碼,OpenXMLSDKTool.msi這個106M的工具就很有必要了。反正現在網速這么快,100多M算啥呢?何況,當你實際開發使用后,你會發現OpenXMLSDKTool.msi對你太關鍵了。
三、編程操作OpenXml文件
如果再不來點代碼示例啥的,我想肯定有人覺得上當受騙了,看着這個標題點進來,結果發現啥代碼都沒有,看了一點似懂非懂的文字。Ok,接下來,我們就一起來創建一個名為Test.docx的Word文檔。這個Word文檔包含了Word里常用的一部分內容:段落、表格、樣式、標題等。如果你需要生成更多的內容,文章的第四部分會介紹個方法。
(1)准備環境:下載並安裝OpenXMLSDKv2.msi和OpenXMLSDKTool.msi,創建OpenXmlTest的ConsoleApplication。
添加如下引用:
C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\WindowsBase.dll
C:\Program Files\Open XML SDK\V2.0\lib\DocumentFormat.OpenXml.dll
(2)創建文檔:
在Program.cs文件中編輯如下:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- <strong>using DocumentFormat.OpenXml;
- using DocumentFormat.OpenXml.Wordprocessing;
- using DocumentFormat.OpenXml.Packaging;</strong>
- namespace OpenXmlTest
- {
- class Program
- {
- static void Main(string[] args)
- {
- // ①:創建WordprocessingDocument實例doc,對應於TEST.docx文件
- using (WordprocessingDocument doc=WordprocessingDocument.Create(@"D:\Test.docx",WordprocessingDocumentType.Document))
- {
- // ②:為doc添加MainDocumentPart部分
- MainDocumentPart mainPart = doc.AddMainDocumentPart();
- // ③:為mainPart添加Document,對應於Word里的文檔內容部分
- mainPart.Document = new Document();
- // ④:為Document添加Body,之后所有於內容相關的均在此body中
- Body body=mainPart.Document.AppendChild(new Body());
- // ⑤:添加段落P,P中包含一個文本“TEST”
- Paragraph p=mainPart.Document.Body.AppendChild(new Paragraph());
- p.AppendChild(new Run(new Text("TEST")));
- }
- }
- }
- }
先總結下Word文檔中的關鍵元素:
WordprocessingML 元素 |
Open XML SDK 2.0 類 |
p(段落) |
Paragraph |
pPr(段落屬性) |
ParagraphProperties |
r |
Run |
t(文本) |
Text |
(3)向文檔中添加表格
- // 新建Table
- Table tb = new Table();
- // 新建行
- TableRow row = new TableRow();
- // 新建、單元格
- TableCell cel = new TableCell(new Paragraph(new Run(new Text("TableCell1"))));
- // 關聯行、單元格和表格
- row.AppendChild(cel);
- tb.AppendChild(row);
- // 把表格添加到文檔中
- body.Append(tb);
(4)向文檔添加樣式。
接下來開始討論比較蛋疼的問題:樣式,通常我們生成文檔只要關注於內容就行了,樣式、格式啥的,愛美的人自己打開文檔自己通過Office去設置吧。(玩笑,樣式啥的確實蛋疼,但同時也很重要)尤其對於生成格式化的文檔,通常都是上百甚至上千頁的內容,如果每一項都自己去設置格式,那做這樣文檔生成工具的初衷根本未達到。
首先要明白一個問題:樣式這些東西是OpenXml文檔自身所包含的信息,而不是Office工具比如Word2007所提供的。也就是說,當OpenXml文件本身包含有樣式相關的信息時,通過編程設置文本的字體、顏色、樣式啥的才有意義。不要幻想着Office工具本身帶的有樣式,所以我只要在編程創建文檔時設置好對應的屬性,比如字體=“宋體”,樣式=“Head 1”就能達到預期,前提是你確保你的OpenXml文檔里確實包含對"宋體"、“Head 1”這些樣式相關的定義。
最簡單的實現方式是:自己手動通過Word創建一個空白的Word文檔,此時它會自動包含包括常用樣式、格式、字體等的xml文件。然后,在編程控制內容時,設置好內容對應的屬性。當然,這種思路對於常用的樣式或者格式有用,但“常用”如何定義,我不確定,唯一確定的是對於手動創建的Word文檔里所使用過的樣式、字體等定義,對應的OpenXml文檔一定包含有對應的定義。
第二中思路則比較蛋疼了:自己通過編程去創建自己需要的樣式,然后在內容輸出時,設置對應的屬性。沒辦法,如果你想要程序更加智能,只需要一個按鈕就能生成所有你需要的文檔,那你的代碼所做的工作就更多。
樣式千千萬萬,但通過編程的思路都很一致,不過是:創建樣式,向文檔添加所創建的樣式,在內容輸出部分使用所創建的樣式。結合到后面會講到的偷懶的方法,以下僅以Heading1,和Heading2為例介紹,
- ①:先創建CreateParagraphStyle方法,為文檔創建並添加樣式
- // 為文檔創建段落樣式
- static void CreateParagraphStyle(WordprocessingDocument doc)
- {
- // 進入文檔控制樣式部分
- StyleDefinitionsPart styleDefinitionsPart;
- styleDefinitionsPart = doc.MainDocumentPart.AddNewPart<StyleDefinitionsPart>();
- Styles root = new Styles();
- root.Save(styleDefinitionsPart);
- Styles styles = styleDefinitionsPart.Styles;
- if (styles == null)
- {
- styleDefinitionsPart.Styles = new Styles();
- styleDefinitionsPart.Styles.Save();
- }
- // 創建樣式Heading1
- Style style2 = new Style() { Type = StyleValues.Paragraph, StyleId = "1" };
- StyleName styleName2 = new StyleName() { Val = "heading 1" };
- BasedOn basedOn1 = new BasedOn() { Val = "a" };
- NextParagraphStyle nextParagraphStyle1 = new NextParagraphStyle() { Val = "a" };
- LinkedStyle linkedStyle1 = new LinkedStyle() { Val = "1Char" };
- UIPriority uIPriority1 = new UIPriority() { Val = 9 };
- PrimaryStyle primaryStyle2 = new PrimaryStyle();
- Rsid rsid2 = new Rsid() { Val = "00B74129" };
- StyleParagraphProperties styleParagraphProperties2 = new StyleParagraphProperties();
- KeepNext keepNext1 = new KeepNext();
- KeepLines keepLines1 = new KeepLines();
- SpacingBetweenLines spacingBetweenLines1 = new SpacingBetweenLines() { Before = "340", After = "330", Line = "578", LineRule = LineSpacingRuleValues.Auto };
- OutlineLevel outlineLevel1 = new OutlineLevel() { Val = 0 };
- styleParagraphProperties2.Append(keepNext1);
- styleParagraphProperties2.Append(keepLines1);
- styleParagraphProperties2.Append(spacingBetweenLines1);
- styleParagraphProperties2.Append(outlineLevel1);
- StyleRunProperties styleRunProperties1 = new StyleRunProperties();
- Bold bold1 = new Bold();
- BoldComplexScript boldComplexScript1 = new BoldComplexScript();
- Kern kern2 = new Kern() { Val = (UInt32)44U };
- FontSize fontSize2 = new FontSize() { Val = "44" };
- FontSizeComplexScript fontSizeComplexScript2 = new FontSizeComplexScript() { Val = "44" };
- styleRunProperties1.Append(bold1);
- styleRunProperties1.Append(boldComplexScript1);
- styleRunProperties1.Append(kern2);
- styleRunProperties1.Append(fontSize2);
- styleRunProperties1.Append(fontSizeComplexScript2);
- style2.Append(styleName2);
- style2.Append(basedOn1);
- style2.Append(nextParagraphStyle1);
- style2.Append(linkedStyle1);
- style2.Append(uIPriority1);
- style2.Append(primaryStyle2);
- style2.Append(rsid2);
- style2.Append(styleParagraphProperties2);
- style2.Append(styleRunProperties1);
- // 創建樣式heading2
- Style style3 = new Style() { Type = StyleValues.Paragraph, StyleId = "2" };
- StyleName styleName3 = new StyleName() { Val = "heading 2" };
- BasedOn basedOn2 = new BasedOn() { Val = "a" };
- NextParagraphStyle nextParagraphStyle2 = new NextParagraphStyle() { Val = "a" };
- LinkedStyle linkedStyle2 = new LinkedStyle() { Val = "2Char" };
- UIPriority uIPriority2 = new UIPriority() { Val = 9 };
- UnhideWhenUsed unhideWhenUsed1 = new UnhideWhenUsed();
- PrimaryStyle primaryStyle3 = new PrimaryStyle();
- Rsid rsid3 = new Rsid() { Val = "00B74129" };
- StyleParagraphProperties styleParagraphProperties3 = new StyleParagraphProperties();
- KeepNext keepNext2 = new KeepNext();
- KeepLines keepLines2 = new KeepLines();
- SpacingBetweenLines spacingBetweenLines2 = new SpacingBetweenLines() { Before = "260", After = "260", Line = "416", LineRule = LineSpacingRuleValues.Auto };
- OutlineLevel outlineLevel2 = new OutlineLevel() { Val = 1 };
- styleParagraphProperties3.Append(keepNext2);
- styleParagraphProperties3.Append(keepLines2);
- styleParagraphProperties3.Append(spacingBetweenLines2);
- styleParagraphProperties3.Append(outlineLevel2);
- StyleRunProperties styleRunProperties2 = new StyleRunProperties();
- RunFonts runFonts2 = new RunFonts() { AsciiTheme = ThemeFontValues.MajorHighAnsi, HighAnsiTheme = ThemeFontValues.MajorHighAnsi, EastAsiaTheme = ThemeFontValues.MajorEastAsia, ComplexScriptTheme = ThemeFontValues.MajorBidi };
- Bold bold2 = new Bold();
- BoldComplexScript boldComplexScript2 = new BoldComplexScript();
- FontSize fontSize3 = new FontSize() { Val = "32" };
- FontSizeComplexScript fontSizeComplexScript3 = new FontSizeComplexScript() { Val = "32" };
- styleRunProperties2.Append(runFonts2);
- styleRunProperties2.Append(bold2);
- styleRunProperties2.Append(boldComplexScript2);
- styleRunProperties2.Append(fontSize3);
- styleRunProperties2.Append(fontSizeComplexScript3);
- style3.Append(styleName3);
- style3.Append(basedOn2);
- style3.Append(nextParagraphStyle2);
- style3.Append(linkedStyle2);
- style3.Append(uIPriority2);
- style3.Append(unhideWhenUsed1);
- style3.Append(primaryStyle3);
- style3.Append(rsid3);
- style3.Append(styleParagraphProperties3);
- style3.Append(styleRunProperties2);
- // 把樣式添加入文檔中
- styles.Append(style2);
- styles.Append(style3);
- }
②創建ApplyStyleToParagraph方法,為段落設定樣式:
- // 對段落應用格式
- static void ApplyStyleToParagraph(Paragraph paragraph, string styleN)
- {
- // 進入該段落的ParagraphProperties部分,如果沒有,創建新的。
- if (paragraph.Elements<ParagraphProperties>().Count() == 0)
- {
- paragraph.PrependChild<ParagraphProperties>(new ParagraphProperties());
- }
- ParagraphProperties pPr = paragraph.ParagraphProperties;
- // 設置StyleId
- if (pPr.ParagraphStyleId == null)
- pPr.ParagraphStyleId = new ParagraphStyleId();
- pPr.ParagraphStyleId.Val = styleN;
- }
③:Main方法中輸出文檔:
- static void Main(string[] args)
- {
- // ①:創建WordprocessingDocument實例doc,對應於TEST.docx文件
- using (WordprocessingDocument doc=WordprocessingDocument.Create(@"D:\Test.docx",WordprocessingDocumentType.Document))
- {
- // ②:為doc添加MainDocumentPart部分
- MainDocumentPart mainPart = doc.AddMainDocumentPart();
- // ③:為mainPart添加Document,對應於Word里的文檔內容部分
- mainPart.Document = new Document();
- // ④:為Document添加Body,之后所有於內容相關的均在此body中
- Body body=mainPart.Document.AppendChild(new Body());
- <strong>// 為文檔添加樣式
- CreateParagraphStyle(doc);
- // 創建段落Heading1
- Paragraph p1=mainPart.Document.Body.AppendChild(new Paragraph(new Run(new Text("標題1"))));
- // 為段落應用樣式
- ApplyStyleToParagraph(p1,"1");
- // 創建段落Heading2
- Paragraph p2 = mainPart.Document.Body.AppendChild(new Paragraph(new Run(new Text("標題2"))));
- // 為段落應用樣式
- ApplyStyleToParagraph(p2, "2");</strong>
- }
- }
總結下思路:通過編程的方法操作OpenXml文檔的基本思路為:從WordprocessingDocument這一級開始,一級一級找到對應的元素,設置對應元素的內容和屬性。涉及到格式相關時,則是先創建好格式,再應用格式。最終生成整個完整的文檔。
內容部分沒問題,本來就該這樣,但對於樣式部分,這么長的代碼怎么寫出來?而且對應於Word文檔里的每一個元素,在OpenXml類里都有對應的屬性、元素去設置,但究竟每個Word元素對應哪些OpenXmlSDK里的類、屬性,究竟該如何去組織C#代碼,最終確保能按我們所想,生成Word文檔。想要熟悉OpenXmlSDK所有的API再去完成工作是不可能,接下來就討論些在進行OpenXml編程時,非常有用的資源。
四、進行OpenXml編程時不可或缺的資源
(1)MSDN,這個東西,對於OpenXml編程新手而言絕對是快速進入工作任務最好的選擇:內容比較全,組織形式比較易懂,用來學習知識,快速開始工作最為理想。但缺點是有些內容不夠准確,也不能算不准確,在它特定的環境下是沒問題的,但是問題就在於它沒有明確提出這些環境限制,對我們新手而言,悲劇就發生了:把它的某些方法、思路應用到一個自己想當然的環境里,結果就是運行的結果出乎意料,更悲劇的就是來了情緒,為嘛它能成功我不行,一遍又一遍的重復錯誤的代碼,運行得到錯誤的結果。。。然后暫時崩潰。
(2)OpenXMLSDKTool.msi,這個工具,文章前面有簡單的介紹。利用這個工具,其實第三部分寫的那么多的代碼都不用怎么理解,只要自己對OpenXml的基本結構有些了解后,需要工具生成怎樣的格式、內容,就按照這些格式標准通過Word手動生成一個文檔,然后用OpenXMLSDKTool.msi這個工具去解析這個文檔:重點是個文檔包含了哪些xml文件,明白主要的xml文件是用來干嘛最好,不明白,就算了,不太影響。然后把每個xml利用工具反射出對應的C#代碼,這樣可以很方便的把已生成好的文檔映射為OpenXmlSdk里的類和屬性,以及C#代碼的思路就顯而易見了,熟悉好這些思路后,接下來不過就是模仿(其實不是模仿,因為思路自己已經理解了,有了自己的思路),就是Copy下工具生成的代碼里比較關鍵、但又比較機械的部分,修改下為我所用。
其實,熟悉了OpenXml的基本結構,利用OpenXMLSDKTool.msi工具的幫助,基本上可以不用太多知識准備就可以生成各種Word文檔(Excel、PoperPoint與此完全類似,因為他們的組織形式也是利用OpenXml)。當你可以按照預定的要求,把一個最簡單的文檔生成時,其他的任務就是邏輯處理,程序設計部分的東西了,接下來的任務就是坐等着上千頁的文檔自動生成出來。
五、關於效率和面臨的問題
通過OpenXml技術去完成Word文檔的生成、修改,在公司里我做了個數據庫文檔生成工具,內容就是對於數據庫中每個表,以表名為一級標題,然后選出這個表的表結構、索引(如果有)、約束(如果有)、序列這四個表出來,名稱作為二級標題,選出來的內容作為表格。數據庫中所有的表部分處理完后,再選出函數和存儲過程這些東西,每個函數作為一級標題,選出該函數或者存儲過程相關的信息作為表格,插入Word文檔中。最后整個文檔超過1000頁,從數據庫里取數據開始,到最終文檔生成為40S左右。當然,公司的電腦處理速度為2.93G,而且實際上我程序的代碼沒怎么優化,所以這個時間還是有減低的空間。
一個問題就是,自己通過編程生成的word文檔其實它的內容只能算是包括了最基本的組成部件,可能還有些內容不全,表現之一就是我生成的文檔不能被WPS打開。解決辦法就是用Word打開后,隨便編輯點啥,然后保存下。Word為自動補齊所缺少的內容。