疲惫的修改
应人才测评产品的需求,导出测评报告是其中一个重要的环节,报告的文件类型也多种多样,其中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、提升设计能力、业务处理能力和总结学习方法尤为关键。
以上就是自己一些体会,时间仓促,不妥之处还请大家批评指正讨论,程序员节就要到啦,祝咱们永远保持年轻的心,健康的心态,用智慧编写美好的人生!