最近工作中在开发的一个项目需要根据数据库的数据来动态生成Word文档存放在服务端,让用户随时随地下载。以前没有接触过Office开发,临时查了些资料,最终用COM接口完成了。项目虽然是完成了,不过我还是对期间处理Word COM接口的繁琐过程心有余悸。后期发现存在遗留问题,而且效率也不高,生成上万条的数据的时候实在是让人无语。跟朋友聊过类似的问题后,朋友提出可以用OpenXML。稍微了解一下OpenXML,于是我就动了用OpenXML的念头,稍稍的研究了一下,果然速度和效率不是吹的。
以下是我整理的一些在项目中用到过的方法。
项目中的功能是根据主模版文档来生成word,还需从子模板中复制内容到word中。总结了一下,分成四部分来操作。(实际上后三部分才使用到Openxml)
注意:(源代码管理器中开发的所有文件都会为“只读”。)需要操作的文档都必须把“只读”属性去掉,最好是把存放模板文件的文件夹整体去掉“只读”属性。
第一部,当然是最简单的,复制模板到指定位置,操作复制后的文件。
if (File.Exists(filename)) //判断文件已存在,则删除 { File.Delete(filename); } File.Copy(objTemplate, filename, true); //先复制一份模板文件
第二部分,复制子模板内容到Word文档中某个位置,位置用书签来定位。要考滤到需要复制的文档中存在一些什么元素,都需要相对应复制过来,并要考滤相关联的xml文件也需改变。此示例中的子模板存在以下元素:项目符号:WNumberingId;段落样式:ParagraphStyleId;超链接:Hyperlink;块样式:RunStyle
以下是复制子模板的内容:
/// <summary> /// 复制模板内容到Word中书签的指定位置 ///========================================= /// </summary> /// <param name="temps">多个子模板</param> /// <param name="filePath">To文档径</param> /// <param name="bookmarkName">书签名称</param> public static void CopyTempsToFile(string[] temps, string filePath, string bookmarkName) { using (WordprocessingDocument objectiveDoc = WordprocessingDocument.Open(filePath, true)) { try { MainDocumentPart mainPart = objectiveDoc.MainDocumentPart; int s = mainPart.ThemePart.ImageParts.Count(); #region var res = from t in objectiveDoc.MainDocumentPart.Document.Body.Descendants<WBookmarkStart>() where t.Name == bookmarkName select t; var bookmark = res.SingleOrDefault(); if (bookmark != null) { var parent = bookmark.Parent.NextSibling<WParagraph>(); foreach (WText txt in bookmark.Parent.Descendants<WText>()) { txt.Text = ""; } foreach (var name in temps) { string filename = name; System.IO.FileInfo info = new System.IO.FileInfo(filename); if (info.IsReadOnly) { throw new ApplicationException("文件:" + name + "为只读,请修改后再执行操作。"); } try { using (WordprocessingDocument sourceDoc = WordprocessingDocument.Open(filename, true)) { Hashtable htStyles = CreateStylesPart(sourceDoc, objectiveDoc); Hashtable htNumberingInstance = CreateNumberingPart(sourceDoc, objectiveDoc); Hashtable htHyperlink = CreateHyperlinkPart(sourceDoc, objectiveDoc); //复制段落到书签 WBody body = sourceDoc.MainDocumentPart.Document.Body; int p = 0; int tb = 0; var ps = from t in body.ChildElements select t; foreach (var item in ps) { if (item.GetType().Name == "Paragraph") { WParagraph p1 = body.Elements<WParagraph>().ElementAt(p); OpenXmlElement oxePara = p1.CloneNode(true); if (oxePara.Descendants<WNumberingId>().Count() > 0) { int numberID = oxePara.Descendants<WNumberingId>().First().Val; if (htNumberingInstance[numberID] != null) { oxePara.Descendants<WNumberingId>().First().Val = WebHelper.SafeParse(htNumberingInstance[numberID], 0); } } if (oxePara.Descendants<ParagraphStyleId>().Count() > 0) { string styleId = oxePara.Descendants<ParagraphStyleId>().First().Val; if (htStyles[styleId] != null) { oxePara.Descendants<ParagraphStyleId>().First().Val = htStyles[styleId].ToString(); } } if (oxePara.Descendants<Hyperlink>().Count() > 0) { string hlnkId = oxePara.Descendants<Hyperlink>().First().Id; if (htHyperlink[hlnkId] != null) { oxePara.Descendants<Hyperlink>().First().Id = htHyperlink[hlnkId].ToString(); } } if (oxePara.Descendants<RunStyle>().Count() > 0) { string rstyId = oxePara.Descendants<RunStyle>().First().Val; if (htStyles[rstyId] != null) { oxePara.Descendants<RunStyle>().First().Val = htStyles[rstyId].ToString(); } } parent.InsertBeforeSelf(oxePara); p++; } if (item.GetType().Name == "Table") { WTable tb1 = body.Elements<WTable>().ElementAt(tb); OpenXmlElement oxeTab = tb1.CloneNode(true); if (oxeTab.Descendants<WNumberingId>().Count() > 0) { foreach (var oexItem in oxeTab.Descendants<WNumberingId>()) { int oldId = oexItem.Val; if (htNumberingInstance[oldId] != null) { oexItem.Val = WebHelper.SafeParse(htNumberingInstance[oldId], 0); } } } parent.InsertBeforeSelf(oxeTab); tb++; } } } } catch (Exception ex) { throw new ApplicationException("出错文件:" + name + ",错误信息:" + ex.Message.ToString()); } } } #endregion } catch (Exception) { throw; } finally { objectiveDoc.Close(); objectiveDoc.Dispose(); } } }
生成word样式的方法:
/// <summary> /// 创建子模板的样式 ///========================================= /// </summary> /// <param name="sourceDoc">来源文档</param> /// <param name="objectiveDoc">目标文档</param> /// <returns></returns> private static Hashtable CreateStylesPart(WordprocessingDocument sourceDoc, WordprocessingDocument objectiveDoc) { try { MainDocumentPart mainPart = objectiveDoc.MainDocumentPart; //目标文档主体 StyleDefinitionsPart stPart = mainPart.StyleDefinitionsPart; Styles styles2 = null; if (stPart != null) { styles2 = stPart.Styles; } else { int st = mainPart.Parts.Count(); styles2 = new Styles(); styles2.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); styles2.AddNamespaceDeclaration("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"); DocDefaults docDefaults2 = GenerateDocDefaults(); LatentStyles latentStyles2 = GenerateLatentStyles(); } Hashtable htStyles = new Hashtable(); if (sourceDoc.MainDocumentPart.StyleDefinitionsPart != null) { #region 为目标文档添加 子模板中的 样式 int styleNum = 0; OpenXmlElement oxeStyle = sourceDoc.MainDocumentPart.StyleDefinitionsPart.Styles.CloneNode(true); foreach (var item in oxeStyle.Descendants<Style>()) { #region 检查新的 styleId string styleId = "a" + styleNum; foreach (var objStyle in styles2.Descendants<Style>()) { if (objStyle.StyleId == styleId) { styleNum++; styleId = "a" + styleNum; } } #endregion string sourId = item.StyleId; htStyles.Add(sourId, styleId); item.StyleId = styleId; int styNum = styles2.Elements<Style>().Count(); if (styNum > 0) { styles2.Elements<Style>().ElementAt(styNum - 1).InsertAfterSelf(item.CloneNode(true)); //下标从0开始 } else { styles2.ChildElements[1].Append(item); //第一个Style 节点 } styleNum++; } if (stPart == null) { stPart.Styles = styles2; stPart.Styles.Append(sourceDoc.MainDocumentPart.StyleDefinitionsPart.Styles.CloneNode(true)); } #endregion } return htStyles; } catch (Exception ex) { throw new ApplicationException("读取子模板样式时出错,错误信息:" + ex.Message.ToString()); } }
生成项目符号的方法:
/// <summary> /// 创建子模板的项目符号 ///========================================= /// </summary> /// <param name="sourceDoc">来源文档</param> /// <param name="objectiveDoc">目标文档</param> /// <returns></returns> private static Hashtable CreateNumberingPart(WordprocessingDocument sourceDoc, WordprocessingDocument objectiveDoc) { try { MainDocumentPart mainPart = objectiveDoc.MainDocumentPart; WNumbering numbering2 = null; NumberingDefinitionsPart ndPart = mainPart.NumberingDefinitionsPart; if (ndPart != null) { numbering2 = ndPart.Numbering; } else { int c = mainPart.Parts.Count(); ndPart = mainPart.AddNewPart<NumberingDefinitionsPart>("rId" + c + 1); numbering2 = new WNumbering(); numbering2.AddNamespaceDeclaration("ve", "http://schemas.openxmlformats.org/markup-compatibility/2006"); numbering2.AddNamespaceDeclaration("o", "urn:schemas-microsoft-com:office:office"); numbering2.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"); numbering2.AddNamespaceDeclaration("m", "http://schemas.openxmlformats.org/officeDocument/2006/math"); numbering2.AddNamespaceDeclaration("v", "urn:schemas-microsoft-com:vml"); numbering2.AddNamespaceDeclaration("wp", "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"); numbering2.AddNamespaceDeclaration("w10", "urn:schemas-microsoft-com:office:word"); numbering2.AddNamespaceDeclaration("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"); numbering2.AddNamespaceDeclaration("wne", "http://schemas.microsoft.com/office/word/2006/wordml"); } Hashtable htNumberingInstance = new Hashtable(); Hashtable htAbsNum = new Hashtable(); if (sourceDoc.MainDocumentPart.NumberingDefinitionsPart != null) { #region 新增项目符号 var numbering1 = from n in sourceDoc.MainDocumentPart.NumberingDefinitionsPart.Numbering.Elements() select n; int npbId = 0; int anId = 0; int niId = 1; int oldAbsId = 0; if (ndPart != null) { npbId = numbering2.Elements<WNumberingPictureBullet>().Count(); anId = numbering2.Elements<WAbstractNum>().Count(); niId = numbering2.Elements<WNumberingInstance>().Count() + 1; oldAbsId = numbering2.Descendants<WAbstractNumId>().Count(); //主模板原有的 WAbstractNumId数量 } foreach (var number in numbering1) { if (number.GetType().Name == "NumberingPictureBullet") { WNumberingPictureBullet numberingPictureBullet1 = new WNumberingPictureBullet() { NumberingPictureBulletId = npbId }; foreach (var pic in number.ChildElements) { numberingPictureBullet1.Append(pic.CloneNode(true)); } int picNum = numbering2.Elements<WNumberingPictureBullet>().Count(); if (picNum > 0) { numbering2.Elements<WNumberingPictureBullet>().ElementAt(picNum - 1).InsertAfterSelf(numberingPictureBullet1); //下标从0开始 } else { numbering2.ChildElements[0].InsertBeforeSelf(numberingPictureBullet1); //第一个 NumberingPictureBullet 节点 } npbId++; } if (number.GetType().Name == "AbstractNum") { WAbstractNum abstractNum1 = new WAbstractNum() { AbstractNumberId = anId }; foreach (var abs in number.ChildElements) { abstractNum1.Append(abs.CloneNode(true)); } numbering2.Elements<WAbstractNum>().ElementAt(anId - 1).InsertAfterSelf(abstractNum1); int oldID = WebHelper.SafeParse(GetXmlNodeId(number.OuterXml, "w:abstractNumId"), 0); htAbsNum.Add(oldID, anId); anId++; } if (number.GetType().Name == "NumberingInstance") { string strNumberingInstance = number.OuterXml; int oldID = WebHelper.SafeParse(GetXmlNodeId(strNumberingInstance, "w:numId"), 0); int newID = niId; htNumberingInstance.Add(oldID, newID); WNumberingInstance numberingInstance1 = new WNumberingInstance() { NumberID = niId }; foreach (var item in number.ChildElements) { if (item.GetType().Name == "AbstractNumId") { int absId = WebHelper.SafeParse(GetXmlNodeId(item.OuterXml, "w:val"), 0); WAbstractNumId abstractNumId1 = new WAbstractNumId() { Val = WebHelper.SafeParse(htAbsNum[absId], 0) }; numberingInstance1.Append(abstractNumId1); } else { numberingInstance1.Append(item.CloneNode(true)); } } numbering2.Elements<WNumberingInstance>().ElementAt(niId - 2).InsertAfterSelf(numberingInstance1); niId++; } } if (ndPart == null) { ndPart.Numbering = numbering2; ndPart.Numbering.Append(sourceDoc.MainDocumentPart.NumberingDefinitionsPart.Numbering.CloneNode(true)); } #endregion } return htNumberingInstance; } catch (Exception ex) { throw new ApplicationException("读取子模板项目符号时出错,错误信息:" + ex.Message.ToString()); } }
生成超链接的方法:
#region /// <summary> /// 创建子模板的超链接到父模板 ///========================================= /// </summary> /// <param name="sourceDoc">来源文档</param> /// <param name="objectiveDoc">目标文档</param> /// <returns></returns> private static Hashtable CreateHyperlinkPart(WordprocessingDocument sourceDoc, WordprocessingDocument objectiveDoc) { try { Hashtable ht = new Hashtable(); MainDocumentPart mainPart = objectiveDoc.MainDocumentPart; //目标文档主体 int c = mainPart.Parts.Count(); int hylnkNum = c + 1; if (sourceDoc.MainDocumentPart.HyperlinkRelationships != null) { foreach (var item in sourceDoc.MainDocumentPart.HyperlinkRelationships) { string hylnkId = "rId" + hylnkNum; foreach (var objHylnk in mainPart.Document.Descendants<Hyperlink>()) { if (objHylnk.Id == hylnkId) { hylnkId = "rId" + hylnkNum + 1; } } ht.Add(item.Id, hylnkId); string url = item.Uri.ToString(); mainPart.AddHyperlinkRelationship(new System.Uri(url, System.UriKind.Absolute), true, hylnkId); hylnkNum++; } } return ht; } catch (Exception ex) { throw new ApplicationException("读取子模板超链时出错,错误信息:" + ex.Message.ToString()); } } #endregion
第三部分:替换word中的部分内容,此处需用到word中的书签功能。首先在word模板中定义好需要替换的书签,第一部复制模板的时候会自动复制过去。然后在程序中定义到要替换的内容,进行替换。
string[] tableremarks = new string[] { "name","age","sex" }; string[]tablevalues = new string[] { "tracy","24","girl" };
using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(filepath, true)) { try { for (int oIndex = 0; oIndex < tableremarks.Length; oIndex++) { string obDD_Name = tableremarks[oIndex]; string val = tablevalues[oIndex].ToString(); var res = from t in wordDoc.MainDocumentPart.Document.Body.Descendants<WBookmarkStart>() where t.Name == obDD_Name select t; var bookmark = res.SingleOrDefault(); var pare = bookmark.Parent; if (bookmark != null) { foreach (WText text in bookmark.NextSibling().Elements<WText>()) { text.Text = val; } } } } catch (Exception) { throw; } finally { wordDoc.Close(); wordDoc.Dispose(); } }
第四部分:为word中的表格插入数据。
/// <summary> /// 根据下标找到表格位置并添加数据 ///========================================= /// </summary> /// <param name="filepath">文件路径</param> /// <param name="dt">需插入的数据,用DataTable接收</param> /// <param name="index">表格的下标(下标从0开始)</param> /// <param name="rowIndex">在表格的第几行开始添加数据(下标从0开始)</param> public static void AddDataToTableByIndex(string filepath, int tableIndex, DataTable dt, int rowIndex) { using (WordprocessingDocument doc = WordprocessingDocument.Open(filepath, true)) { try { WTable table = doc.MainDocumentPart.Document.Body.Elements<WTable>().ElementAt(tableIndex); WTableRow row = table.Elements<WTableRow>().ElementAt(rowIndex); //从第几行开始插入数据 #region 循环为表格添加数据 for (int i = 0; i < dt.Rows.Count; i++) { //添加行,复制第一行的元素,以免格式丢失 if (i > 0) { List<OpenXmlElement> rowElements = new List<OpenXmlElement>(); foreach (var item in row.Elements()) { rowElements.Add(item.Clone() as OpenXmlElement); } WTableRow newRow = table.InsertAfter(new WTableRow(), row); newRow.Append(rowElements); } //循环添加数据 for (int j = 0; j < dt.Columns.Count; j++) { WTableCell cell = row.Elements<WTableCell>().ElementAt(j); AddTextToTableCell(cell, dt.Rows[i][j].ToString()); } } #endregion } catch (Exception) { throw; } finally { doc.Close(); doc.Dispose(); } } }
PS:插入的数据太大,有可能会出现异常,小数据量没有问题。
拒绝访问。(Exception FROM HRESULT:0x80070005(E_ACCESSDENIED))
我在项目中就出现过,找了很久没有找到原因,最后的解决方案是:
在C:\Documents and Settings\Default User\Local Settings\Application Data 位置下,建一个名为 IsolatedStorage的文件夹。
原理还没有搞清楚,个人认为,可能是因为数据量太大,需要一个虚拟存储空间吧。
Openxml很强大,需要研究的东西还很多,下面是几个比较详细的有关Openxml 的文章。
http://www.cnblogs.com/brooks-dotnet/archive/2010/02/08/1665600.html
http://blog.csdn.net/atian15/article/details/6948602
http://blog.csdn.net/april_zheng/article/details/6741143