大家好,我是一名菜鸟工程师,这篇文章用来记录我工作遇到的需求及解决方式,如果有更好的解决方式,也欢迎大家提出,话不多说,赶快开始今天的教学吧。
需求
这次客户那边的需求是希望把某个用于呈现统计图表的页面加上一个汇出Word档的功能
,内容包含目前页面所呈现的表格及图片
使用的后端框架及套件
这次虽然用的是 ASP .NET Core 2.2 如果是用别的版本的应该没差,原理上差不多,稍微修改一下应该能用
图表部分当然就是HighChart.js
产生Word档这次用的是Novacode Docx装在ASP端,用NuGet装搜寻DocXCore
解决心路历程
首先,这个页面呈现图表的地方已经是做好的功能,简单描述一下作法就是:先透过linq group by资料包成好一个Model回传Json,然后透过Ajax的方式把Model的内容塞到HighChart提供的图的js中,有兴趣知道怎么做的在下方留言,我在做一篇专门来教学,这里就不多加赘述了。
这次的需求看似简单,但实际上有一段难度(对我来说 哈哈),主要的难度在于HighChart是透过js的方式Render出图片,因此在Html Code的里面不是一个img标籤,如果直接把这段Script丢到ASP中不可能产生出图片的。
因此我一开始的思路是这样的,既然资料Select是在ASP端,那就试试看能不能够在ASP端就产生图片,或是用NovaCode这个套件直接产生图片Insert到档案中。
但事与愿违,原因如下
1.HighChart的图太美了太强大了XD,客户那边无法接受画面跟产生出来的图片看起来不一样
2.Novacode Docx这个套件只提供一些简单的圆饼图、长条图、折线图,但没办法像HighChart一样有複合图形(EX:同时包含折线图,长条图)
3.我不太知道怎么用C#产生图片,也没办法产生像HighChart一样精美的图
OK既然这条路不通,那就是走下一条,那就是怎么真的把画面上的图片内容传到ASP端后能够产生一样的图片。
有用过HighChart套件的应该都知道HighChart有提供可以直接下载图片的功能在右上角的地方,如下图
或是在js中HighChart有提供export可以汇出图片,这边我就不介绍了,google大神有超多範例
大多数的範例都是放一个button然后在click事件的调用highchart.export()这个方法,然后实现图片下载的功能
不过这次是希望能够在C#端产生,所以我稍微得查了一下,发现highchart.export()主要透过https://export.highcharts.com/ 去产生图片
所以要在C#端产生图片,作法就是把highchart的script透过json的方式然后透过C#的WebRequest去进行Request跟Response然后存成图片或是Image 物件
这边我先去highchart下载一个範例,然后HTML只放这个图就好
HTML 部分
<div> <div id="chartTest"></div></div><!--------------------------引入套件---------------------------><script src="https://code.jquery.com/jquery-1.12.4.js" integrity="sha256-Qw82+bXyGq6MydymqBxNPYTaUXXq7c8v3CwiYwLLNXU=" crossorigin="anonymous"></script><script src="https://code.highcharts.com/highcharts.js"></script><!---------------------------主要程式---------------------------><script src="~/js/chartTest.js"></script>
由于Highchart有用到jQuery所以必须引入,highchart部分我是直接用cdn的方式,主要js,我放在chartTest.js中
chartTest.js内容
(function ($) { $(document).ready(function () { //highchart官方给的範例 var option = { title: { text: 'Solar Employment Growth by Sector, 2010-2016' }, subtitle: { text: 'Source: thesolarfoundation.com' }, yAxis: { title: { text: 'Number of Employees' } }, xAxis: { accessibility: { rangeDescription: 'Range: 2010 to 2017' } }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle' }, plotOptions: { series: { label: { connectorAllowed: false }, pointStart: 2010 } }, series: [{ name: 'Installation', data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175] }, { name: 'Manufacturing', data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434] }, { name: 'Sales & Distribution', data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387] }, { name: 'Project Development', data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227] }, { name: 'Other', data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111] }], responsive: { rules: [{ condition: { maxWidth: 500 }, chartOptions: { legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom' } } }] } }; //将option给chartTest产生图片 Highcharts.chart('chartTest', option); });}(jQuery));
结果
OK 接下来就是重点了,要怎么产生图片呢? 答案是透过HttpWebRequest跟HttpWebResponse
原理就是透过HttpWebRequest把chartTest中的option用json丢到https://export.highcharts.com/
然后再把Response回来的东西转换成Image,这样就能得到图片,再来就是把图片插入到Word中就完成了
完整程式码如下
javascript
(function ($) { $(document).ready(function () { var option = { title: { text: 'Solar Employment Growth by Sector, 2010-2016' }, subtitle: { text: 'Source: thesolarfoundation.com' }, yAxis: { title: { text: 'Number of Employees' } }, xAxis: { accessibility: { rangeDescription: 'Range: 2010 to 2017' } }, legend: { layout: 'vertical', align: 'right', verticalAlign: 'middle' }, plotOptions: { series: { label: { connectorAllowed: false }, pointStart: 2010 } }, series: [{ name: 'Installation', data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175] }, { name: 'Manufacturing', data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434] }, { name: 'Sales & Distribution', data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387] }, { name: 'Project Development', data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227] }, { name: 'Other', data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111] }], responsive: { rules: [{ condition: { maxWidth: 500 }, chartOptions: { legend: { layout: 'horizontal', align: 'center', verticalAlign: 'bottom' } } }] } }; Highcharts.chart('chartTest', option); var imageData = { data: option, width: false, scale: false, constr: "Test", type: "image/png", async: true }; var imageDataJson = JSON.stringify(imageData) + ""; $('#Test').on('click', function () { var imageData = []; imageData.push(imageDataJson); $.ajax({ type: 'POST', //timeout: 120000, beforeSend: function () { $.blockUI({ message: '<h1>档案产生中...</h1>' }) }, data: { imgDatas: imageData}, url: '/Home/ExportDocx/', success: function (data) { }, complete: function () { } }).done(function (data) { $.unblockUI(); //下载的url location.href = '/Home/HighChartDocxDownload?fileName=' + data.fileName; }); }); });}(jQuery));
这边的重点
1.不能把option直接丢给https://export.highcharts.com/ 而是要用imageData这个物件才行
2.页面部分我就新增一个button做为触发输出文件用,在ajax的部分我分成两部分第一次会透过/Home/ExportDocx/ 产生档案并暂存在资料夹内,再透过/Home/HighChartDocxDownload 把档案载下来
会这么做的主要原因是因为Asp端如果在Action 最后 return File () 在ajax会失效
3.Post Data部分 我是用把imageDataJson丢到一个阵列(imgDatas)中,这么做主要是想提供大家需要插入多张图片时的作法
ASP端
[HttpPost] public IActionResult ExportDocx(List<string> imgDatas) { try { //产生doc相关内容 var result = ExportDocxFile(imgDatas); //产生一个"测试.docx"在wwwrooot资料夹中 var _fullPath = Path.Combine($"{_hostingEnvironment.WebRootPath}", "测试.docx"); FileStream file = new FileStream(_fullPath, FileMode.Create, FileAccess.Write); result.Position = 0; result.WriteTo(file);//储存档案 file.Close(); var resultObj = new { FileName = $@"测试", Successful = true }; return Json(resultObj);//回传档名 } catch (Exception ex) { return Content(ex.Message); } // return View(); }
稍微解释一下
这边的主要是藉由ExportDocxFile这个function产生相关docx内容,然后先暂存一份到wwwroot中,回传档名,让等一下下载用
public MemoryStream ExportDocxFile(List<string> imgDatas) { using (var document = DocX.Load($@"{_hostingEnvironment.WebRootPath}/ChartTest.docx")) { var imageTableIdx = 1;//定义第一张图的表格位置 var failImage = new List<int>(); using (var ms = new MemoryStream()) { var mmsg = ""; var imageTable = document.Tables[0];// foreach (var imgData in imgDatas) { using (var Img = GenerateHighChartImage(imgData, ref mmsg)) { if (Img != null) { Img.Save(ms, ImageFormat.Png);//Save your picture in a memory stream. ms.Seek(0, SeekOrigin.Begin); Novacode.Image img = document.AddImage(ms); //Paragraph p = document.InsertParagraph("Hello", false); Picture pic1 = img.CreatePicture(); var row = imageTable.Rows[0]; row.MergeCells(0, 1); var MaxWidth = row.Cells[0].Width; var ratio = MaxWidth / Img.Width; var width = Math.Round((double)Img.Width * 0.35); var height = Math.Round((double)Img.Height * 0.35); row.Cells[0].Paragraphs[0].Alignment = Alignment.center; row.Cells[0].Paragraphs[0].InsertPicture(pic1, 0); row.Cells[0].VerticalAlignment = VerticalAlignment.Center; // imageTable.Alignment = Alignment.center; //imageTableIdx += 2; } } } } var memory = new MemoryStream(); document.SaveAs(memory); return memory; //return FileTool.ConvertDocToMemoryStream(document); } }
这边就是产生docx内容的程式码了,我这边先读取ChartTest这个docx,透过GenerateHighChartImage这个Function产生图片,那在这个docx套件塞图片的方式不是简单的用 "img = document.AddImage(ms);"就好了
必须用CreatePicture的方式才能把图片塞进去。
至于图片放的位置我这边用的方式是抓table,因为在Novacode这个套件可以抓到文件中有几个table
所以可以用table来指定图片要放的位置,例如我今天想要把图片放在某段文字之后,这个时候可以在文字后面放一个空的table,放图片的时候就可以抓这个table把图放进去,下面是我的ChartTest内容
这边可以注意到table部分我不是用单行单列的table,原因主要是如果使用单行单列的table好像会出错,所以我才这么做
那些下来是最后了,产生图片的code
public System.Drawing.Image GenerateHighChartImage(string option, ref string mmsg) { option = option.Replace("\r\n", ""); option = option.Replace(" ", ""); var request = (HttpWebRequest)WebRequest.Create("https://export.highcharts.com/"); System.Drawing.Image ResultImg; var bytes = Encoding.UTF8.GetBytes(option); //因应关闭TLS1.0与TLS1.2 ServicePointManager.SecurityProtocol = (SecurityProtocolType)192 | (SecurityProtocolType)768 | (SecurityProtocolType)3072; request.Method = "POST"; request.ContentType = "application/json;charset=utf-8"; request.ContentLength = bytes.Length; //var mmsg = ""; var ErrorMessage = ""; try { request.GetRequestStream().Write(bytes, 0, bytes.Length); //var ResultImg = new System.Drawing.Image(); using (var response = (HttpWebResponse)request.GetResponse()) { mmsg = new StreamReader(response.GetResponseStream(), Encoding.UTF8).ReadToEnd(); if (!string.IsNullOrEmpty(mmsg)) { var requestPic = WebRequest.Create("https://export.highcharts.com/" + mmsg); using (WebResponse responsePic = requestPic.GetResponse()) { using (var webImage = System .Drawing .Image .FromStream(responsePic.GetResponseStream())) { ResultImg = (System.Drawing.Image)webImage.Clone(); return ResultImg; } { ResultImg = (System.Drawing.Image)webImage.Clone(); return ResultImg; } } } } } catch (Exception exception1) { //ProjectData.SetProjectError(exception1); //Exception exception = exception1; ErrorMessage = exception1.Message; // ProjectData.ClearProjectError(); } ResultImg = null; mmsg = ErrorMessage; return ResultImg; }
这边的code原理很简单就是透过HttpWebRequest跟WebResponse拿到图片,主要都是丢到https://export.highcharts.com 这里去进行转换的动作
那最后就是下载的部分
public IActionResult HighChartDocxDownload(string fileName){ using (var document = DocX.Load($@"{_hostingEnvironment.WebRootPath}/{fileName}.docx")) { var memory = new MemoryStream(); document.SaveAs(memory); memory.Position = 0; return File(memory.ToArray(), "application/vnd.openxmlformats-officedocument.wordprocessingml.document", $"测试2.docx"); } }
产生的结果
心得
这次的需求花了我一段时间才完成,由于我刚进入这个行业不久,很多东西都在摸索,如果有什么地方需要改进,也请大家指导,这部分的程式码我会放在我的github上,如果有兴趣的人可以下载来试看看,谢谢大家
github网址: https://github.com/paul09253336/GenerateHighChartImageAndInertToDocx_project