将很早之前写的一个小组件重新整理优化一下,做成一个通用的功能。适用于导出数据库的结构(表、字段等)到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("ra", ""); 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 { 261 columns.add(i); 262 } 263 } 264 } 265 columns.sort(); 266 columncount = columns.count; 267 int rowscount = datarows.length; 268 269 word.chartdata chartdata = chart.chartdata; 270 271 //chartdata.activate(); 272 //此处有个比较疑惑的问题, 不执行此条,生成的报告中的图表无法再次右键编辑数据. 执行后可以, 但有两个问题就是第一会弹出excel框, 处理完后会自动关闭. 第二部分chart的数据range设置总不对 273 //不知道是不是版本的问题, 谁解决了分享一下,谢谢 274 275 excel.workbook dataworkbook = (excel.workbook)chartdata.workbook; 276 dataworkbook.application.visible = false; 277 278 excel.worksheet datasheet = (excel.worksheet)dataworkbook.worksheets[1]; 279 //设定范围 280 string a = (chartelement.columnnameforhead ? rowscount + 1 : rowscount) + "|" + columncount; 281 console.writeline(a); 282 283 excel.range trange = datasheet.range["a1", datasheet.cells[(chartelement.columnnameforhead ? rowscount + 1 : rowscount), columncount]]; 284 excel.listobject tbl1 = datasheet.listobjects[1]; 285 //datasheet.listobjects[1].delete(); //想过重新删除再添加 这样 原有数据清掉了, 但觉得性能应该会有所下降 286 //excel.listobject tbl1 = datasheet.listobjects.addex(); 287 tbl1.resize(trange); 288 for (int j = 0; j < rowscount; j++) 289 { 290 datarow row = datarows[j]; 291 for (int k = 0; k < columncount; k++) 292 { 293 datasheet.cells[j + 2, k + 1].formular1c1 = row[columns[k]]; 294 } 295 } 296 297 if (chartelement.columnnameforhead) 298 { 299 for (int k = 0; k < columns.count; k++) 300 { 301 datasheet.cells[1, k + 1].formular1c1 = datatable.columns[columns[k]].columnname; 302 } 303 } 304 chart.charttitle.text = chartelement.name; 305 //datasheet.application.quit(); 306 } 307 } 308 } 309 310 return true; 311 } 312 313 /// <summary> 314 /// 更新目录 315 /// </summary> 316 /// <returns></returns> 317 private bool updatetablesofcontents() 318 { 319 foreach (word.tableofcontents item in worddoc.tablesofcontents) 320 { 321 item.update(); 322 } 323 324 return true; 325 } 326 327 /// <summary> 328 /// 保存文件 329 /// </summary> 330 /// <param name="newfilepath"></param> 331 /// <param name="newfilename"></param> 332 /// <param name="saveformat"></param> 333 /// <returns></returns> 334 private bool savefile(string newfilepath, ref string newfilename, int saveformat = 16) 335 { 336 if (string.isnullorempty(newfilename)) 337 { 338 newfilename = datetime.now.tostring("yyyymmddhhmmss"); 339 340 switch (saveformat) 341 { 342 case 0:// word.wdsaveformat.wdformatdocument 343 newfilename += ".doc"; 344 break; 345 case 16:// word.wdsaveformat.wdformatdocumentdefault 346 newfilename += ".docx"; 347 break; 348 case 17:// word.wdsaveformat.wdformatpdf 349 newfilename += ".pdf"; 350 break; 351 default: 352 break; 353 } 354 } 355 356 object newfile = path.combine(newfilepath, newfilename); 357 object wdsaveformat = saveformat; 358 worddoc.saveas(ref newfile, ref wdsaveformat); 359 return true; 360 } 361 362 /// <summary> 363 /// 清理 364 /// </summary> 365 private void closeandclear() 366 { 367 if (wordapp == null) 368 { 369 return; 370 } 371 worddoc.close(word.wdsaveoptions.wddonotsavechanges); 372 wordapp.quit(word.wdsaveoptions.wddonotsavechanges); 373 system.runtime.interopservices.marshal.releasecomobject(worddoc); 374 system.runtime.interopservices.marshal.releasecomobject(wordapp); 375 worddoc = null; 376 wordapp = null; 377 gc.collect(); 378 killprocess("excel", "winword"); 379 } 380 381 /// <summary> 382 /// 杀进程.. 383 /// </summary> 384 /// <param name="processnames"></param> 385 private void killprocess(params string[] processnames) 386 { 387 //process myproc = new process(); 388 //得到所有打开的进程 389 try 390 { 391 foreach (string name in processnames) 392 { 393 foreach (process thisproc in process.getprocessesbyname(name)) 394 { 395 if (!thisproc.closemainwindow()) 396 { 397 if (thisproc != null) 398 thisproc.kill(); 399 } 400 } 401 } 402 } 403 catch (exception) 404 { 405 //throw exc; 406 // msg.text+= "杀死" + processname + "失败!"; 407 } 408 } 409 } 410 411 /// <summary> 412 /// 封装的word元素 413 /// </summary> 414 public class wordelement 415 { 416 public wordelement(word.range range, string elementname = "", datarow datarow = null, dictionary<string, string> groupby = null, int tableindex = 0) 417 { 418 this.range = range; 419 this.elementname = elementname; 420 this.groupby = groupby; 421 this.datarow = datarow; 422 if (string.isnullorempty(elementname)) 423 { 424 this.level = 0; 425 this.tableindex = tableindex; 426 this.name = string.empty; 427 this.columnnameforhead = false; 428 } 429 else 430 { 431 string[] element = elementname.split('_'); 432 this.level = int.parse(element[1]); 433 this.columnnameforhead = false; 434 this.columnstart = -1; 435 this.columnend = -1; 436 437 if (element[0].equals("label")) 438 { 439 this.name = element[2]; 440 this.tableindex = 0; 441 } 442 else 443 { 444 this.name = element[4]; 445 this.tableindex = int.parse(element[2]) - 1; 446 447 if (!string.isnullorempty(element[3])) 448 { 449 string[] filters = element[3].split(new string[] { "xx" }, stringsplitoptions.removeemptyentries); 450 if (this.groupby == null) 451 { 452 this.groupby = new dictionary<string, string>(); 453 } 454 foreach (string item in filters) 455 { 456 if (!this.groupby.keys.contains(item)) 457 { 458 this.groupby.add(item, datarow[item].tostring()); 459 } 460 461 } 462 } 463 464 if (element[0].equals("chart") && element.count() > 5) 465 { 466 this.columnnameforhead = element[5].equals("1"); 467 this.columnstart = string.isnullorempty(element[6]) ? -1 : int.parse(element[6]); 468 this.columnend = string.isnullorempty(element[7]) ? -1 : int.parse(element[7]); 469 } 470 } 471 } 472 } 473 474 public word.range range { get; set; } 475 public int level { get; set; } 476 public int tableindex { get; set; } 477 public string elementname { get; set; } 478 479 public datarow datarow { get; set; } 480 public dictionary<string, string> groupby { get; set; } 481 482 public string name { get; set; } 483 484 public bool columnnameforhead { get; set; } 485 public int columnstart { get; set; } 486 public int columnend { get; set; } 487 488 public string groupbystring 489 { 490 get 491 { 492 if (groupby == null || groupby.count == 0) 493 { 494 return string.empty; 495 } 496 497 string rtn = string.empty; 498 foreach (string key in this.groupby.keys) 499 { 500 rtn += "and " + key + " = '" + groupby[key] + "' "; 501 } 502 return rtn.substring(3); 503 } 504 } 505 506 public static string getname(string elementname) 507 { 508 string[] element = elementname.split('_'); 509 510 511 if (element[0].equals("label")) 512 { 513 return element[2]; 514 } 515 else 516 { 517 return element[4]; 518 } 519 } 520 } 521 522 /// <summary> 523 /// table配置项 524 /// </summary> 525 public class tableconfig 526 { 527 public tableconfig(string tabledescr = "") 528 { 529 this.datarow = 2; 530 this.summaryrow = -1; 531 532 if (!string.isnullorempty(tabledescr)) 533 { 534 string[] element = tabledescr.split(','); 535 foreach (string item in element) 536 { 537 if (!string.isnullorempty(item)) 538 { 539 string[] configs = item.split(':'); 540 if (configs.length == 2) 541 { 542 switch (configs[0].tolower()) 543 { 544 case "data": 545 case "d": 546 this.datarow = int.parse(configs[1]); 547 break; 548 case "summary": 549 case "s": 550 this.summaryrow = int.parse(configs[1]); 551 break; 552 case "summaryfilter": 553 case "sf": 554 this.summaryfilter = configs[1]; 555 break; 556 default: 557 break; 558 } 559 } 560 } 561 } 562 } 563 564 } 565 public int datarow { get; set; } 566 public int summaryrow { get; set; } 567 public string summaryfilter { get; set; } 568 } 569 }