開源Word讀寫組件DocX 的深入研究和問題總結

一. 前言

     前兩天看到了asxinyu大神的【原創】開源Word讀寫組件DocX介紹與入門,正好我也有類似的自動生成word文檔得需求,於是便仔細的研究了這個DocX。 我也把它融入到我的項目當中並進行了實踐。工具果然牛叉,但也有一些問題,後邊一併列出來。

  二. DocX的基本原理

     Word有一個開放的文件格式,叫做Office Open XML。Office 從2007版本開始用它。它的基本方法是將文本和格式存儲成xml,把其他資源(圖片等)存儲成獨立文件,並將其進行Zip壓縮。這樣的好處是它的體積遠比03版本的office文件小得多,但也造成了一部分不兼容性。因此,別指望DocX支持Office2003.

     當理解Word實質上是XML以後,就不難了解如何操作Word了。理論上說,你不需要任何工具就能對它進行操作,當然複雜性極高。微軟推出了Open XML SDK, 專門幫助.NET語言與Office實現互操作,這也成了COM組件外的新選擇,但它的安裝包有100M, 對很多部署來說,難度不小。

    這個組件DocX本身實際上是對XML操作的封裝庫,如果你有興趣看它的源代碼,基本核心就是XML的字符串組裝和拼接,添加一個圖表的本質就是在對應XML標籤下面再增加一個圖表的子文檔。

    看到字符串拼接,有經驗的同學肯定站出來會問性能如何。它沒有使用StringBuilder,但本身性能不差,我生成100頁的圖文並茂的Word文檔也是瞬間的事情,所以,沒有關係。

    三. 自動文檔生成的方法

     對程序開發來說,最常見的需求便是自動生成文檔,完全從0生成圖文並茂,排版合理的文檔對程序員來說不現實,代碼多得海了去了。所以很多人的做法是字符串替換,通過替換特定文字來操作,但這樣顯然是相當不專業的。

    比較合理的做法,是Office裏面的“域”。域的本質,對程序員來說就是表達式,這個變量可以是文檔字數,文檔頁數(這是Office裏面自帶的屬性)。域相當牛逼,甚至可以連接到一個數據庫,一個按鈕,乃至一篇網頁上去,真心無所不能。

     同樣你也可以自己添加屬性,也就是所謂“自定義屬性”。

    如何查看文檔中的所有自定義域呢?

    在Word最上面的“插入”卷展欄下選擇文檔部件->域,如下圖所示,並在域控制框中左側選擇DocProperty,即可看到所有的屬性:

imageimage

  怎麼在文檔中插入一個自定義域呢?一種做法是,隨便在上圖中選擇一個域(比如Author),點擊確定,就會在插入的位置生成一個域。 然後點擊右鍵,選擇‘切換域代碼’,即可改變裏面的域定義:

image

image

  修改Author,變成你想要的屬性,就可以了,把這個東西拷到別的地方,再修改下域代碼,就是一個新的域定義了。這些域定義,可以被我們用程序操作來替換。

  由於域實質上是表達式,所以涉及一個計算過程。可以選擇打印時自動更新,或者Ctrl+A全選,然後F9,就可全部更新所有域表達式。

   至於如何在程序中操作域,【原創】開源Word讀寫組件DocX介紹與入門已經介紹的很清楚了,就是變量賦值,你可以在表格中添加域,然後就動態填寫了表格。所以就不介紹了。

    但是,在實際開發中,有個致命的問題: 當你通過模板,爲自定義屬性賦值,生成新文檔後,打開新文檔這些域並沒有自動更新,這是非常麻煩的,因爲客戶可不想打開文檔後發現那些核心數據都是奇怪的東西,讓他們去摁F9自動更新域更是不可能。但要命的是,有些域被更新了,有些域沒有更新,從域定義上來看,沒有任何區別,這到底是怎麼回事? 如果有大神解決了這個問題,請不吝賜教!

 

四. 插入圖表的困擾 

   說到這裏,有一個良好排版的模板,加上自動替換的功能,文檔生成應該差不多了吧?不,還要插入圖表和圖片,這些用域暫時還不好解決。

   插入圖片的問題,關鍵是插入位置,你需要找到要插入的位置所在的段落(Paragraph).我的做法是,用Linq查詢,通過定位關鍵字的做法找到段落,然後插入之即可。雖然粗糙,但還能用。插入表格類似。

   但,怎麼插入圖表?所謂圖表,就是柱狀圖餅狀圖等等的東西?雖然官方示例裏有生成圖表的功能,但我用Word2013怎麼都打不開: “該文檔有問題”。百思不得其解,花了半天才在別人的2010上打開,大喊坑爹(因此,DocX對Office2013的兼容性不夠!)

   那好吧,我們用Word2010或者07總可以了吧?但目前版本的源代碼,只能往文檔的最後添加圖表,因爲只有一個這樣的函數:

image

這不是坑爹呢麼?另外有時候插入圖表或圖片會出錯,顯示XML錯誤,建立連接的ID重複! 更是坑爹。

   image

在這個地方會拋異常。

經過分析,是上面那個生成RelationshipID的函數出錯了, 後來,索性改了這個函數的方法,直接從GUID生成,這樣就不會錯了,代碼如下。

