最近工作中在开发的一个项目需要根据数据库的数据来动态生成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