简介:将Delphi与UniGUI框架及百度ECharts图表库结合,实现服务端生成数据并在Web端可视化的完整示例。资源面向熟悉VCL或希望扩展Web开发的Delphi程序员,演示了从HTTP请求接收、读取txt数据文件到封装ECharts配置并渲染图表的全流程。压缩包共21个文件,约21KB,其中9个txt提供数据样例与配置选项,3组pas/dfm构建UniGUI服务端与窗体逻辑,js脚本定义ECharts交互样式,dpr/dproj为项目工程文件,结构清晰便于逐模块拆解。已有701人学习,适合希望快速上手Delphi Web数据可视化、或需在现有系统中嵌入图表的开发者参考。通过该示例可掌握UniGUI服务器模块编写、前后端数据交互及ECharts配置方法,并可延伸至实时监控、数据分析等场景。 开头:
最近手里接了个老项目,要把Delphi写的ERP统计界面升级成Web版。团队选型时纠结了很久,最后定了UniGUI作为Web框架,图表部分引入了ECharts。随手把整个集成方案打了个包,就是网盘里那个“Delphi-unigui-echart.rar”。如果你也在用Delphi做B/S架构,想在UniGUI页面里画出折线图、甘特图、移动端自适应图表,那这个包正好能对上需求。我先把完整思路和踩过的坑写出来,方便你直接参照落地。
1. 整体思路:UniGUI和ECharts是怎么凑到一起的
1.1 UniGUI的界面机制和ECharts的定位
UniGUI本质上是一套基于ExtJS的RAD框架,让你用Delphi的VCL思维写Web页面。服务端跑的是Delphi代码,前端页面由UniGUI自动生成,窗体、按钮、表格都被封装成了组件。但封装归封装,它对自定义JavaScript的支持一直很开放,TUniHTMLFrame就是专门用来塞自定义HTML和JS的容器。
ECharts是百度开源的一套纯前端图表库,底层用Canvas渲染,图表类型覆盖折线图、柱状图、饼图、地图、甘特图等,交互和动效都非常成熟。它的核心特点是:你只负责配置option对象,渲染和更新都交给它内部处理。
在UniGUI里用ECharts,其实就是把ECharts的JS库加载进页面,然后在UniGUI的某个宿主容器里创建图表实例。UniGUI负责页面框架和数据交互,ECharts负责视觉呈现,两边通过JavaScript桥接通信,分工很清晰。
1.2 三条集成路线,我为什么选了HTMLFrame
实际操作中,UniGUI里集成第三方JS库大致有三条路线:
| 方案 | 实现方式 | 优缺点 |
|---|---|---|
| TUniHTMLFrame | 在框架内直接写HTML+JS,通过UniSession.AddJS与Delphi通讯 | 灵活度高,便于双向调用,适合图表密集项目 |
| TUniURLFrame | 加载独立HTML页面的URL | 页面隔离好,但Delphi与页面数据交互麻烦 |
| WebResource+自定义JS | 把JS文件放到资源文件里,运行时注册 | 适合分发包,但调试麻烦,路径易出错 |
我实际开发更偏向前两种结合:如果图表不多,用TUniURLFrame加载一个写好的HTML文件,省去字符串拼接;如果图表要跟Delphi逻辑频繁交互,比如根据数据库查询结果动态更新,还是得用TUniHTMLFrame。这个压缩包里我最终采用了TUniHTMLFrame方案,因为ERP报表场景下,一个页面可能要同时渲染折线图、柱状图、甘特图,而且每个图都依赖后台数据,用HTMLFrame可以在同一个上下文里管理所有图表实例。
2. 基础集成:从环境准备到一个能跑的折线图
2.1 环境准备与文件放置
我用的环境是Delphi 10.4.2,UniGUI版本1.90.0.1567,ECharts用的是5.4.3的min版本。Delphi版本影响不大,只要是UniGUI支持的版本都能跑。关键是ECharts的JS文件要放在合适的位置。
我的习惯是在项目根目录下建一个js文件夹,把echarts.min.js放进去,然后在UniGUI的ServerModule里设置好静态资源目录。如果你用的是默认的HTML发布方式,可以直接在TUniHTMLFrame的HTML属性里写绝对路径或相对路径。
有一点需要注意:UniGUI默认的Session机制会把页面内容包装在框架里,如果JS路径写错,往往只在浏览器控制台报404,页面上看起来就是空白。所以建议先用相对路径/files/js/echarts.min.js测通一次,再考虑资源优化。
2.2 用TUniHTMLFrame渲染第一张折线图
在窗体上放一个TUniHTMLFrame,把它的Align设为alClient,然后在HTML属性里写入基础HTML结构。实际上我更推荐在OnCreate事件里动态拼接HTML字符串,因为这样能根据业务条件灵活生成。
下面是最小可用示例:
procedure TMainForm.UniHTMLFrame1Create(Sender: TObject); var html: string; begin html := '<div id="chart1" style="width:100%;height:400px;"></div>' + '<script src="files/js/echarts.min.js"></script>' + '<script>' + 'var chart1 = echarts.init(document.getElementById("chart1"));' + 'chart1.setOption({' + ' xAxis: { type: "category", data: ["1月","2月","3月","4月"] },' + ' yAxis: { type: "value" },' + ' series: [{ name: "销售额", type: "line", data: [820, 932, 901, 1290] }]' + '});' + '</script>'; UniHTMLFrame1.HTML := html; end;这段代码执行后,页面上会出现一个400像素高的折线图,x轴是月份,y轴是数值。注意div必须显式设置宽度和高度,否则ECharts在初始化时会拿到0宽0高的容器,图表直接不显示。这是新手最常踩的坑,后面会单独讲。
2.3 Delphi到JS的数据传递
实际项目里数据不会硬编码在HTML里,而是从数据库查出来,拼成JSON,再塞进setOption。我常用的方式是用UniSession.AddJS在服务端执行一段JavaScript,动态更新图表。
procedure TMainForm.RefreshChart(AJsonData: string); var js: string; begin js := 'if (typeof chart1 !== "undefined") {' + ' chart1.setOption({' + ' xAxis: { data: ' + GetXAxisJson + ' },' + // 实际要拼接JSON数组 ' series: [{ data: ' + AJsonData + ' }]' + ' });' + '}'; UniSession.AddJS(js); end;这里有个关键点:往JS里拼字符串时,JSON的引号会被Delphi字符串解析影响。我早期直接用QuotedStr,结果生成的JS经常语法错。后来统一用TJson对象生成JSON,再用TJson.ToString,可以避免大部分转义问题。你也可以用StringReplace把'处理成\',但最省心的还是别手动拼。
3. 进阶细节:事件回调、移动端tooltip和甘特图
3.1 点击图表事件回传Delphi
图表不只是展示,还要跟业务交互。比如点击折线图的某个点,把这个点的类别和数值回传给Delphi,用于联动刷新其他组件。
UniGUI提供的桥接方式是UniSession的回调机制。在ECharts的点击事件里,调用一个UniGUI生成的全局函数ajaxRequest,把参数传过去:
chart1.on('click', function(params) { ajaxRequest(unigui.MainForm, 'OnChartClick', [params.name, params.value]); });在Delphi对应的窗体里,添加一个公开方法(或TUniFrame中可以写OnAjaxEvent处理):
procedure TMainForm.UniFormAjaxEvent(Sender: TComponent; EventName: string; EventParams: TUniStrings); begin if EventName = 'OnChartClick' then begin ShowMessage('点击了 ' + EventParams.Values['arg1'] + ',值:' + EventParams.Values['arg2']); end; end;这里的arg1、arg2是UniGUI自动按顺序给回调参数取的名称。如果参数多,建议直接在JS里拼成JSON字符串,回传后解析,这样代码更整洁。
3.2 移动端适配与渲染完成后自动显示最后一个点的tooltip
热词里提到“echart折线图在移动端,怎么让它渲染完成后显示最后一个点的tooltip”,这个场景很常见:移动端页面空间狭窄,用户就想一眼看到最新数值,不想手动点击散点。
ECharts本身支持dispatchAction手动触发tooltip。你只需要在setOption完成后的回调里,找到最后一个数据点,再派发showTip事件:
chart1.setOption(option, false); var lastIndex = option.xAxis.data.length - 1; chart1.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: lastIndex });注意setOption的第二个参数传了false,意思是不要合并默认配置,避免老配置残留。实际业务里,如果你的数据是异步加载的,要在chart1.setOption(option, true, function() { dispatchAction(...) })的回调里触发,确保图表已经渲染完成。
移动端适配还有个隐藏问题:容器宽度在移动端往往没固定,ECharts初始化时如果页面还没布局完成,宽度可能只有默认的100px。我的做法是在window.onresize里调用chart1.resize(),同时在UniGUI页面加载完成后延迟一帧再初始化:
window.addEventListener('resize', function() { chart1.resize(); }); setTimeout(function() { if (!chart1) { chart1 = echarts.init(document.getElementById('chart1')); } chart1.setOption(option); }, 100);3.3 用ECharts实现甘特图的思路
ECharts原生没有专门的甘特图类型,但通过custom系列可以模拟。甘特图本质是若干条横条,横跨不同的时间区间。在ECharts里,我们可以用type: 'custom'配合renderItem函数,根据数据项的起始和结束时间计算矩形的位置和长度。
我压缩包里附带了一个简单的甘特图示例,核心思路是:
option = { xAxis: { type: 'time' }, yAxis: { type: 'category', data: ['任务A', '任务B', '任务C'] }, series: [{ type: 'custom', renderItem: function(params, api) { var start = api.coord([api.value(1), api.value(0)]); var end = api.coord([api.value(2), api.value(0)]); return { type: 'rect', shape: { x: start[0], y: start[1] - 10, width: end[0] - start[0], height: 20 }, style: api.style() }; }, encode: { x: [1, 2], // 起始时间,结束时间 y: 0 // 任务分类 }, data: [ ['任务A', '2024-01-01', '2024-01-10'], ['任务B', '2024-01-03', '2024-01-15'] ] }] };这个方案能实现基本甘特图,但交互上比专业甘特库弱一些。如果你的需求涉及拖动进度条、多人协作,建议考虑专用甘特插件。ECharts更多适用于展示型甘特,胜在不用额外引库,样式跟页面统一。
4. 常见问题排查与性能优化
4.1 ECharts资源加载失败
症状:页面打开,控制台报echarts is not defined,或者404找不到JS文件。
排查顺序:
| 检查点 | 方法 |
|---|---|
| JS文件是否发布 | 看files/js文件夹编译后是否包含echarts.min.js |
| 路径大小写 | UniGUI在Windows下不敏感,但Linux服务器上敏感 |
| Session隔离 | 多窗口标签打开时,资源路径是否被当前Session重写 |
| 域名或端口 | 如果部署在子路径,需要配合UniGUI的BaseURL调整 |
我用过一种笨但有效的方法:在浏览器Network面板直接访问JS文件URL,如果返回200,说明资源路径没问题;如果404,就去调整ServerModule里的资源目录映射。
4.2 图表白屏、容器高度为0
ECharts初始化失败最常见的原因是容器本身高度为零。UniGUI的TUniHTMLFrame在页面加载初期,可能还没撑开布局。特别是放在TabSheet里时,shown事件之前容器宽度高度都不稳定。
我的解决方法是:不要在组件创建时立即初始化,放在UniForm.show之后再执行。或者在HTML中给div一个固定高度,比如height:400px,先跑通再考虑响应式。
4.3 大数据量渲染卡顿
几千个点以上的折线图,ECharts默认渲染会卡。我的优化组合是:
sampling: 'lttb',让ECharts采用降采样算法,减少绘图点数量。animation: false,数据量大时关闭入场动画,能明显提升渲染速度。- 使用
dataZoom组件的inside类型,让用户通过滚轮缩放查看局部数据,而不是一次性渲染全部细节。
series: [{ type: 'line', sampling: 'lttb', data: data }], dataZoom: [{ type: 'inside' }], animation: false4.4 常见问题速查表
| 问题 | 原因 | 解法 |
|---|---|---|
| 图表不显示 | 容器高度为0 | 给div设置固定高度,或延迟初始化 |
| 数据更新后图表还是旧数据 | 没有重新setOption,或没设置notMerge | setOption(option, true)强制替换 |
| JS中文乱码 | 页面编码没设置UTF-8 | HTML头部加上<meta charset="utf-8"> |
| click事件触发不了 | 图表被其他元素遮挡 | 检查zIndex,或改用extend事件委托 |
| 多个图表实例冲突 | 全局变量重名 | 用闭包或对象封装,每个图表独立变量 |
| Delphi回调无响应 | 方法名不在事件注册列表 | 在UniFormEvents里检查AjaxEvent声明 |
这块总结下来,很大一部分问题都出在“前端的JS运行时机”和“Delphi服务端执行时机”没对齐。UniGUI是服务端事件模型,但前端图表完全是浏览器行为,两者之间天然有个异步的鸿沟。理解了这个,排查问题时思路会清晰很多。
5. 一点个人的实操体会
这个方案我用了一年多,最深的感受是:Delphi开发者在做Web化改造时,别试图用纯VCL的思路写前端。UniGUI只是个壳,真正让你拥有的是它暴露出来的UniSession.AddJS和ajaxRequest这两个沟通山。
而且ECharts的学习成本远比想象中低,官方示例库直接抄,改改数据就好。真正花时间的是前端事件和服务端逻辑的对齐。我的习惯是每个图表封装成一个Delphi类,类的属性对应ECharts的option,类方法负责生成JSON。这样后端的查询逻辑和前端的渲染配置完全分层,业务人员要改颜色、改标题,不用动前端代码。
最后再分享一个小技巧:调试时,我经常在UniHTMLFrame的HTML里临时加上console.log(JSON.stringify(option)),然后在浏览器控制台直接看生成的配置对不对。比在Delphi里反复断点快得多。等你把所有图表组件都封装好,打包发布时,这些调试代码记得清理干净。
本文还有配套的精品资源,点击获取