複製代碼
private string GetNextFreeRelationshipID()
        {
            String guid = String.Empty;
            do
            {
                guid = Guid.NewGuid().ToString();
            } while (Char.IsDigit(guid[0]));
            return guid;


            //string id =
            //(
            //    from r in mainPart.GetRelationships()
            //    select r.Id
            //).Max();

            //// The convension for ids is rid01, rid02, etc
            //string newId = id.Replace("rId", "");
            //int result;
            //if (int.TryParse(newId, out result))
            //    return ("rId" + (result + 1));
            //else
            //{
            //    String guid = String.Empty;
            //    do
            //    {
            //        guid = Guid.NewGuid().ToString();
            //    } while (Char.IsDigit(guid[0]));
            //    return guid;
            //}
        }
複製代碼

 

 至於只能在文檔最後添加圖表的問題,我做了以下的代碼修改:

複製代碼
 1 /// 
 2         /// Insert a chart in document
 3         /// 
 4         public void InsertChart(Chart chart,Paragraph paragraph=null)
 5         {
 6             // Create a new chart part uri.
 7             String chartPartUriPath = String.Empty;
 8             Int32 chartIndex = 1;
 9             do
10             {
11                 chartPartUriPath = String.Format
12                 (
13                     "/word/charts/chart{0}.xml",
14                     chartIndex
15                 );
16                 chartIndex++;
17             } while (package.PartExists(new Uri(chartPartUriPath, UriKind.Relative)));
18 
19             // Create chart part.
20             PackagePart chartPackagePart = package.CreatePart(new Uri(chartPartUriPath, UriKind.Relative), "application/vnd.openxmlformats-officedocument.drawingml.chart+xml");
21 
22             // Create a new chart relationship
23             String relID = GetNextFreeRelationshipID();
24          PackageRelationship rel = mainPart.CreateRelationship(chartPackagePart.Uri, TargetMode.Internal, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart", relID);
25 
26             // Save a chart info the chartPackagePart
27             using (TextWriter tw = new StreamWriter(chartPackagePart.GetStream(FileMode.Create, FileAccess.Write)))
28                 chart.Xml.Save(tw);
29 
30             // Insert a new chart into a paragraph.
31             
32             Paragraph p = paragraph ?? this.InsertParagraph()
33 //如果指定了paragraph,則從這個段落插入
34             XElement chartElement = new XElement(
35                 XName.Get("r", DocX.w.NamespaceName),
36                 new XElement(
37                     XName.Get("drawing", DocX.w.NamespaceName),
38                     new XElement(
39                         XName.Get("inline", DocX.wp.NamespaceName),
40                         new XElement(XName.Get("extent", DocX.wp.NamespaceName), new XAttribute("cx", "5486400"), new XAttribute("cy", "3200400")),
41                         new XElement(XName.Get("effectExtent", DocX.wp.NamespaceName), new XAttribute("l", "0"), new XAttribute("t", "0"), new XAttribute("r", "19050"), new XAttribute("b", "19050")),
42                         new XElement(XName.Get("docPr", DocX.wp.NamespaceName), new XAttribute("id", "1"), new XAttribute("name", "chart")),
43                         new XElement(
44                             XName.Get("graphic", DocX.a.NamespaceName),
45                             new XElement(
46                                 XName.Get("graphicData", DocX.a.NamespaceName),
47                                 new XAttribute("uri", DocX.c.NamespaceName),
48                                 new XElement(
49                                     XName.Get("chart", DocX.c.NamespaceName),
50                                     new XAttribute(XName.Get("id", DocX.r.NamespaceName), relID)
51                                 )
52                             )
53                         )
54                     )
55                ));
56             p.Xml.Add(chartElement);
57         }
複製代碼

 

和源代碼對比,很容易就能看出兩者的區別。

我能添加更多的圖表嗎?當然可以。源代碼只內置了三種圖表:Pie, Line和Bar,不能滿足要求,既然充分理解了它的原理,不妨我們擴展它吧:

複製代碼
public class PieChart : Chart 
  { 
      #region Properties

      public override Boolean IsAxisExist 
      { 
          get 
          { 
              return false; 
          } 
      }

      public override Int16 MaxSeriesCount 
      { 
          get 
          { 
              return 1; 
          } 
      }

      #endregion

      #region Methods

      protected override XElement CreateChartXml() 
      { 
          return XElement.Parse(@"<c:pieChart xmlns:c=""http://schemas.openxmlformats.org/drawingml/2006/chart""> 
                </c:pieChart>"); 
      }

      #endregion 
  } 
  
複製代碼

 

  注意看上面PieChart的定義,只要修改CreateChartXml函數,修改pieChart變成你想要的圖表就可以了,通過一個工廠方法指定枚舉類型生成想要的圖表。至於Word支持什麼類型的圖表,在微軟的技術文章這裏可以看到更詳細的類型。我又創建了四五個新類型。滿足了我的需要。

五. 總結

    用了這個組件,感受良多,這哥們和我一樣的在校學生,,但已經做了這樣的開源項目,下載量超過18000+。 雖然理論不一定多牛逼,但確實滿足廣大人民羣衆需要了,400KB的dll文件,直接解決了.NET平臺word生成這一剛性需求。  但作者確實下了功夫,大量的XML轉換和分析,純粹的體力活啊!

   1. 對Office2013的支持不夠

   2. API遠沒達到完善,例如無法良好的操作圖表類型,只能使用默認值。

   3. 代碼欠重構,可獲得更好的程序風格和性能的提升。

   4. 域更新不正常

  如果這幾個問題能解決,那確實是最好不過的了。除此之外,其實還有很多小問題,一方面期待作者解決,另外一方面如果項目需求緊急的話,索性我們自己先改了得了。代碼還是很容易理解的。

  我嘗試把它用在項目中,經過測試,發現基本穩定,大家可以嘗試採納。

  有任何問題,歡迎隨時交流,如果您覺得對您有幫助,請點推薦,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章