疲惫的修改
应人才测评产品的需求,导出测评报告是其中一个重要的环节,报告的文件类型也多种多样,其中WORD输出也扮演了一个重要的角色。
实现方法比较简单,结合分析结果数据,通过WORD模板文件进行替换输出。在实现的过程中,图表的设计是必不可少的,根据初次产品的设计方案,图表采用微软Chart图表控件进行开发,采用雷达图进行呈现。使用该控件首先要引入 System.Web.DataVisualization.dll 程序集,通过定义 System.Web.UI.DataVisualization.Charting.Chart 类来实现,本来采用该开发方案的初衷是觉得都是微软的技术,图表的呈现类型也比较丰富,可在实际的开发中,情况没有想像的那么顺利,提供的技术文档非常有限,各种百度也是凤毛麟角,经过努力与探索,最终还是实现了需求。
但后来由于种种原因,图表要求采用饼状3D图进行呈现,虽然已经有了第一次的经验,但细节的变化,不得不再次进行探索和学习,可当需求再次改变的时候,我决定游说产品设计和改变设计思路。
新的思路
由于引入 Microsoft.Office.Interop.Word 程序集进行开发,因此在Word上的所有操作都能用程序去实现,其内置的图表功能也不例外,通过演练和内部讨论,图形化的呈现基本能够满足需求。
通用性
举例,我们在Word中插入一个图表并选择雷达图,如下图:
插入后,我们看到 Word 会自动弹出一个微缩版的 Excel 应用,改变其中的项和系列值,图表就会对应的产生变化。
我们右击雷达图,选择更改图表类型为饼图,如下图:
可以看到饼图按照EXCEL数据中的系列1数据进行呈现,也不会因为系列2的数据存在而出现错误。由此可以分析出,控制好这个 Excel 的数据应用即可按照我们的设计实现任一图表的输出。
设计方案
(1)负责具体业务的应用程序,输出后的数据,存入一个二维字符串数组里,模拟 Excel 数据存储模式。
(2)考虑未来的扩展性,将二维数组转化为Json数据格式,并添加一个查找关键字节点,假设为“ t:chart1”。
(3)在 Word 模板设计图表,图表的标题设置为Json对应的查找关键字,即“ t:chart1”。
(4)编写数据导出EXCEL方法,传递JSON字符串参数,读取Word模板文件,遍历模板文件中的图表对象,并按查找关键字与图表的标题进行对比,匹配成功,则将JSON中数组转化为图表需要的EXCEL数组形式,到此输出完毕。
为什么用 Json 过渡
我们的云架构里设计了一个 Office 计算中心,在某些环境下,比如 Linux 中需要这种方式传递并返回值,以达到导入导出Office文件的目的。所以大家要根据实际的应用进行设计,这里仅作为参考。
关键代码实现
开发环境
操作系统:Windows Server 2019 DataCenter
开发工具:VisualStudio2019
框架及语言:.net 4.7.1 C#
服务上需要安装 Office 2016或以上
现在开始!
在此我们以最易懂的代码形式举例,假设文件模板中的图表为条状图,关键查找字(图表标题)设为 “ t:chart1”,如下图:
(1)创建二维数组
//定义二维字符串数组,第一列为项目名称,第二列为值 string[,] chart1 = new string[11, 2]; chart1[0, 0] = "项"; chart1[0, 1] = "值"; chart1[1, 0] = "全局观"; chart1[2, 0] = "影响力"; chart1[3, 0] = "公正性"; chart1[4, 0] = "果敢性"; chart1[5, 0] = "执行力"; chart1[6, 0] = "人际理解"; chart1[7, 0] = "成就意识"; chart1[8, 0] = "创新意识"; chart1[9, 0] = "情绪控制"; chart1[10, 0] = "学习发展"; Random rnd = new Random(); for (int si = 1; si <= 10; si++) { chart1[si, 0] = rnd.NextDouble().ToString(); //循环赋值随机浮点数 }
(2)二维数组转Json格式
这里引入 Newtonsoft.Json.dll 程序集进行操作,代码如下:
StringWriter sw = new StringWriter(); //using System.IO using (Newtonsoft.Json.JsonWriter writer = new Newtonsoft.Json.JsonTextWriter(sw)) { writer.Formatting = Newtonsoft.Json.Formatting.Indented; writer.WriteStartObject(); //t:chart1 转化数组chart1 为 json 对象 writer.WritePropertyName("t:chart1"); writer.WriteStartArray(); writer.WriteStartObject(); writer.WritePropertyName("col1"); writer.WriteValue(chart1[0, 0]); writer.WritePropertyName("col2"); writer.WriteValue(chart1[0, 1]); writer.WriteEndObject(); for (int r = chart1.GetLength(0) - 1; r > 0; r--) { writer.WriteStartObject(); //循环写入列2的具体值 for (int c = 0; c < 2; c++) { writer.WritePropertyName("col" + (c + 1).ToString()); writer.WriteValue(chart1[r, c]); } writer.WriteEndObject(); } writer.WriteEndArray(); //t:chart1 writer.WriteEndObject(); writer.Flush(); } sw.Close(); string jsonContent = sw.GetStringBuilder().ToString(); //得到最终json字串
转化成功的样例如下:
{ "t:chart1": [ { "col1": "项", "col2": "值" }, { "col1": "学习发展", "col2": "4.1" }, { "col1": "情绪控制", "col2": "5" }, { "col1": "创新意识", "col2": "5.1" }, { "col1": "成就意识", "col2": "4.8" }, { "col1": "人际理解", "col2": "4" }, { "col1": "执行力", "col2": "5" }, { "col1": "果敢性", "col2": "5.7" }, { "col1": "公正性", "col2": "4.5" }, { "col1": "影响力", "col2": "4.7" }, { "col1": "全局观", "col2": "4.2" } ] }
(3)查找图表且替换数据
本代码程序只是示例片断,非完整程序,仅供参考。
一些引用 using Word=Microsoft.Office.Interop.Word; using Newtonsoft.Json.Linq; 转换 json 字符串为 json 对象 Newtonsoft.Json.Linq.JObject jObject = null; if (jsonContent != "") { try { jObject = Newtonsoft.Json.Linq.JObject.Parse(jsonContent); //转换为json对象 } catch (Exception e) { resultReport += "create json object fail.<br>"; //失败记入调试报告 } } 初始化 Word 应用程序 Word.Application WordApp=new Word.Application(); //创建一个名为WordDoc的文档对象 WordApp.DisplayAlerts=Word.WdAlertLevel.wdAlertsNone; //禁止一切提示警告 //打开 filename 的文件 Word.Document WordDoc=WordApp.Documents.Open(ref filename,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing,ref Nothing); //禁用拼写检查 WordDoc.SpellingChecked = false; WordDoc.ShowSpellingErrors = false; 遍历word 里的 shapes for (int i = 1; i <= WordDoc.InlineShapes.Count; i++) { Word.InlineShape shape = WordDoc.InlineShapes[i]; //得到 shape对象 //遍历 json 对象 foreach (var item in jObject) { string tcmd = item.Key.ToString(); //取关键字 //如果 shape 包含图表,则继续 if (shape.HasChart == Microsoft.Office.Core.MsoTriState.msoTrue) { //如果图表未设置标题,则短路 if (shape.Chart.HasTitle == false) { continue; } //获取图表的title string _name = shape.Chart.ChartTitle.Text.Trim().ToLower(); if (_name.IndexOf(tcmd) != -1) //如果包含关键字则继续 { //替换掉关键字,保留下来的是真正的标题 shape.Chart.ChartTitle.Text = shape.Chart.ChartTitle.Text.Replace(tcmd, ""); //这是一个玄机,否则会报错,目前我是这样的解决,A1:Z100,先赋值为空串 shape.Chart.ChartData.Workbook.Worksheets[1].Range("A1:Z100").Value = ""; //计算最后的单元格地址 string lastcellAddress = "$" + ((char)(64 + jObject[tcmd][0].Count())).ToString() + "$" + jObject[tcmd].Count().ToString(); //获得最终地址字串 string sourceDataAddress = "='Sheet1'!$A$1:" + lastcellAddress; //遍历json对象节点里的数组 for (int i = 0; i < jObject[tcmd].Count(); i++) { List<JToken> tokens = jObject[tcmd][i].ToList(); int k = 0; foreach (JToken jToken in tokens) { //为每一个单元格赋值 string celladdress = ((char)(65 + k)).ToString() + (i + 1).ToString(); shape.Chart.ChartData.Workbook.Worksheets[1].Range(celladdress).Value = jToken.ToArray()[0].ToString(); k++; } } shape.Chart.SetSourceData(sourceDataAddress); //设置更新图表的数据源 break; } // index of name } // has chart }//foreach tcmd } //WordDoc.InlineShapes
小结
通过这种设计可以实现任意更换图表的类型,基本无需关注图表的实现原理,而让开发人员更多的关注于业务逻辑,当然这些图表的种类受限于Word的提供能力,如果能够满足需求,不失为一种解决思路。另外,我们可以继续扩展程序的功能,实现动态的图表添加或切换能力等。
一些体会
作为一名全程管理加全栈开发的 “野战军”,更多的时候考虑的是满足需求、稳定功能和控制各种成本,而无法深入地研究各项领域。随着年龄的增长,唯一能做到的就是业务经验弥补精力和学习时间的不足,还是有几点体会与大家共勉吧:
1、后悔学生时代没有端正态度和认识到认真学习的重要性,所谓书到用时方恨少,熟练掌握修炼数据结构与算法、数学等程序员的内功至关重要。
2、语言只是一种模型和工具而已,可先从一门语言入手到实际应用,抽象的来看所有语言总体上都是大同小异,后来会觉得学习一门新语言是一件非常有趣的事。
3、时间允许的情况下还是要深入掌握一些底层的技术开发和原理,这至少是一件非常有趣的事。
4、在工作中平衡最为关键,也包括人,换位思考很重要。
5、提升设计能力、业务处理能力和总结学习方法尤为关键。
以上就是自己一些体会,时间仓促,不妥之处还请大家批评指正讨论,程序员节就要到啦,祝咱们永远保持年轻的心,健康的心态,用智慧编写美好的人生!