简介:面向Java桌面开发者的SWTChart图表类库资源,基于SWT构建,适合在Eclipse RCP或SWT界面中快速加入轻量级图表能力,解决原生SWT缺少可视化组件的问题。类库覆盖线图、散点图、堆栈图、对数标度、分类轴、多轴、柱形图、面积图、步骤图、轴取向与系列标签等常见需求,便于按业务数据选择合适呈现方式。压缩包共36个文件,约287KB,包含24个Java源文件、2个JAR包,以及HTML说明和工程配置文件,兼顾直接引用与源码阅读、二次调整。已有640人学习或下载,可作为SWT图表集成、示例参考与轻量可视化实践的入门素材。
1. 为什么在 SWT 桌面里选 SWTChart:三个场景和一条选型边界
桌面工具里最容易被低估的不是业务逻辑,而是现场人员要看的那张趋势图。SWTChart 是跑在 SWT 上的 Java 图表类库,做 SWT 图表时它把线图、散点图、柱状图、饼图直接嵌进 Composite,不依赖浏览器内核,启动和打包都轻。它适合已经用 SWT 做桌面客户端、监控面板、设备调试工具和离线报表的人;不适合要把图表塞进 Web 页面的场景。选它的理由很直接:控件模型和现有 SWT 界面一致,事件、资源释放、布局都能沿用同一套习惯;代价是样式定制和复杂交互要自己补。下面按能复现的顺序拆。
2. 把第一张 SWTChart 跑起来:环境检查、最小窗口和四个必调参数
2.1 资源包落位:SWT 本地库、SWTChart jar 和类路径三个检查点
SWTChart 本身是 Java 类库,但它依赖 SWT,而 SWT 在不同桌面环境里需要对应的本地库。很多人第一次跑不起来,不是图表 API 写错,而是类路径里只有 SWTChart 的 jar,没有 SWT 的 jar 和本地库,或者本地库位数和 JVM 位数对不上。我的习惯是先做一个最小窗口,只放一个空 Chart,确认 SWT 能启动,再往里加数据。这样出问题时排查面小,不会一上来就混着数据解析、线程和图表配置一起查。
资源包拿到后,先做三个检查:第一,构建路径里同时有 SWT 的 jar 和 SWTChart 的 jar;第二,SWT 的本地库和目标桌面环境匹配,32 位 JVM 配 32 位库,64 位 JVM 配 64 位库;第三,启动命令或工程配置里没有把资源包里的示例类误当成主类。下面这个命令只是示意类路径怎么拼,实际路径按你的工程替换,不要照抄目录名。
# 示意:把资源包里的库和编译输出一起加入类路径 # 实际路径按你的工程结构调整,重点是 SWT jar、SWTChart jar、本地库都要在 java -cp "lib/*:classes" your.Main这段命令的关键在-cp:lib/*表示把库目录下的 jar 都加进来,classes是编译输出目录。参数说明上,lib/*的通配符由 JVM 展开,不同 shell 对通配符处理不一样,命令不生效时先把 jar 全路径写出来试一次。本地库如果不在 jar 里,还要通过-Djava.library.path指过去;这一步经常被漏掉,现象是启动就报找不到本地库,连 Shell 都出不来。先把这个黑匣子排掉,后面的图表问题才有意义。
提示:不要用 AWT/Swing 的 EDT 思路去套 SWT。SWT 的 UI 操作有单独的 Display 线程约束,后面第 4 章会展开。
2.2 最小可运行窗口:从 Shell 到第一根折线
最小示例不要一上来就做多系列、多轴、图例定制。先创建一个 Shell,再创建 SWTChart,挂一个线系列,设置 X/Y 数组,调整范围,最后进事件循环。下面这段代码省略了 import,按资源包里的实际类路径补全即可;老版本和新版本的泛型写法略有差异,ILineSeries如果带泛型,用原始类型也能先跑通,编译警告不影响验证。
// 省略 import:按资源包中的实际类路径导入 SWT 与 SWTChart 相关类 public class FirstChart { public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setText("SWTChart 最小示例"); shell.setSize(800, 600); // 创建图表控件,父容器是 Shell,样式先用 NONE SWTChart chart = new SWTChart(shell, SWT.NONE); chart.setBounds(20, 20, 740, 520); // 创建线系列,名字会出现在图例里 ILineSeries series = chart.getSeriesSet().createLineSeries("温度"); series.setXArray(new double[]{0, 1, 2, 3, 4, 5}); series.setYArray(new double[]{21.5, 22.1, 21.8, 23.0, 22.6, 22.9}); series.setLineColor(display.getSystemColor(SWT.COLOR_BLUE)); series.setLineWidth(2); // 标题和轴标题,先确认文字能正常显示 chart.getTitle().setText("设备温度趋势"); chart.getAxisSet().getXAxis(0).getTitle().setText("采样点"); chart.getAxisSet().getYAxis(0).getTitle().setText("温度 / ℃"); // 让轴范围覆盖数据,再触发重绘 chart.getAxisSet().adjustRange(); chart.update(); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } }这段代码的逻辑顺序是:先有 Display,再有 Shell,再把 Chart 放到 Shell 上。SWT.NONE是控件样式,先用默认样式,避免一上来被边框、滚动条之类的样式干扰。setBounds是给 Chart 一个初始大小,SWT 的布局体系里如果不设大小,控件可能显示成一条线或者干脆看不见。createLineSeries返回系列对象,名字用于图例;setXArray和setYArray必须长度一致,不一致时常见现象是图不出来或者只画出一部分。setLineColor用系统颜色,不要自己new Color之后忘了释放;setLineWidth控制线宽,桌面高分屏下太细的线看起来会发虚。
adjustRange()是让轴范围自动包住数据,update()是触发重绘。很多人只调了setXArray就以为图会自己变,实际要更新范围或显式重绘。如果编译时报ILineSeries泛型错误,先看资源包里库的 API 签名,用原始类型或者补上对应的数组泛型;不要在这个阶段改业务代码,先把最小图跑出来。
2.3 四个必调参数:标题、图例、轴范围和重绘时机
最小图跑出来后,先调四个参数,不要急着做样式。第一个是标题,chart.getTitle().setText(...)只影响图表顶部标题,不影响轴标题;第二个是图例,chart.getLegend().setVisible(true)控制图例显示,系列多了以后图例位置和字号要一起看;第三个是轴范围,axis.setRange(new Range(min, max))是手动范围,adjustRange()是自动范围,两者顺序不能反;第四个是重绘,数据改完、范围改完,最后统一chart.update(),不要在每次setXArray后面都update。
下面这段代码把 X 轴固定成 0 到 24,Y 轴固定成 0 到 100,并设置刻度格式。手动范围适合监控面板这种“坐标轴不能抖”的场景;自动范围适合探索式查看。注意Range的上下界是数值,不是像素;DecimalFormat只影响刻度文字,不影响数据精度。
// 省略 import IAxis xAxis = chart.getAxisSet().getXAxis(0); IAxis yAxis = chart.getAxisSet().getYAxis(0); // 手动范围:适合固定量程的面板,避免刷新时轴来回跳 xAxis.setRange(new Range(0, 24)); yAxis.setRange(new Range(0, 100)); // 刻度格式:X 轴整数,Y 轴一位小数 xAxis.getTick().setFormat(new DecimalFormat("0")); yAxis.getTick().setFormat(new DecimalFormat("0.0")); // 图例和标题 chart.getLegend().setVisible(true); chart.getTitle().setText("24 小时采样"); chart.update();参数说明上,Range(0, 24)的两个参数是下界和上界,设反了会出现轴倒挂或者图跑出可视区。DecimalFormat("0")表示不要小数,"0.0"表示保留一位小数;格式串写错不会崩,但刻度会变成很奇怪的形式。chart.update()放在最后,前面所有设置批量生效。这里有个血泪经验:如果先setRange再adjustRange,手动范围会被自动范围覆盖,现象是“我明明设了 0 到 100,刷新后又变了”。顺序应该是先adjustRange做兜底,再setRange做固定,最后update。
3. 数据系列与坐标轴:线图、柱状图、散点图的组合和 CSV 装载
3.1 三种系列的创建差异与适用场景
SWTChart 的系列创建入口都在chart.getSeriesSet()上,线图用createLineSeries,柱状图用createBarSeries,散点图用createScatterSeries。它们的数据设置方式接近,都是 X 数组加 Y 数组,但视觉语义不同:线图强调连续趋势,柱状图强调离散分类或区间对比,散点图强调两个变量之间的关系。选错系列不是“能不能画”的问题,而是读图的人会不会误判。比如设备温度随时间变化,用线图;不同工位的产量对比,用柱状图;温度与压力的相关性,用散点图。
| 系列类型 | 创建方法 | 适合场景 | 参数注意点 |
|---|---|---|---|
| 线图 | createLineSeries | 时间序列、趋势、连续采样 | X 最好递增,线宽和颜色要区分系列 |
| 柱状图 | createBarSeries | 分类对比、离散区间 | X 轴刻度容易挤,分类多时要旋转标签或减柱 |
| 散点图 | createScatterSeries | 相关性、离群点 | 点大小和透明度影响可读性,点数多要降采样 |
代码上,线图和柱状图的骨架几乎一样,差别在系列类型和颜色设置。下面这段代码同时创建线图和柱状图,线图放温度,柱状图放产量。注意两个系列的 X 数组长度可以不同,但同一个系列内部 X/Y 必须等长。
// 省略 import ILineSeries line = chart.getSeriesSet().createLineSeries("温度"); line.setXArray(new double[]{0, 1, 2, 3, 4, 5}); line.setYArray(new double[]{21.5, 22.1, 21.8, 23.0, 22.6, 22.9}); line.setLineColor(display.getSystemColor(SWT.COLOR_BLUE)); line.setLineWidth(2); IBarSeries bar = chart.getSeriesSet().createBarSeries("产量"); bar.setXArray(new double[]{0, 1, 2, 3, 4, 5}); bar.setYArray(new double[]{12, 18, 9, 22, 16, 20}); bar.setBarColor(display.getSystemColor(SWT.COLOR_DARK_GREEN)); chart.getAxisSet().adjustRange(); chart.update();这段代码的逻辑是:两个系列共用同一组 X 轴刻度,但 Y 轴量纲不同。如果温度和产量都挂在同一根 Y 轴上,产量会把温度压成一条平线,读图的人会以为温度没变化。实际项目里,量纲不同的系列要么拆成两张图,要么开第二根 Y 轴。setBarColor控制柱体颜色,柱状图系列多的时候颜色要错开;adjustRange()会把两根轴的范围都包住所有系列,但不会解决量纲问题,只解决“看不看得见”的问题。
3.2 多 Y 轴:第二根轴什么时候值得开
第二根 Y 轴不是越多越好。开第二根轴的条件通常是:两个系列量纲不同、变化趋势都需要看、并且读者能通过颜色或轴标题区分。比如温度是 20 到 30,压力是 0 到 100,共用一根轴时温度几乎是一条直线。SWTChart 支持创建额外的 Y 轴,然后把系列绑定到该轴。不同版本对第二根轴的位置枚举和创建方法略有差异,核心步骤是创建轴、设标题和格式、把系列绑到轴 id 上。
// 省略 import IAxis pressureAxis = chart.getAxisSet().createYAxis(); pressureAxis.getTitle().setText("压力 / kPa"); pressureAxis.getTick().setFormat(new DecimalFormat("0.00")); ILineSeries pressureSeries = chart.getSeriesSet().createLineSeries("压力"); pressureSeries.setXArray(new double[]{0, 1, 2, 3, 4, 5}); pressureSeries.setYArray(new double[]{101.2, 102.5, 100.8, 103.1, 102.0, 101.6}); pressureSeries.setYAxisId(pressureAxis.getId()); pressureSeries.setLineColor(display.getSystemColor(SWT.COLOR_RED)); chart.getAxisSet().adjustRange(); chart.update();这段代码的关键是setYAxisId(pressureAxis.getId()),不绑 id 的话系列还挂在默认 Y 轴上,第二根轴就是空的。pressureAxis.getTitle().setText(...)和刻度格式要设,否则两条轴的颜色和文字一样,读图的人分不清哪条线对应哪根轴。第二根轴的位置如果默认在错误的一侧,需要查资源包里对应版本的轴位置设置;不要硬编码一个枚举值,编译不过就看 IDE 提示。多轴场景下,图例颜色和轴线颜色要一致,这是最低成本的防误读手段。
3.3 从 CSV 到 Series:一个可复用的装载器
真实数据很少是手写数组,常见做法是从 CSV 或数据库读出来再塞给系列。下面这个装载器只做三件事:跳过表头、按列解析 double、把列表转成数组。它故意不做复杂容错,先让主流程跑通;空值、NaN、Infinity 在后面过滤。参数上,分隔符写死成逗号,列索引 0 和 1 分别代表 X 和 Y,实际项目里把这两个参数提出来即可。
// 省略 import public final class CsvSeriesLoader { public static void loadLine(SWTChart chart, String seriesName, Path csv) throws IOException { List<Double> xs = new ArrayList<>(); List<Double> ys = new ArrayList<>(); try (BufferedReader br = Files.newBufferedReader(csv)) { String line; boolean first = true; while ((line = br.readLine()) != null) { if (first) { first = false; continue; } // 跳过表头 String[] cols = line.split(","); if (cols.length < 2) continue; String xText = cols[0].trim(); String yText = cols[1].trim(); if (xText.isEmpty() || yText.isEmpty()) continue; xs.add(Double.parseDouble(xText)); ys.add(Double.parseDouble(yText)); } } double[] x = xs.stream().mapToDouble(Double::doubleValue).toArray(); double[] y = ys.stream().mapToDouble(Double::doubleValue).toArray(); ILineSeries series = chart.getSeriesSet().createLineSeries(seriesName); series.setXArray(x); series.setYArray(y); chart.getAxisSet().adjustRange(); chart.update(); } }逻辑说明:Files.newBufferedReader默认字符集在部分环境里可能和文件不一致,CSV 有中文或特殊符号时显式指定字符集更稳;first标记只跳第一行,如果文件有多段表头要另加判断;cols.length < 2跳过残行;空字符串跳过,避免Double.parseDouble直接抛异常。参数说明:seriesName会进图例,重复创建同名系列不会自动合并,反而会出现两个图例项;Path csv建议用绝对路径调试,相对路径受工作目录影响,常见翻车是“文件明明在,程序说找不到”。
这段代码跑通后,再考虑把解析和 UI 更新分开。读文件可能慢,不要在主 UI 线程里读大文件;常见做法是后台线程读成数组,再回到 Display 线程设置系列,这部分第 4 章会写。
4. 动态刷新与大数据量:线程模型、批量更新和降采样
4.1 SWT 线程模型:为什么图表更新必须回到 Display 线程
SWT 的 UI 控件有线程约束,后台线程直接改 Chart 的数据或调用update(),轻则界面不刷新,重则抛Invalid thread access。这个错误不是 SWTChart 的锅,是 SWT 的控件模型决定的。正确做法是把“改数据 + 重绘”包在Display.asyncExec或Display.syncExec里。asyncExec不阻塞后台线程,适合高频采集;syncExec会等 UI 线程执行完,适合需要立刻拿到结果的场景,但不要在 UI 线程里再调syncExec,会死锁。
// 省略 import public void appendPoint(Display display, SWTChart chart, ILineSeries series, double x, double y) { display.asyncExec(() -> { if (chart.isDisposed()) return; // 窗口已关,别再碰控件 double[] oldX = series.getXArray(); double[] oldY = series.getYArray(); double[] newX = Arrays.copyOf(oldX, oldX.length + 1); double[] newY = Arrays.copyOf(oldY, oldY.length + 1); newX[newX.length - 1] = x; newY[newY.length - 1] = y; series.setXArray(newX); series.setYArray(newY); chart.getAxisSet().adjustRange(); chart.update(); }); }这段代码的逻辑是:后台线程只调appendPoint,真正的控件操作被投递到 Display 线程。chart.isDisposed()是后悔药,窗口关闭后后台线程还在跑时,不加判断会报控件已释放。参数上,x和y是本次追加的点,series必须是同一个系列对象;每次追加都复制整个数组,点数少时没问题,点数上万时复制开销会变成瓶颈。高频场景不要用这个写法,应该用环形缓冲或批量数组,下一节写。
4.2 批量更新:setRedraw、adjustRange 和 update 的顺序
批量更新比单点追加更适合监控面板。做法是先把一批数据准备好,关掉重绘,设置数组,调范围,开重绘,最后统一update()。setRedraw(false)是 Composite 的方法,能减少中间状态的重绘次数;adjustRange()放在数据设置之后、update()之前;update()只调一次。顺序错了会出现“数据设置了但图没变”或者“刷新过程中闪屏”。
// 省略 import chart.setRedraw(false); try { series.setXArray(batchX); series.setYArray(batchY); chart.getAxisSet().adjustRange(); } finally { chart.setRedraw(true); } chart.update();逻辑说明:try/finally保证异常时重绘开关能恢复,否则整个图表会一直不刷新,看起来像卡死。batchX和batchY必须等长,长度不一致时 SWTChart 的行为依赖版本,常见是只画较短的那部分。参数上,批量大小不是越大越好,一次几千到一万点足够;再大就要降采样。adjustRange()每批都调会让轴范围频繁变化,固定量程的面板应该改用手动setRange,只在初始化时调一次。
4.3 降采样与定时刷新:把 10 万点压到可交互
10 万点全画出来,桌面控件也能画,但交互会明显变慢,尤其是窗口拖动和鼠标悬停。我的习惯是超过 5000 点就降采样,趋势预览用等步长采样,峰值敏感的场景用最大最小桶。下面这个等步长版本最简单:按步长取点,保留首尾,参数maxPoints控制输出点数,通常 2000 到 5000 之间。它的缺点是可能漏掉尖峰,所以只适合趋势展示,不适合报警判定。
// 省略 import public static double[][] downsample(double[] x, double[] y, int maxPoints) { if (x.length <= maxPoints) { return new double[][]{x, y}; } int step = (int) Math.ceil(x.length / (double) maxPoints); int n = (x.length + step - 1) / step; double[] nx = new double[n]; double[] ny = new double[n]; for (int i = 0, j = 0; i < x.length; i += step, j++) { nx[j] = x[i]; ny[j] = y[i]; } return new double[][]{nx, ny}; }逻辑说明:step是采样步长,n是输出点数,循环按步长取点。参数maxPoints越小,绘制越快,但细节越少;maxPoints小于 2 时数组会退化,调用前要做边界判断。峰值敏感的场景不要用这个版本,改成每个桶保留最大值和最小值两个点,能明显减少“尖峰被抹平”的误判。定时刷新用display.timerExec(interval, runnable)比后台线程自己 sleep 更稳,interval一般 100 到 200 毫秒,太快没有意义,人眼也看不出来。
5. 避坑与排查:五类从空白图到导出失真的翻车现场
5.1 图不出来:先查数据、轴和线程
现象一:坐标轴有刻度,图表区域是空白,图例里也没有系列名。原因通常是系列创建失败或者数据数组长度不一致。SWTChart 的多个系列 API 对 X/Y 长度有隐含要求,长度不一致时有的版本直接不画,有的版本画到较短的长度。解决方法是先在设置数组前加断言,长度不一致直接抛异常,把问题暴露在数据装载层;同时检查createLineSeries的返回值有没有被丢弃,系列对象没被引用不代表没创建,但后续设置必须落在同一个对象上。数据里含NaN或Infinity也会导致整条线不画,装载时用Double.isFinite过滤。
现象二:运行时报Invalid thread access,或者窗口一闪就没了。原因是从后台线程直接操作了 Chart,或者 Display 已经 dispose 还在投递 UI 任务。解决方法是所有控件操作走Display.asyncExec,并在 runnable 开头判断chart.isDisposed()。窗口关闭时取消定时器,后台线程用标志位退出,不要依赖异常来结束循环。这个坑在采集类工具里很常见,血泪经验是:只要看到Invalid thread access,先搜代码里有没有在非 UI 线程调setXArray、update、setRange。
现象三:图能出来,但鼠标拖动窗口时图表重影或闪烁。原因多半是重绘开关和update()顺序不对,或者在高频刷新时每来一个点就重绘一次。解决方法是批量更新,setRedraw(false)包住数据设置,最后只update()一次;固定量程的面板不要每批都adjustRange(),轴范围变化会触发额外布局。SWT 控件本身有双缓冲,但中间状态太多时仍然会闪。
5.2 图能出来但不好用:查资源、刷新和导出
现象四:程序跑几个小时后越来越卡,最后报资源句柄相关错误。原因是每次刷新都new Color或new Font,没有 dispose。SWT 的 Color 和 Font 是系统资源,系统颜色用display.getSystemColor(...)拿到的不要手动释放;自己创建的资源要挂在控件的DisposeListener上统一释放,或者在初始化时创建一次、复用到结束。图表系列颜色如果每次刷新都换,先检查是不是在循环里 new Color。
现象五:导出的 PNG 尺寸不对,文字发虚,或者图只导出可见区域。原因是导出前没有按目标尺寸布局,直接抓当前控件;高 DPI 下字体按固定像素设置,放大后自然模糊。解决方法是导出前把 Chart 设成目标尺寸,调layout()和update(),再用库自带的 save 方法导出;不同版本的 save 签名和格式常量可能不同,编译不过就看 IDE 提示,不要硬编码。字体按 DPI 缩放,导出尺寸和屏幕尺寸分开,不要指望屏幕截图能当报表图。
注意:避坑章里的“现象→原因→解决”不是让你背,而是排错顺序。先确认图能不能出来,再确认刷新稳不稳,最后才调样式和导出。
6. 进阶验证与导出:用基准线核对轴范围,再把 PNG 输出钉死
6.1 基准线验证:用 y=2x+1 检查轴范围和刻度
图表类库最容易出的问题不是画不出来,而是画出来了但轴范围不对,读图的人被误导。我一般会先用一条已知斜线做基准:X 取 0 到 4,Y 取 1 到 9,跑adjustRange()后读回轴范围,确认下界不大于最小值、上界不小于最大值。这个验证不依赖业务数据,能快速区分“数据问题”和“轴问题”。
// 省略 import double[] x = {0, 1, 2, 3, 4}; double[] y = {1, 3, 5, 7, 9}; ILineSeries series = chart.getSeriesSet().createLineSeries("基准"); series.setXArray(x); series.setYArray(y); chart.getAxisSet().adjustRange(); Range xr = chart.getAxisSet().getXAxis(0).getRange(); Range yr = chart.getAxisSet().getYAxis(0).getRange(); if (xr.lower > 0 || xr.upper < 4 || yr.lower > 1 || yr.upper < 9) { throw new IllegalStateException("轴范围没有覆盖基准数据"); }这段代码只做一件事:轴范围必须包住数据。xr.lower和xr.upper是读回来的下界和上界,adjustRange()之后如果还不覆盖,说明系列没绑定到默认轴,或者数据设置顺序有问题。基准线跑通后,再换成真实数据,轴范围问题就不会和业务逻辑混在一起。
6.2 导出前先定尺寸:PNG 输出别靠屏幕截图
导出 PNG 的常见做法是先把 Chart 设成目标尺寸,布局完再保存。屏幕截图受当前窗口大小和缩放影响,报表
本文还有配套的精品资源,点击获取