Word报告自动生成(例如 导出数据库结构)

时间:2024-04-10 19:36:04

将很早之前写的一个小组件重新整理优化一下,做成一个通用的功能。适用于导出数据库的结构(表、字段等)到Word或将体检数据自动生成Word版的体检报告等。代码:Github

一、主要需要完成功能:

1. 灵活的配置规则及word样式设置(文本、表格、图表、颜色等).

2. 支持表格.

3. 支持图表.

4. 支持章节内容循环生成.

5. 支持目录.

6.支持文档结构图

7.更新指定位置的文字

8.支持pdf导出.

最后结果如下:

Word报告自动生成(例如 导出数据库结构)

图一

Word报告自动生成(例如 导出数据库结构)

图二

Word报告自动生成(例如 导出数据库结构)

图三

二、需求分析与实现方式

   功能主要涉及3个比较重要的部分:数据源、Word样式、配置规则。

为了简单,数据源决定采用一个存储过程返回Dataset的方式, 整张报告的数据来源于此Dataset的多个Datatable.

  样式与配置:首先想到的是写一个config文件,所有配置都放到一个文件里,然后将数据按照这个规则生成word。但无疑这样的配置项太多了,关键是“样式”问题,比如字体、颜色、表格宽度.....想想就头大。而且没有“所见即所得”的效果,配置完都不知道啥样。

后来决定采取修改的方式, 先以一个word文件作为模板,在模板中定义好上面提到的“样式”,然后在模板中做一个个标记,然后将数据按照规则更新到对应的标记。

Word报告自动生成(例如 导出数据库结构)

图四

而这个标记我们用到了word的一个叫【书签】的功能,打开word按ctrl+shift+F5, 打开书签功能,如下图:

Word报告自动生成(例如 导出数据库结构)

图五

这样将【规则】通过一系列规则的【书签】定义到word模板中。

三、规则配置

  思路确定了,那就开始设计如何通过【书签】将规则定义到word模板中去,这里决定将所有规则都通过【书签】实现,而放弃config文件的方式,这个更统一而且直观一些。

A.循环

以图四为例,数据库有多少张表是不固定的,我们在制作模板的时候不可能先画好N(N为表的总数)个表格等待数据填充, 这里就会需要遍历数据源中提供的所有表结构数据,然后逐一形成表格。这里就需要将图四中的表格循环一下,自动复制生成多个这样的表格。当然,这只是一种情况,还有可能会出现循环嵌套循环的情况,那么我将这个循环定义成一个书签的时候按照这样的格式:

loop_级别_表序号_filter_名称

含义如下:

loop:代表这是一个循环。

级别:默认文档级别为0,出现的第一层循环为1,其内部若再次嵌套循环则级别为2,依次类推。

表序号:取Dataset中的第几张表(从1开始)

filter:循环的时候可能会用到对datatable的查找过滤,在此写出,多个字段用XX隔开(因为此处不允许有下划线外其他特殊字符, 就用这个XX吧 )

名称:loop名称,方便与其他 loop区别

B.更新指定位置的文字

如图四中的【服务器名】、【表总数】等,只需要替换对应的文字即可:

label_级别_名称

含义如下:

label:代表这是一个label。

级别:默认文档级别为0,出现的第一层循环为1,其内部若再次嵌套循环则级别为2,依次类推。

名称:label名称

注意这里省略了表序号,当级别为0的时候 ,自动取最后一个datatable中的数据,因为这个label经常会用到其他表汇总的数据,可能会用到之前几张表的数据,所以放在其他表都处理好后。当级别为1的时候,自然取该级别循环的行数据。

C.表格

表格的配置原本也想对表格添加书签,后来发现有个表格属性, 觉得写在这里更好一些。

Word报告自动生成(例如 导出数据库结构)

如上图所示,【标题】格式为:table_级别_取Dataset中的第几张表(从1开始)_filter字段多个用XX隔开(此处不允许有下划线外其他特殊字符, 就用这个XX吧 )_名称

