首页 > Python基础教程 >
-
C#教程之Word报告自动生成(例如 导出数据库结构
将很早之前写的一个小组件重新整理优化一下,做成一个通用的功能。适用于导出数据库的结构(表、字段等)到Word或将体检数据自动生成Word版的体检报告等。代码:Github
一、主要需要完成功能:
1. 灵活的配置规则及word样式设置(文本、表格、图表、颜色等).
2. 支持表格.
3. 支持图表.
4. 支持章节内容循环生成.
5. 支持目录.
6.支持文档结构图
7.更新指定位置的文字
8.支持pdf导出.
最后结果如下:
图一
图二
图三
二、需求分析与实现方式
功能主要涉及3个比较重要的部分:数据源、Word样式、配置规则。
为了简单,数据源决定采用一个存储过程返回Dataset的方式, 整张报告的数据来源于此Dataset的多个Datatable.
样式与配置:首先想到的是写一个config文件,所有配置都放到一个文件里,然后将数据按照这个规则生成word。但无疑这样的配置项太多了,关键是“样式”问题,比如字体、颜色、表格宽度.....想想就头大。而且没有“所见即所得”的效果,配置完都不知道啥样。
后来决定采取修改的方式, 先以一个word文件作为模板,在模板中定义好上面提到的“样式”,然后在模板中做一个个标记,然后将数据按照规则更新到对应的标记。
图四
而这个标记我们用到了word的一个叫【书签】的功能,打开word按ctrl+shift+F5, 打开书签功能,如下图:
图五
这样将【规则】通过一系列规则的【书签】定义到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.表格
表格的配置原本也想对表格添加书签,后来发现有个表格属性, 觉得写在这里更好一些。
如上图所示,【标题】格式为: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)_截止列,
如下图所示配置即可。
E.目录
无需标识, 模板中添加目录, 当内容处理完成之后, 会根据处理后的结果动态更新目录.
四、主要代码
1 using System; 2 using System.Collections.Generic; 3 using System.Data; 4 using System.Diagnostics; 5 using System.IO; 6 using System.Linq; 7 using System.Reflection; 8 using Excel = Microsoft.Office.Interop.Excel; 9 using Word = Microsoft.Office.Interop.Word; 10 11 namespace FlyLolo.WordReport.Demo 12 { 13 public class WordReportHelper 14 { 15 private Word.Application wordApp = null; 16 private Word.Document wordDoc = null; 17 private DataSet dataSource = null; 18 private object line = Word.WdUnits.wdLine; 19 private string errorMsg = ""; 20 21 /// <summary> 22 /// 根据模板文件,创建数据报告 23 /// </summary> 24 /// <param name="templateFile">模板文件名(含路径)</param> 25 /// <param name="newFilePath">新文件路径)</param> 26 /// <param name="dataSource">数据源,包含多个datatable</param> 27 /// <param name="saveFormat">新文件格式:</param> 28 public bool CreateReport(string templateFile, DataSet dataSource, out string errorMsg, string newFilePath, ref string newFileName, int saveFormat = 16) 29 { 30 this.dataSource = dataSource; 31 errorMsg = this.errorMsg; 32 bool rtn = OpenTemplate(templateFile) 33 && SetContent(new WordElement(wordDoc.Range(), dataRow: dataSource.Tables[dataSource.Tables.Count - 1].Rows[0])) 34 && UpdateTablesOfContents() 35 && SaveFile(newFilePath, ref newFileName, saveFormat); 36 37 CloseAndClear(); 38 return rtn; 39 } 40 41 /// <summary> 42 /// 打开模板文件 43 /// </summary> 44 /// <param name="templateFile"></param> 45 /// <returns></returns> 46 private bool OpenTemplate(string templateFile) 47 { 48 if (!File.Exists(templateFile)) 49 { 50 return false; 51 } 52 53 wordApp = new Word.Application(); 54 wordApp.Visible = true;//使文档可见,调试用 55 wordApp.DisplayAlerts = Word.WdAlertLevel.wdAlertsNone; 56 object file = templateFile; 57 wordDoc = wordApp.Documents.Open(ref file, ReadOnly: false); 58 return true; 59 } 60 61 /// <summary> 62 /// 为指定区域写入数据 63 /// </summary> 64 /// <param name="element"></param> 65 /// <returns></returns> 66 private bool SetContent(WordElement element) 67 { 68 string currBookMarkName = string.Empty; 69 string startWith = "loop_" + (element.Level + 1).ToString() + "_"; 70 foreach (Word.Bookmark item in element.Range.Bookmarks) 71 { 72 currBookMarkName = item.Name; 73 74 if (currBookMarkName.StartsWith(startWith) && (!currBookMarkName.Equals(element.ElementName))) 75 { 76 SetLoop(new WordElement(item.Range, currBookMarkName, element.DataRow, element.GroupBy)); 77 } 78 79 } 80 81 SetLabel(element); 82 83 SetTable(element); 84 85 SetChart(element); 86 87 return true; 88 } 89 90 /// <summary> 91 /// 处理循环 92 /// </summary> 93 /// <param name="element"></param> 94 /// <returns></returns> 95 private bool SetLoop(WordElement element) 96 { 97 DataRow[] dataRows = dataSource.Tables[element.TableIndex].Select(element.GroupByString); 98 int count = dataRows.Count(); 99 element.Range.Select(); 100 101 //第0行作为模板 先从1开始 循环后处理0行; 102 for (int i = 0; i < count; i++) 103 { 104 105 element.Range.Copy(); //模板loop复制 106 wordApp.Selection.InsertParagraphAfter();//换行 不会清除选中的内容,TypeParagraph 等同于回车,若当前有选中内容会被清除. TypeParagraph 会跳到下一行,InsertParagraphAfter不会, 所以movedown一下. 107 wordApp.Selection.MoveDown(ref line, Missing.Value, Missing.Value); 108 wordApp.Selection.Paste(); //换行后粘贴复制内容 109 int offset = wordApp.Selection.Range.End - element.Range.End; //计算偏移量 110 111 //复制书签,书签名 = 模板书签名 + 复制次数 112 foreach (Word.Bookmark subBook in element.Range.Bookmarks) 113 { 114 if (subBook.Name.Equals(element.ElementName)) 115 { 116 continue; 117 } 118 119 wordApp.Selection.Bookmarks.Add(subBook.Name + "_" + i.ToString(), wordDoc.Range(subBook.Start + offset, subBook.End + offset)); 120 } 121 122 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)); 123 } 124 125 element.Range.Delete(); 126 127 return true; 128 } 129 130 /// <summary> 131 /// 处理简单Label 132 /// </summary> 133 /// <param name="element"></param> 134 /// <returns></returns> 135 private bool SetLabel(WordElement element) 136 { 137 if (element.Range.Bookmarks != null && element.Range.Bookmarks.Count > 0) 138 { 139 string startWith = "label_" + element.Level.ToString() + "_"; 140 string bookMarkName = string.Empty; 141 foreach (Word.Bookmark item in element.Range.Bookmarks) 142 { 143 bookMarkName = item.Name; 144 145 if (bookMarkName.StartsWith(startWith)) 146 { 147 bookMarkName = WordElement.GetName(bookMarkName); 148 149 item.Range.Text = element.DataRow[bookMarkName].ToString(); 150 } 151 } 152 } 153 154 return true; 155 } 156 157 /// <summary> 158 /// 填充Table 159 /// </summary> 160 /// <param name="element"></param> 161 /// <returns></returns> 162 private bool SetTable(WordElement element) 163 { 164 if (element.Range.Tables != null && element.Range.Tables.Count > 0) 165 { 166 string startWith = "table_" + element.Level.ToString() + "_"; 167 foreach (Word.Table table in element.Range.Tables) 168 { 169 if (!string.IsNullOrEmpty(table.Title) && table.Title.StartsWith(startWith)) 170 { 171 WordElement tableElement = new WordElement(null, table.Title, element.DataRow); 172 173 TableConfig config = new TableConfig(table.Descr); 174 175 object dataRowTemplate = table.Rows[config.DataRow]; 176 Word.Row SummaryRow = null; 177 DataRow SummaryDataRow = null; 178 DataTable dt = dataSource.Tables[tableElement.TableIndex]; 179 DataRow[] dataRows = dataSource.Tables[tableElement.TableIndex].Select(tableElement.GroupByString); ; 180 181 if (config.SummaryRow > 0) 182 { 183 SummaryRow = table.Rows[config.SummaryRow]; 184 SummaryDataRow = dt.Select(string.IsNullOrEmpty(tableElement.GroupByString) ? config.SummaryFilter : tableElement.GroupByString + " and " + config.SummaryFilter).FirstOrDefault(); 185 } 186 187 foreach (DataRow row in dataRows) 188 { 189 if (row == SummaryDataRow) 190 { 191 continue; 192 } 193 194 Word.Row newRow = table.Rows.Add(ref dataRowTemplate); 195 for (int j = 0; j < table.Columns.Count; j++) 196 { 197 newRow.Cells[j + 1].Range.Text = row[j].ToString(); ; 198 } 199 200 } 201 202 ((Word.Row)dataRowTemplate).Delete(); 203 204 if (config.SummaryRow > 0 && SummaryDataRow != null) 205 { 206 for (int j = 0; j < SummaryRow.Cells.Count; j++) 207 { 208 string temp = SummaryRow.Cells[j + 1].Range.Text.Trim().Replace("\r\a", ""); 209 210 if (!string.IsNullOrEmpty(temp) && temp.Length > 2 && dt.Columns.Contains(temp.Substring(1, temp.Length - 2))) 211 { 212 SummaryRow.Cells[j + 1].Range.Text = SummaryDataRow[temp.Substring(1, temp.Length - 2)].ToString(); 213 } 214 } 215 } 216 217 table.Title = tableElement.Name; 218 } 219 220 221 } 222 } 223 224 return true; 225 } 226 227 /// <summary> 228 /// 处理图表 229 /// </summary> 230 /// <param name="element"></param> 231 /// <returns></returns> 232 private bool SetChart(WordElement element) 233 { 234 if (element.Range.InlineShapes != null && element.Range.InlineShapes.Count > 0) 235 { 236 List<Word.InlineShape> chartList = element.Range.InlineShapes.Cast<Word.InlineShape>().Where(m => m.Type == Word.WdInlineShapeType.wdInlineShapeChart).ToList(); 237 string startWith = "chart_" + element.Level.ToString() + "_"; 238 foreach (Word.InlineShape item in chartList) 239 { 240 Word.Chart chart = item.Chart; 241 if (!string.IsNullOrEmpty(chart.ChartTitle.Text) && chart.ChartTitle.Text.StartsWith(startWith)) 242 { 243 WordElement chartElement = new WordElement(null, chart.ChartTitle.Text, element.DataRow); 244 245 DataTable dataTable = dataSource.Tables[chartElement.TableIndex]; 246 DataRow[] dataRows = dataTable.Select(chartElement.GroupByString); 247 248 int columnCount = dataTable.Columns.Count; 249 List<int> columns = new List<int>(); 250 251 foreach (var dr in dataRows) 252 { 253 for (int i = chartElement.ColumnStart == -1 ? 0 : chartElement.ColumnStart - 1; i < (chartElement.ColumnEnd == -1 ? columnCount : chartElement.ColumnEnd); i++) 254 { 255 if (columns.Contains(i) || dr[i] == null || string.IsNullOrEmpty(dr[i].ToString())) 256 { 257 258 } 259 else 260