简介:SWTChart是一款基于SWT的Java图表类库,面向需要在Eclipse RCP或SWT桌面应用中嵌入数据可视化的Java开发者,尤其适合追求轻量、无需额外依赖的图表绘制场景。它支持线图、散点图、堆栈图、柱形图、面积图、步骤图、对数标度、分类轴、多轴、轴取向与系列标签等多种图表类型,可满足常规统计与工程数据展示需求。资源包共36个文件,约287KB,以24个java源码文件为核心,辅以2个jar包、2个project与classpath配置、2个html说明文档及gif、properties、mf、xml等辅助文件,目录中同时包含插件工程与示例工程,便于直接导入Eclipse运行调试。目前已有640人学习下载。通过阅读源码与示例,读者可掌握SWTChart的图表构建流程、坐标轴与系列配置方式,并借鉴其插件化目录组织,快速将图表能力集成到自己的SWT项目中。
1. 桌面端图表选型:为什么我最终把 SWTChart 塞进了工具箱
做 Java 桌面端的人大概都经历过这种尴尬:界面用 SWT 搭得挺利索,一到画图表就卡壳。Swing 那边有 JFreeChart 撑场子,可 SWT 和 Swing 混用要么线程模型打架,要么渲染风格割裂,轻则界面闪烁,重则直接抛异常。我早些年接手一个工业监控面板,需求是实时折线加柱状对比,试过把 JFreeChart 嵌进 SWT 的 Composite,结果重绘频率一高就掉帧,鼠标悬停提示还经常错位。后来翻到 SWTChart 这个纯 SWT 实现的图表类库,才算把这块补上。它不依赖 Swing,直接基于 SWT 的 GC 绘制,坐标轴、系列、图例、工具提示都是原生控件风格,跟 RCP 应用能无缝拼在一起。如果你也在用 SWT 做桌面工具、测试台软件或者设备上位机,需要轻量、可嵌入、不引入额外 UI 框架的图表方案,这个类库值得花半小时摸一遍。
2. SWTChart 核心对象模型:从 Chart 到 Series 的装配逻辑
2.1 三个必须分清的层级:Chart、Axis、Series
SWTChart 的 API 设计比较直白,但新手容易把 Axis 和 Series 的关系搞反。一个 Chart 实例是画布容器,它内部默认带 X 轴和 Y 轴对象,但轴本身不存数据,只负责刻度范围、标签格式和网格线样式。真正承载数据点的是 Series,每个 Series 持有一组 x/y 值,并且必须绑定到某个轴。常见做法是:先new Chart(parent, SWT.NONE)拿到画布,再通过chart.getAxisSet().getXAxis(0)和getYAxis(0)拿到默认轴,然后用chart.getSeriesSet().createSeries(SeriesType.LINE, "温度")创建系列。这里有个细节,SeriesType 决定了渲染方式,LINE、BAR、AREA、SCATTER 都支持,但一个 Chart 里混用多种类型时,轴的范围要手动算,否则柱状图的基线可能被折线图的负值带偏。
// 创建画布,父容器通常是 SWT 的 Composite Chart chart = new Chart(parent, SWT.NONE); // 拿到默认的 X 轴和 Y 轴,索引 0 表示主坐标轴 IXAxis xAxis = chart.getAxisSet().getXAxis(0); IYAxis yAxis = chart.getAxisSet().getYAxis(0); // 创建折线系列,名称会显示在图例里 ILineSeries lineSeries = (ILineSeries) chart.getSeriesSet() .createSeries(SeriesType.LINE, "温度曲线"); // 设置数据,xSeries 和 ySeries 是 double[] 数组 lineSeries.setXSeries(xSeries); lineSeries.setYSeries(ySeries); // 让轴根据数据自动调整范围,否则默认是 0 到 100 chart.getAxisSet().adjustRange();上面这段是标准起手式。adjustRange()很关键,不调用的话轴范围固定,数据超出就看不见。参数方面,setXSeries和setYSeries接收 double 数组,长度必须一致,否则运行时会抛 IllegalArgumentException。如果数据是动态追加的,别每次重建 Series,用lineSeries.add(x, y)或者直接操作底层数组再调chart.update(),前者适合低频更新,后者适合批量刷新。
2.2 样式与交互:线宽、符号、工具提示的配置入口
SWTChart 的样式配置分散在 Series 和 Axis 上,没有统一的样式表。线宽用lineSeries.setLineWidth(2),颜色用setLineColor(display.getSystemColor(SWT.COLOR_BLUE)),数据点符号通过setSymbolType(SymbolType.CIRCLE)开启,符号大小用setSymbolSize(4)。工具提示需要显式启用:lineSeries.setToolTipEnabled(true),然后实现ISeriesToolTip接口或者直接用默认的,默认会显示系列名和当前点的 x/y 值。这里有个坑,工具提示的格式化依赖setXAxisFormat和setYAxisFormat,如果轴格式是整数而数据是浮点,提示会四舍五入,看起来像数据丢了精度。
// 设置折线颜色和线宽 lineSeries.setLineColor(display.getSystemColor(SWT.COLOR_DARK_GREEN)); lineSeries.setLineWidth(2); // 开启数据点符号,类型为圆形,大小为 5 像素 lineSeries.setSymbolType(SymbolType.CIRCLE); lineSeries.setSymbolSize(5); // 启用工具提示,鼠标悬停时显示数值 lineSeries.setToolTipEnabled(true); // 设置 Y 轴数字格式,保留两位小数 yAxis.setNumberFormat("#.##");setNumberFormat用的是 DecimalFormat 模式,#.##表示最多两位小数,整数部分不补零。如果数据范围跨几个数量级,建议用科学计数法模式0.00E0,否则轴标签会挤成一团。另外,display对象要从当前 Shell 获取,别在非 UI 线程里调这些 setter,SWT 的控件操作必须在 UI 线程执行,否则抛 SWTException。
3. 把数据画出来:从静态数组到实时刷新的完整链路
3.1 静态数据绑定:一次填充与范围校准
静态场景最简单,准备两个 double 数组,创建 Series,塞进去,调adjustRange(),最后chart.update()触发重绘。但实际项目里数据往往来自 CSV 或数据库查询结果,常见做法是先把结果集转成List<Double>,再转 double 数组。注意空值和 NaN 的处理,SWTChart 遇到 NaN 会直接跳过该点,折线断开,这有时是想要的效果,有时不是。如果希望断点处连线,得手动插值或者用Double.NaN之外的哨兵值。下面是一个从二维数组填充的示例,顺便设置轴标题。
// 假设 data 是 double[][],第一列是 x,第二列是 y double[] xData = new double[data.length]; double[] yData = new double[data.length]; for (int i = 0; i < data.length; i++) { xData[i] = data[i][0]; yData[i] = data[i][1]; } ILineSeries series = (ILineSeries) chart.getSeriesSet() .createSeries(SeriesType.LINE, "采样值"); series.setXSeries(xData); series.setYSeries(yData); // 设置轴标题,显示在轴末端 chart.getAxisSet().getXAxis(0).getTitle().setText("时间 (s)"); chart.getAxisSet().getYAxis(0).getTitle().setText("幅值 (mV)"); // 根据数据自动调整轴范围 chart.getAxisSet().adjustRange(); chart.update();adjustRange()会遍历所有 Series 的 x/y 值,算出最小最大值,然后留一点边距。如果数据里有异常大值,轴范围会被拉得很开,正常数据挤在底部。这时候别用 adjustRange,手动setRange(min, max)更稳妥。轴标题的字体和颜色可以通过getTitle().setForegroundColor()改,但默认样式通常够用。
3.2 实时刷新:定时器、双缓冲与重绘频率控制
实时图表是 SWTChart 的高频使用场景,也是翻车重灾区。常见做法是用Display.timerExec(interval, runnable)或者Executors.newScheduledThreadPool配合Display.asyncExec往 UI 线程推更新。关键点是:不要每次新数据都调chart.update(),那样重绘太频繁,CPU 直接飙满。我一般会攒一批点,比如每 200ms 刷新一次,或者数据量超过阈值时再刷。另外,Series 的数据数组如果频繁扩容,内存抖动明显,建议用固定长度的环形缓冲区,或者直接操作lineSeries.getXSeries()返回的数组引用,改完值再调chart.update()。
// 使用 Display.timerExec 实现定时刷新,间隔 200ms display.timerExec(200, new Runnable() { @Override public void run() { // 模拟从设备读取新数据 double newX = System.currentTimeMillis() / 1000.0; double newY = readSensorValue(); // 追加到 Series,内部会扩容,低频场景可接受 lineSeries.add(newX, newY); // 只保留最近 100 个点,避免内存无限增长 if (lineSeries.getXSeries().length > 100) { double[] xArr = lineSeries.getXSeries(); double[] yArr = lineSeries.getYSeries(); double[] newXArr = new double[100]; double[] newYArr = new double[100]; System.arraycopy(xArr, xArr.length - 100, newXArr, 0, 100); System.arraycopy(yArr, yArr.length - 100, newYArr, 0, 100); lineSeries.setXSeries(newXArr); lineSeries.setYSeries(newYArr); } // 重新校准轴范围并重绘 chart.getAxisSet().adjustRange(); chart.update(); // 继续下一次定时 display.timerExec(200, this); } });这段代码里lineSeries.add每次都会新建数组,数据量大时 GC 压力不小。如果刷新频率高,建议自己维护环形缓冲区,直接改数组元素,然后调chart.update()。adjustRange()在实时场景下也会遍历所有点,数据多了是 O(n) 开销,可以改成手动setRange只显示滑动窗口。另外,display.timerExec必须在 UI 线程调用,通常放在createContents或者按钮事件里。
4. 避坑与排查:SWTChart 落地时最容易踩的五条
4.1 现象:图表一片空白,没有任何线条
原因通常是 Series 没绑定到正确的轴,或者轴范围是默认的 0 到 100 而数据在 1000 以上。还有一种可能是chart.update()没调,SWTChart 不会自动重绘。解决:先确认createSeries返回的 Series 非空,再检查adjustRange()是否执行,最后手动setRange到数据实际范围。如果用的是自定义轴,确认 Series 通过setXAxis和setYAxis绑定了。
4.2 现象:鼠标悬停工具提示不显示或显示错位
原因一般是setToolTipEnabled(true)没开,或者 Series 的setXSeries和setYSeries长度不一致导致内部索引越界。错位则可能是轴格式和实际数据精度不匹配,比如数据是 0.001 级别,轴格式却是整数。解决:开启工具提示后,检查两个数组长度,设置合适的setNumberFormat,必要时自定义ISeriesToolTip实现。
4.3 现象:实时刷新时界面卡顿,CPU 占用高
原因是chart.update()调用过于频繁,或者adjustRange()在每次刷新时遍历全量数据。解决:降低刷新频率到 100ms 以上,用滑动窗口限制数据点数量,把adjustRange()换成手动setRange,只在数据范围变化时调整。另外,避免在非 UI 线程直接操作 Series,用Display.asyncExec包一层。
4.4 现象:多条 Series 混用时,柱状图被折线图压扁
原因是所有 Series 共享同一个 Y 轴,柱状图的值域和折线图差异大时,轴范围被拉偏。解决:SWTChart 支持多轴,通过chart.getAxisSet().createYAxis()创建第二个 Y 轴,然后把柱状图 Series 绑定到新轴,并设置新轴的范围和颜色。注意多轴时图例会混在一起,需要手动调整位置。
4.5 现象:导出图片时背景全黑或文字丢失
原因是 SWTChart 的save方法依赖 GC 绘制,如果直接调chart.save("test.png", SWT.IMAGE_PNG)在某些平台下背景透明处理有问题。解决:先创建一个Image对象,用GC画上去,再保存。或者用chart.print(new GC(image))的方式,确保背景色显式设置为白色。导出前调chart.update()保证渲染状态最新。
5. 进阶技巧:用 SWTChart 做可交互的实时频谱图
频谱图对刷新率和数据量都敏感,我一般会关掉符号和工具提示,只保留折线,线宽设为 1,颜色用亮绿或青色。数据源用环形缓冲区,每次刷新只更新数组末尾的 N 个点,然后调chart.update()。轴范围固定为 0 到采样率的一半,避免adjustRange的开销。如果要做瀑布图,SWTChart 本身不支持,但可以用多个 Chart 叠加或者换成 SWT 的 Canvas 自绘。验证方法很简单:开一个Display.timerExec每 50ms 推一次随机数据,观察任务管理器的 CPU 占用,正常应该在 5% 以下。如果超过 20%,检查是不是每次都在重建 Series 或者调了adjustRange。
// 频谱图初始化:固定轴范围,关闭符号和提示 ILineSeries spectrumSeries = (ILineSeries) chart.getSeriesSet() .createSeries(SeriesType.LINE, "频谱"); spectrumSeries.setSymbolType(SymbolType.NONE); spectrumSeries.setToolTipEnabled(false); spectrumSeries.setLineWidth(1); spectrumSeries.setLineColor(display.getSystemColor(SWT.COLOR_GREEN)); // 固定 X 轴范围 0 到 20000 Hz,Y 轴 0 到 100 dB chart.getAxisSet().getXAxis(0).setRange(0, 20000); chart.getAxisSet().getYAxis(0).setRange(0, 100); // 预分配数组,避免每次刷新重建 double[] freqData = new double[512]; double[] ampData = new double[512]; spectrumSeries.setXSeries(freqData); spectrumSeries.setYSeries(ampData); // 定时刷新:只改数组值,不重建 Series display.timerExec(50, new Runnable() { @Override public void run() { // 模拟填充新频谱数据 for (int i = 0; i < 512; i++) { freqData[i] = i * 40.0; ampData[i] = 50 + 30 * Math.sin(i * 0.1 + System.currentTimeMillis() / 1000.0); } // 直接更新 Series 数据引用,触发重绘 spectrumSeries.setXSeries(freqData); spectrumSeries.setYSeries(ampData); chart.update(); display.timerExec(50, this); } });这段代码的关键是预分配数组,每次只改值,不新建对象。setXSeries和setYSeries会复制数组引用,所以改原数组后需要重新 set 一次才能让 Series 感知到变化。如果嫌麻烦,可以直接用spectrumSeries.getXSeries()拿到内部数组改,但那样绕过了封装,版本升级可能出问题。我一般还是老老实实 set 一遍,性能差异在 512 点级别可以忽略。
从那以后我每次接 SWT 桌面项目,只要涉及图表,都会先拿 SWTChart 搭个最小原型跑一遍实时刷新,确认 CPU 和内存曲线平稳再往下做。希望帮到你。
本文还有配套的精品资源,点击获取