【说明】为可选项,若需要合计行, 则需要标识, summary或缩写s: [合计]行是模板中表格的第几行   summaryfilter或缩写sf:数据集进一步filter到summary行的条件(因为一个表格只取一个Datatable,通过一个标识指定了哪些datarow是用来作为合计的)

D.图表

同样为了方便将配置写在了【标题】,图表生成后会将名称修改过来。

配置格式为:chart_级别_取Dataset中的第几张表(从1开始)_filter字段多个用XX隔开(此处不允许有下划线外其他特殊字符, 就用这个XX吧 )_chart名称_是否将Datatable的columnName作为第一行_从datatable第几列开始(列起始为1)_截止列,

如下图所示配置即可。

Word报告自动生成(例如 导出数据库结构)

E.目录

无需标识, 模板中添加目录, 当内容处理完成之后, 会根据处理后的结果动态更新目录.

四、主要代码

 using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word; namespace FlyLolo.WordReport.Demo
{
public class WordReportHelper
{
private Word.Application wordApp = null;
private Word.Document wordDoc = null;
private DataSet dataSource = null;
private object line = Word.WdUnits.wdLine;
private string errorMsg = ""; /// <summary>
/// 根据模板文件,创建数据报告
/// </summary>
/// <param name="templateFile">模板文件名(含路径)</param>
/// <param name="newFilePath">新文件路径)</param>
/// <param name="dataSource">数据源,包含多个datatable</param>
/// <param name="saveFormat">新文件格式:</param>
public bool CreateReport(string templateFile, DataSet dataSource, out string errorMsg, string newFilePath, ref string newFileName, int saveFormat = )
{
this.dataSource = dataSource;
errorMsg = this.errorMsg;
bool rtn = OpenTemplate(templateFile)
&& SetContent(new WordElement(wordDoc.Range(), dataRow: dataSource.Tables[dataSource.Tables.Count - ].Rows[]))
&& UpdateTablesOfContents()
&& SaveFile(newFilePath, ref newFileName, saveFormat); CloseAndClear();
return rtn;
} /// <summary>
/// 打开模板文件
/// </summary>
/// <param name="templateFile"></param>
/// <returns></returns>
private bool OpenTemplate(string templateFile)
{
if (!File.Exists(templateFile))
{
return false;
} wordApp = new Word.Application();
wordApp.Visible = true;//使文档可见,调试用
wordApp.DisplayAlerts = Word.WdAlertLevel.wdAlertsNone;
object file = templateFile;
wordDoc = wordApp.Documents.Open(ref file, ReadOnly: false);
return true;
} /// <summary>
/// 为指定区域写入数据
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool SetContent(WordElement element)
{
string currBookMarkName = string.Empty;
string startWith = "loop_" + (element.Level + ).ToString() + "_";
foreach (Word.Bookmark item in element.Range.Bookmarks)
{
currBookMarkName = item.Name; if (currBookMarkName.StartsWith(startWith) && (!currBookMarkName.Equals(element.ElementName)))
{
SetLoop(new WordElement(item.Range, currBookMarkName, element.DataRow, element.GroupBy));
} } SetLabel(element); SetTable(element); SetChart(element); return true;
} /// <summary>
/// 处理循环
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool SetLoop(WordElement element)
{
DataRow[] dataRows = dataSource.Tables[element.TableIndex].Select(element.GroupByString);
int count = dataRows.Count();
element.Range.Select(); //第0行作为模板 先从1开始 循环后处理0行;
for (int i = ; i < count; i++)
{ element.Range.Copy(); //模板loop复制
wordApp.Selection.InsertParagraphAfter();//换行 不会清除选中的内容,TypeParagraph 等同于回车,若当前有选中内容会被清除. TypeParagraph 会跳到下一行,InsertParagraphAfter不会, 所以movedown一下.
wordApp.Selection.MoveDown(ref line, Missing.Value, Missing.Value);
wordApp.Selection.Paste(); //换行后粘贴复制内容
int offset = wordApp.Selection.Range.End - element.Range.End; //计算偏移量 //复制书签,书签名 = 模板书签名 + 复制次数
foreach (Word.Bookmark subBook in element.Range.Bookmarks)
{
if (subBook.Name.Equals(element.ElementName))
{
continue;
} wordApp.Selection.Bookmarks.Add(subBook.Name + "_" + i.ToString(), wordDoc.Range(subBook.Start + offset, subBook.End + offset));
} SetContent(new WordElement(wordDoc.Range(wordApp.Selection.Range.End - (element.Range.End - element.Range.Start), wordApp.Selection.Range.End), element.ElementName + "_" + i.ToString(), dataRows[i], element.GroupBy));
} element.Range.Delete(); return true;
} /// <summary>
/// 处理简单Label
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool SetLabel(WordElement element)
{
if (element.Range.Bookmarks != null && element.Range.Bookmarks.Count > )
{
string startWith = "label_" + element.Level.ToString() + "_";
string bookMarkName = string.Empty;
foreach (Word.Bookmark item in element.Range.Bookmarks)
{
bookMarkName = item.Name; if (bookMarkName.StartsWith(startWith))
{
bookMarkName = WordElement.GetName(bookMarkName); item.Range.Text = element.DataRow[bookMarkName].ToString();
}
}
} return true;
} /// <summary>
/// 填充Table
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool SetTable(WordElement element)
{
if (element.Range.Tables != null && element.Range.Tables.Count > )
{
string startWith = "table_" + element.Level.ToString() + "_";
foreach (Word.Table table in element.Range.Tables)
{
if (!string.IsNullOrEmpty(table.Title) && table.Title.StartsWith(startWith))
{
WordElement tableElement = new WordElement(null, table.Title, element.DataRow); TableConfig config = new TableConfig(table.Descr); object dataRowTemplate = table.Rows[config.DataRow];
Word.Row SummaryRow = null;
DataRow SummaryDataRow = null;
DataTable dt = dataSource.Tables[tableElement.TableIndex];
DataRow[] dataRows = dataSource.Tables[tableElement.TableIndex].Select(tableElement.GroupByString); ; if (config.SummaryRow > )
{
SummaryRow = table.Rows[config.SummaryRow];
SummaryDataRow = dt.Select(string.IsNullOrEmpty(tableElement.GroupByString) ? config.SummaryFilter : tableElement.GroupByString + " and " + config.SummaryFilter).FirstOrDefault();
} foreach (DataRow row in dataRows)
{
if (row == SummaryDataRow)
{
continue;
} Word.Row newRow = table.Rows.Add(ref dataRowTemplate);
for (int j = ; j < table.Columns.Count; j++)
{
newRow.Cells[j + ].Range.Text = row[j].ToString(); ;
} } ((Word.Row)dataRowTemplate).Delete(); if (config.SummaryRow > && SummaryDataRow != null)
{
for (int j = ; j < SummaryRow.Cells.Count; j++)
{
string temp = SummaryRow.Cells[j + ].Range.Text.Trim().Replace("\r\a", ""); if (!string.IsNullOrEmpty(temp) && temp.Length > && dt.Columns.Contains(temp.Substring(, temp.Length - )))
{
SummaryRow.Cells[j + ].Range.Text = SummaryDataRow[temp.Substring(, temp.Length - )].ToString();
}
}
} table.Title = tableElement.Name;
} }
} return true;
} /// <summary>
/// 处理图表
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool SetChart(WordElement element)
{
if (element.Range.InlineShapes != null && element.Range.InlineShapes.Count > )
{
List<Word.InlineShape> chartList = element.Range.InlineShapes.Cast<Word.InlineShape>().Where(m => m.Type == Word.WdInlineShapeType.wdInlineShapeChart).ToList();
string startWith = "chart_" + element.Level.ToString() + "_";
foreach (Word.InlineShape item in chartList)
{
Word.Chart chart = item.Chart;
if (!string.IsNullOrEmpty(chart.ChartTitle.Text) && chart.ChartTitle.Text.StartsWith(startWith))
{
WordElement chartElement = new WordElement(null, chart.ChartTitle.Text, element.DataRow); DataTable dataTable = dataSource.Tables[chartElement.TableIndex];
DataRow[] dataRows = dataTable.Select(chartElement.GroupByString); int columnCount = dataTable.Columns.Count;
List<int> columns = new List<int>(); foreach (var dr in dataRows)
{
for (int i = chartElement.ColumnStart == - ? : chartElement.ColumnStart - ; i < (chartElement.ColumnEnd == - ? columnCount : chartElement.ColumnEnd); i++)
{
if (columns.Contains(i) || dr[i] == null || string.IsNullOrEmpty(dr[i].ToString()))
{ }
else
{
columns.Add(i);
}
}
}
columns.Sort();
columnCount = columns.Count;
int rowsCount = dataRows.Length; Word.ChartData chartData = chart.ChartData; //chartData.Activate();
//此处有个比较疑惑的问题, 不执行此条,生成的报告中的图表无法再次右键编辑数据. 执行后可以, 但有两个问题就是第一会弹出Excel框, 处理完后会自动关闭. 第二部分chart的数据range设置总不对
//不知道是不是版本的问题, 谁解决了分享一下,谢谢 Excel.Workbook dataWorkbook = (Excel.Workbook)chartData.Workbook;
dataWorkbook.Application.Visible = false; Excel.Worksheet dataSheet = (Excel.Worksheet)dataWorkbook.Worksheets[];
//设定范围
string a = (chartElement.ColumnNameForHead ? rowsCount + : rowsCount) + "|" + columnCount;
Console.WriteLine(a); Excel.Range tRange = dataSheet.Range["A1", dataSheet.Cells[(chartElement.ColumnNameForHead ? rowsCount + : rowsCount), columnCount]];
Excel.ListObject tbl1 = dataSheet.ListObjects[];
//dataSheet.ListObjects[1].Delete(); //想过重新删除再添加 这样 原有数据清掉了, 但觉得性能应该会有所下降
//Excel.ListObject tbl1 = dataSheet.ListObjects.AddEx();
tbl1.Resize(tRange);
for (int j = ; j < rowsCount; j++)
{
DataRow row = dataRows[j];
for (int k = ; k < columnCount; k++)
{
dataSheet.Cells[j + , k + ].FormulaR1C1 = row[columns[k]];
}
} if (chartElement.ColumnNameForHead)
{
for (int k = ; k < columns.Count; k++)
{
dataSheet.Cells[, k + ].FormulaR1C1 = dataTable.Columns[columns[k]].ColumnName;
}
}
chart.ChartTitle.Text = chartElement.Name;
//dataSheet.Application.Quit();
}
}
} return true;
} /// <summary>
/// 更新目录
/// </summary>
/// <returns></returns>
private bool UpdateTablesOfContents()
{
foreach (Word.TableOfContents item in wordDoc.TablesOfContents)
{
item.Update();
} return true;
} /// <summary>
/// 保存文件
/// </summary>
/// <param name="newFilePath"></param>
/// <param name="newFileName"></param>
/// <param name="saveFormat"></param>
/// <returns></returns>
private bool SaveFile(string newFilePath, ref string newFileName, int saveFormat = )
{
if (string.IsNullOrEmpty(newFileName))
{
newFileName = DateTime.Now.ToString("yyyyMMddHHmmss"); switch (saveFormat)
{
case :// Word.WdSaveFormat.wdFormatDocument
newFileName += ".doc";
break;
case :// Word.WdSaveFormat.wdFormatDocumentDefault
newFileName += ".docx";
break;
case :// Word.WdSaveFormat.wdFormatPDF
newFileName += ".pdf";
break;
default:
break;
}
} object newfile = Path.Combine(newFilePath, newFileName);
object wdSaveFormat = saveFormat;
wordDoc.SaveAs(ref newfile, ref wdSaveFormat);
return true;
} /// <summary>
/// 清理
/// </summary>
private void CloseAndClear()
{
if (wordApp == null)
{
return;
}
wordDoc.Close(Word.WdSaveOptions.wdDoNotSaveChanges);
wordApp.Quit(Word.WdSaveOptions.wdDoNotSaveChanges);
System.Runtime.InteropServices.Marshal.ReleaseComObject(wordDoc);
System.Runtime.InteropServices.Marshal.ReleaseComObject(wordApp);
wordDoc = null;
wordApp = null;
GC.Collect();
KillProcess("Excel", "WINWORD");
} /// <summary>
/// 杀进程..
/// </summary>
/// <param name="processNames"></param>
private void KillProcess(params string[] processNames)
{
//Process myproc = new Process();
//得到所有打开的进程
try
{
foreach (string name in processNames)
{
foreach (Process thisproc in Process.GetProcessesByName(name))
{
if (!thisproc.CloseMainWindow())
{
if (thisproc != null)
thisproc.Kill();
}
}
}
}
catch (Exception)
{
//throw Exc;
// msg.Text+= "杀死" + processName + "失败!";
}
}
} /// <summary>
/// 封装的Word元素
/// </summary>
public class WordElement
{
public WordElement(Word.Range range, string elementName = "", DataRow dataRow = null, Dictionary<string, string> groupBy = null, int tableIndex = )
{
this.Range = range;
this.ElementName = elementName;
this.GroupBy = groupBy;
this.DataRow = dataRow;
if (string.IsNullOrEmpty(elementName))
{
this.Level = ;
this.TableIndex = tableIndex;
this.Name = string.Empty;
this.ColumnNameForHead = false;
}
else
{
string[] element = elementName.Split('_');
this.Level = int.Parse(element[]);
this.ColumnNameForHead = false;
this.ColumnStart = -;
this.ColumnEnd = -; if (element[].Equals("label"))
{
this.Name = element[];
this.TableIndex = ;
}
else
{
this.Name = element[];
this.TableIndex = int.Parse(element[]) - ; if (!string.IsNullOrEmpty(element[]))
{
string[] filters = element[].Split(new string[] { "XX" }, StringSplitOptions.RemoveEmptyEntries);
if (this.GroupBy == null)
{
this.GroupBy = new Dictionary<string, string>();
}
foreach (string item in filters)
{
if (!this.GroupBy.Keys.Contains(item))
{
this.GroupBy.Add(item, dataRow[item].ToString());
} }
} if (element[].Equals("chart") && element.Count() > )
{
this.ColumnNameForHead = element[].Equals("");
this.ColumnStart = string.IsNullOrEmpty(element[]) ? - : int.Parse(element[]);
this.ColumnEnd = string.IsNullOrEmpty(element[]) ? - : int.Parse(element[]);
}
}
}
} public Word.Range Range { get; set; }
public int Level { get; set; }
public int TableIndex { get; set; }
public string ElementName { get; set; } public DataRow DataRow { get; set; }
public Dictionary<string, string> GroupBy { get; set; } public string Name { get; set; } public bool ColumnNameForHead { get; set; }
public int ColumnStart { get; set; }
public int ColumnEnd { get; set; } public string GroupByString
{
get
{
if (GroupBy == null || GroupBy.Count == )
{
return string.Empty;
} string rtn = string.Empty;
foreach (string key in this.GroupBy.Keys)
{
rtn += "and " + key + " = '" + GroupBy[key] + "' ";
}
return rtn.Substring();
}
} public static string GetName(string elementName)
{
string[] element = elementName.Split('_'); if (element[].Equals("label"))
{
return element[];
}
else
{
return element[];
}
}
} /// <summary>
/// Table配置项
/// </summary>
public class TableConfig
{
public TableConfig(string tableDescr = "")
{
this.DataRow = ;
this.SummaryRow = -; if (!string.IsNullOrEmpty(tableDescr))
{
string[] element = tableDescr.Split(',');
foreach (string item in element)
{
if (!string.IsNullOrEmpty(item))
{
string[] configs = item.Split(':');
if (configs.Length == )
{
switch (configs[].ToLower())
{
case "data":
case "d":
this.DataRow = int.Parse(configs[]);
break;
case "summary":
case "s":
this.SummaryRow = int.Parse(configs[]);
break;
case "summaryfilter":
case "sf":
this.SummaryFilter = configs[];
break;
default:
break;
}
}
}
}
} }
public int DataRow { get; set; }
public int SummaryRow { get; set; }
public string SummaryFilter { get; set; }
}
}