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