ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SWT桌面程序实时曲线:SWTChart嵌入、刷新与避坑

SWT桌面程序实时曲线:SWTChart嵌入、刷新与避坑

简介:面向Java桌面开发者的SWTChart图表类库压缩包,适合需要在SWT/JFace界面中快速加入可视化图表的中级开发者,也便于学习者理解轻量级绘图组件的组织方式。包内共36个文件,约287KB,以24个Java源码为主,辅以工程配置、类路径文件、HTML说明文档及可直接引用的jar包,兼顾二次开发与开箱试用。目录中还保留示例工程与插件化打包配置,便于按需抽取核心类或整体集成。已有640人学习下载,说明其在SWT图表选型中有一定参考价值。借助这些内容,读者可查看线图、散点图、柱形图、面积图、堆栈图、多轴、对数标度与分类轴等实现线索,结合示例快速搭建图表界面,并据此扩展系列标签、轴取向等配置,减少从零封装绘图控件的时间,对离线查阅API与实现细节也较为友好。

1. 给 SWT 桌面程序加曲线,别急着上 JFreeChart

一个基于 SWT 的采集上位机,界面上要加一条实时温度曲线,很多人的第一反应是引 JFreeChart,结果嵌进 SWT 容器后字体发虚、打包体积翻倍,还多带了一整套 AWT 的依赖。SWTChart 走的是另一条路:它本身就是Composite的子类,new到Shell里就能画,折线、柱状、散点、饼图都有现成实现,坐标轴、图例、网格、PNG 导出也都是库内自带。它的定位很清楚——给 SWT 桌面程序、RCP 客户端、工控上位机这类原生窗口做嵌入式图表,数据量在万点以内、要求启动快、不想引入 Swing 栈的场景最合适。下面按「怎么装起来、怎么调样式、怎么刷实时数据、哪里会翻车」的顺序拆一遍。

2. 把第一张图跑起来:依赖、平台 native 与最小 Chart

SWTChart 的 API 不难,难的是让它在你机器上先画出东西。绝大多数人卡在第一步不是代码写错,而是 SWT 的 native 库没对上平台。这一章从依赖配置讲到能看见一条折线为止,中间会解释每一步在做什么,方便你后面改参数时知道该动哪。

2.1 依赖坐标与平台 native 的匹配

SWTChart 本身是纯 Java 字节码,但它继承Composite,运行时必须有 SWT 的 native 实现托底。Maven 里直接引主包,传递依赖会带进来一份通用的 SWT,那份 jar 里没有swt-win32-*.dll或libswt-gtk-*.so,双击运行就会报UnsatisfiedLinkError。常见做法是按操作系统拆 profile,只激活当前平台那一份 native 包。

<properties> <!-- 两个版本号都填你们仓库里实际存在的,别照抄占位符 --> <swtchart.version>${swtchart.version}</swtchart.version> <swt.version>${swt.version}</swt.version> </properties> <dependencies> <dependency> <groupId>org.eclipse.swtchart</groupId> <artifactId>org.eclipse.swtchart</artifactId> <version>${swtchart.version}</version> </dependency> </dependencies> <profiles> <profile> <id>swt-win</id> <activation><os><family>windows</family></os></activation> <dependencies> <dependency> <groupId>org.eclipse.platform</groupId> <artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId> <version>${swt.version}</version> </dependency> </dependencies> </profile> <profile> <id>swt-linux</id> <activation><os><family>unix</family><name>Linux</name></os></activation> <dependencies> <dependency> <groupId>org.eclipse.platform</groupId> <artifactId>org.eclipse.swt.gtk.linux.x86_64</artifactId> <version>${swt.version}</version> </dependency> </dependencies> </profile> </profiles>

这段配置的逻辑是:主依赖只管图表逻辑,native 由 profile 按<os>条件注入,打包到 Windows 就只带 Windows 的 dll,Linux 同理。参数上有两个地方要盯:swt.version不能比 SWTChart 编译时依赖的 SWT 版本低太多,否则会在运行期抛NoSuchMethodError,表现为图表构造到一半直接崩;swtchart.version尽量选和团队基线一致的那一版,不同小版本之间ILineSeries的泛型签名改过,0.14 之后是ILineSeries<?>,老代码直接写ILineSeries会编译告警但不影响运行。

如果是 RCP 客户端,一般不走 Maven 传递依赖,而是在 Target Platform 里勾选对应的 SWT 和 SWTChart 插件,这时候要注意别同时勾两个平台的 fragment,否则启动时类加载器随机挑一个,现象就是「同一份代码在同事机器上好好的,在我这儿白屏」。

提示:装完先写三行代码 new 一个空Chart,能显示出来再往下写业务,别一上来就堆数据系列,出错时分不清是依赖问题还是数据问题。

2.2 从 Display 到 Chart:最小可运行骨架

SWT 程序的骨架和 Swing 不一样,没有隐式的 EDT,事件循环要自己泵。下面这段是能直接跑起来的最小结构,去掉业务后只剩窗口、图表和事件循环。

import org.eclipse.swt.SWT; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swtchart.Chart; public class MinimalChart { public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setText("最小 SWTChart"); shell.setSize(900, 620); shell.setLayout(new FillLayout()); // 让 Chart 自动撑满 // Chart 是 Composite 子类,父容器和 style 是必填的 Chart chart = new Chart(shell, SWT.NONE); chart.getTitle().setText("示例曲线"); shell.open(); // SWT 的标准事件泵:没有消息就睡,有消息就分发 while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } }

逻辑说明:Display是 SWT 里线程与资源的根,Shell是顶层窗口,readAndDispatch负责把系统消息分发给控件,sleep在空闲时让出 CPU。这一整套必须跑在创建Display的那个线程上,也就是主线程,后面讲实时刷新时所有的跨线程操作都要回到这个循环里来。

参数说明:new Chart(shell, SWT.NONE)的第二个参数是控件样式,SWT.NONE表示默认绘制,需要边框可以叠加SWT.BORDER;shell.setLayout(new FillLayout())决定Chart是否随窗口缩放,如果你用setBounds手工定位,窗口拉伸时图表不会跟着变,这是新手最常见的一个「图表不刷新」的误判来源。shell.setSize给的是像素,高 DPI 屏上要乘display.getDPI().x / 96.0,否则字会偏小。

跑通这一步之后,窗口里应该是一块灰白色画布加一个标题,没有坐标轴也没有曲线,这是正常的——空Chart不会自己造轴,轴是随系列一起被adjustRange激活的。

2.3 折线系列与坐标轴的最小配置

有画布之后加数据。SWTChart 的系列通过getSeriesSet().createSeries(type, id)创建,id同时是图例上的名字;折线对应SeriesType.LINE,返回对象强转成ILineSeries后才有setXSeries/setYSeries。

import org.eclipse.swtchart.IAxis; import org.eclipse.swtchart.ILineSeries; import org.eclipse.swtchart.ILineSeries.PlotSymbolType; import org.eclipse.swtchart.ISeries.SeriesType; // 造一段正弦数据,实际项目里换成你的采样值 int n = 200; double[] x = new double[n]; double[] y = new double[n]; for (int i = 0; i < n; i++) { x[i] = i * 0.1; y[i] = Math.sin(x[i]); } ILineSeries<?> series = (ILineSeries<?>) chart.getSeriesSet() .createSeries(SeriesType.LINE, "温度"); series.setXSeries(x); series.setYSeries(y); series.setLineColor(display.getSystemColor(SWT.COLOR_BLUE)); series.setLineWidth(2); // 点数多时关掉数据点符号,否则每个点都画一个圆,性能掉得厉害 series.setSymbolType(PlotSymbolType.NONE); IAxis xAxis = chart.getAxisSet().getXAxis(0); IAxis yAxis = chart.getAxisSet().getYAxis(0); xAxis.getTitle().setText("时间/s"); yAxis.getTitle().setText("温度/℃"); xAxis.getTick().setTickLabelAngle(0); xAxis.getGrid().setVisible(true); chart.getLegend().setVisible(true); chart.getAxisSet().adjustRange(); // 根据所有系列重算轴范围 chart.redraw();

逻辑说明:createSeries每调一次就多一个系列,不会覆盖同名系列,重复调用会叠出好几条重合的线,这是后面排查「线越来越粗」的根源。数据灌进系列之后必须调adjustRange(),否则轴范围还停在默认的 0 到 1,你只能看到曲线的一小段甚至什么都看不到。

参数说明:setLineWidth(2)的单位是像素,超过 4 在高 DPI 下会显得糊;PlotSymbolType.NONE适合 500 点以上的场景,小于 100 点时用CIRCLE更利于读数;setTickLabelAngle控制 X 轴标签倾斜角度,时间戳很长时设 45 能避免标签互相压字。ILineSeries<?>的泛型参数在老版本里没有,如果你那边的库版本较旧,去掉<?>写成原始类型即可,编译告警不影响功能。

到这一步,窗口里应该有一条蓝色正弦曲线、带刻度的两根轴和右上角的图例。接下来所有工作都是围绕这个骨架做样式和数据流的调整。

3. 系列、坐标轴与样式:把默认图表调到能交付

默认样式能看,但离能交付还差一截:多组数据挤在一根 Y 轴上互相压、刻度密到看不清、导出给别人时只有半张图。这一章解决三件事——多系列怎么组织、轴范围和网格怎么控、图怎么导成文件。

3.1 多系列与 SeriesSet 的生命周期

一个监控面板上往往同时有温度、湿度、压力,量纲不同,硬塞一根 Y 轴的话小量程那条线会被压成一条直线。SWTChart 允许再建一根 Y 轴,把系列挂到上面去,实现双轴效果。

import org.eclipse.swtchart.ISeriesSet; import org.eclipse.swtchart.IAxis; ISeriesSet set = chart.getSeriesSet(); ILineSeries<?> temp = (ILineSeries<?>) set.createSeries(SeriesType.LINE, "温度"); ILineSeries<?> humi = (ILineSeries<?>) set.createSeries(SeriesType.LINE, "湿度"); // 新建第二根 Y 轴,index 从 1 开始 IAxis y2 = chart.getAxisSet().createYAxis(); y2.getTitle().setText("湿度/%RH"); y2.getTick().setForeground(display.getSystemColor(SWT.COLOR_DARK_GREEN)); // 把湿度系列绑到 axisId=1 上 humi.setYAxisId(1); humi.setLineColor(display.getSystemColor(SWT.COLOR_DARK_GREEN)); chart.getAxisSet().adjustRange(); chart.redraw();

逻辑说明:createYAxis()返回新的轴对象,轴的数量和系列数量是独立的,一个轴可以挂多个系列。绑定关系靠setYAxisId(int),索引从 0 开始,0 是默认轴,1 是刚建的那根。颜色上建议轴和系列保持一致,读者一眼能对上哪条线读哪根轴。

参数说明:createYAxis之后如果忘了adjustRange,第二根轴的范围会停在 0 到 1,曲线直接飞出画面。删除系列用set.deleteSeries("温度"),删完同样要adjustRange加redraw;注意deleteSeries只解绑数据和图元,系列对象上你手工new出来的Color不会被回收,得自己dispose,这一点在第五章会展开。

系列对象的复用原则很简单:数据变了就setYSeries覆盖,不要删了重建。重建在低频场景下看不出问题,一旦进了 100ms 一次的刷新循环,图元反复创建销毁会把 GC 压力拉满,界面开始一顿一顿的。

3.2 坐标轴范围、刻度与网格的三种控制方式

轴范围的控制分三档,用哪一档取决于你的业务对数轴的敏感程度。全自动是adjustRange(),每次根据所有系列的最小最大值重算,适合静态报表;半自动是固定一端、放开另一端;全手动是直接setRange(new Range(min, max)),适合实时监控。

import org.eclipse.swtchart.Range; IAxis xAxis = chart.getAxisSet().getXAxis(0); IAxis yAxis = chart.getAxisSet().getYAxis(0); // 方式一:手动锁定 Y 轴量程,监控场景常用 yAxis.setRange(new Range(0, 100)); // 方式二:X 轴做滑动窗口,只显示最近 60 秒 double newest = x[x.length - 1]; xAxis.setRange(new Range(Math.max(0, newest - 60), newest)); // 刻度密度:给个期望步长,库会自己取整到好看的刻度 xAxis.getTick().setTickMarkStepHint(50); yAxis.getTick().setTickMarkStepHint(40); // 网格线只留 Y 方向,X 方向关掉减少视觉噪音 xAxis.getGrid().setVisible(false); yAxis.getGrid().setVisible(true); yAxis.getGrid().setForeground(display.getSystemColor(SWT.COLOR_GRAY));

逻辑说明:setRange的优先级高于adjustRange,一旦手动设过范围,后续再调adjustRange会把它覆盖掉,所以两者不要混用。滑动窗口的写法是把 X 轴的右端钉在最新时间点,左端减掉窗口长度,曲线看起来就是向左滚动的。

参数说明:setTickMarkStepHint是「期望值」不是「精确值」,库会根据可用像素宽度做取整,设 50 最后可能落到 40 或 60,这属于正常行为,不要以为是没生效。Range的构造参数顺序是(lower, upper),写反了不会报错,只是图反着画,排查时容易看漏。网格颜色的Color如果用new Color(display, 200, 200, 200)创建,用完记得 dispose;用display.getSystemColor(...)拿的是系统共享色,不要 dispose。

刻度标签的格式在数值很大或很小时会显得难受,比如 0.0001 这种量级,默认可能显示成科学计数法。这类需求一般不改轴本身,而是在数据进系列之前自己做单位换算,把「伏」换成「毫伏」,比去改渲染逻辑省事得多。

3.3 导出 PNG 的两种路径与像素密度

图表做完要给同事看、要贴报告,导出是刚需。SWT 里没有现成的「图表截图」按钮,标准做法是把控件打印到一个Image的GC上,再用ImageLoader落盘。

import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; public static void exportPng(Chart chart, String path, int scale) { // scale=2 时导出两倍尺寸,贴到文档里不糊 int w = chart.getSize().x * scale; int h = chart.getSize().y * scale; Image image = new Image(chart.getDisplay(), w, h); GC gc = new GC(image); try { gc.setAdvanced(true); gc.setAntialias(SWT.ON); gc.setInterpolation(SWT.HIGH); // 关键:先把 GC 缩放,再让控件按逻辑坐标画进来 gc.setTransform(new org.eclipse.swt.graphics.Transform(chart.getDisplay(), scale, 0, 0, scale, 0, 0)); chart.print(gc); } finally { gc.dispose(); } ImageLoader loader = new ImageLoader(); loader.data = new ImageData[] { image.getImageData() }; loader.save(path, SWT.IMAGE_PNG); image.dispose(); }

逻辑说明:chart.print(gc)是Control的公开方法,内部会触发一次完整的绘制流程,把控件内容画到给定 GC 上。先建Image再建GC,是因为GC必须绑定一个绘制目标。Transform做整体缩放,比事后拉伸图片清晰得多。

参数说明:scale建议取display.getDPI().x / 96,1080P 屏是 1,4K 屏是 2,直接写死 2 在普通屏上会导出一张巨大的图。setAntialias(SWT.ON)让曲线边缘平滑,代价是导出耗时翻倍,批量导出时可以考虑关掉。落盘路径用FileDialog让用户选,别硬编码到某个盘符下。

如果导出的图只有白底、没有曲线,先检查chart.getSize()是不是 0——窗口还没layout完就导出,尺寸自然是空的,先调一次shell.layout(true, true)再导。如果图有坐标轴但曲线缺失,多半是print之前redraw还没走完,加一个chart.update()兜底。

4. 实时数据刷新:单线程模型下的环形缓冲与重绘节流

静态图谁都会画,实时曲线才是这类库的主战场。SWT 的线程模型比 Swing 更严格,跨线程碰 UI 对象直接抛异常,没有「有时候能跑」的余地。这一章讲清楚为什么不能直接刷、数据怎么缓存、刷新频率怎么控。

4.1 为什么不能在采集线程里直接 setYSeries

SWT 给每个控件记了创建它的线程 ID,任何非该线程的访问都会抛SWTException: Invalid thread access。串口接收、定时采样、socket 回调这些都在后台线程,直接在里头调series.setYSeries就是必崩,而且是那种「开发机上偶尔不崩、现场一跑就崩」的玄学故障。

// 采集线程里:不要直接碰 chart,把动作丢回 UI 线程 double[] snapshot = buffer.snapshot(); display.asyncExec(() -> { if (chart.isDisposed() || display.isDisposed()) { return; // 窗口已经关掉,丢弃这次更新 } series.setYSeries(snapshot); chart.getAxisSet().adjustRange(); chart.redraw(); });

逻辑说明:asyncExec把Runnable塞进 UI 线程的事件队列,等readAndDispatch取到它再执行,等于把跨线程调用变成了消息投递。isDisposed的判断不能省,窗口关闭后队列里可能还压着几条待执行的更新,不判就会在关闭瞬间抛异常。

参数说明:asyncExec是非阻塞的,调用方立刻返回,适合高频推送;syncExec会阻塞到 UI 线程执行完,只在必须拿到结果时用,比如导出前确认布局完成。频繁syncExec会把采集线程和 UI 线程绑死,采集一卡界面就跟着卡,实时曲线场景不要用。

另一个容易忽略的点是Display的获取方式。主窗口自己new的Display直接持有引用就行;写在工具类里的代码用Display.getDefault()或chart.getDisplay(),后者更稳,控件销毁后能第一时间发现。

4.2 环形缓冲与批量提交的实现

后台线程不能直接刷 UI,那数据就得先在内存里攒着。固定长度的环形缓冲是最省事的结构,写入 O(1),不产生垃圾对象,读的时候一次性拷出快照。

public final class RingBuffer { private final double[] data; private int head = 0; private int size = 0; public RingBuffer(int capacity) { this.data = new double[capacity]; } // 采集线程写 public synchronized void add(double v) { data[head] = v; head = (head + 1) % data.length; if (size < data.length) { size++; } } // UI 线程读,返回按时间正序排列的副本 public synchronized double[] snapshot() { double[] out = new double[size]; for (int i = 0; i < size; i++) { out[i] = data[(head - size + i + data.length) % data.length]; } return out; } public synchronized int size() { return size; } }

逻辑说明:head指向下一个写入位置,size记录当前有效元素个数,两者配合完成环绕。snapshot里的下标运算(head - size + i + len) % len是把环形展开成线性,保证返回的数组是「最老到最新」的顺序,直接喂给setYSeries就是对的。

参数说明:capacity按「刷新窗口点数 × 2」估,比如 60 秒窗口、每秒 10 个点,容量取 1200 足够;容量给太大只是多占几 KB,给太小会导致窗口里的曲线被截断。synchronized修饰两个方法保证读写不打架,代价是每次快照要短暂锁一下,1000 点量级下耗时在微秒级,可以忽略。真要极致性能可以换AtomicLongArray存 double 的位模式,但绝大多数采集场景用不上。

X 轴数据也可以一并缓存,或者干脆用「采样序号 × 采样间隔」在 UI 线程现算,后者省一份内存,前提是采样间隔稳定。

4.3 重绘节流与 adjustRange 的代价

数据每 10ms 来一个,不等于图要每 10ms 重画一次。显示器刷新率是 60Hz,人眼分辨不出 120 次和 60 次的差别,但 CPU 分得出。节流的思路是设一个最小提交间隔,攒够一批再刷。

private long lastPushNanos = 0; private static final long MIN_INTERVAL = 100_000_000L; // 100ms // 采集循环里 buffer.add(readSensor()); long now = System.nanoTime(); if (now - lastPushNanos < MIN_INTERVAL) { return; // 没到间隔就只入缓冲,不刷 UI } lastPushNanos = now; double[] ys = buffer.snapshot(); double[] xs = new double[ys.length]; for (int i = 0; i < ys.length; i++) { xs[i] = i * sampleIntervalSec; } display.asyncExec(() -> { if (chart.isDisposed()) return; series.setXSeries(xs); series.setYSeries(ys); // 固定窗口,避免每帧 adjustRange 引起的轴抖动 IAxis xAxis = chart.getAxisSet().getXAxis(0); double newest = xs[xs.length - 1]; xAxis.setRange(new Range(Math.max(0, newest - 60), newest)); chart.redraw(); });

逻辑说明:节流放在采集线程侧,不满足间隔的数据只进缓冲不提交,UI 线程收到的永远是「一批」而不是「一个点」。轴范围改成手动滑动窗口,不去调adjustRange,因为adjustRange在 1 万点上是 O(n) 的遍历,100ms 一次足以让 CPU 占用爬到两位数百分比。

参数说明:MIN_INTERVAL取 100ms 对应 10 帧/秒,曲线滚动看起来是连续的;取 200ms 会明显顿挫,取 50ms 对视觉没提升但重绘次数翻倍。窗口长度 60 秒要和缓冲容量对齐,窗口比缓冲长就会出现左侧空白。chart.redraw()之后不需要再update(),redraw只是打标记,真正的绘制发生在下一次事件循环,反复update会把消息队列压爆。

注意:如果曲线出现「跳一段」的现象,先查采集侧的节流逻辑,再看 UI 侧 asyncExec 是否被其他耗时任务堵住。SWT 的事件队列是单线程串行的,任何一个asyncExec里的耗时操作都会拖慢后面所有的更新。

5. 避坑与排查:五个把图表拖垮的现场问题

这一章都是从实际项目里扒出来的,症状、原因、处理各写一段,遇到对应现象可以直接对号。

5.1 启动报 UnsatisfiedLinkError,换个机器就好

现象:IDE 里跑得好好的,打成可执行包发给同事,双击就弹no swt-win32-xxxx in java.library.path;或者在自己机器上正常,换台 32 位系统就崩。

原因:SWT 的 native 是按平台拆包的,Maven 传递依赖带进来的是通用 jar,里面没有 dll;另一种情况是打包工具把好几个平台的 swt jar 全塞进了同一个 fat jar,类加载器先加载到错误的那一份,System.loadLibrary去找一个不存在的库名。

解决:按 2.1 的 profile 方式只引入当前平台的 native 依赖;已经打成 fat jar 的,用 shade 插件的excludes把非当前平台的 swt artifact 排除掉;临时验证可以在启动命令里加-Djava.library.path=./lib指向手动解压的 dll 目录。发布前在 Windows 和 Linux 各跑一次冒烟,别只在开发机上验。

5.2 Invalid thread access 偶发,日志里看不到堆栈

现象:实时曲线跑几分钟后界面崩掉,异常信息只有一句Invalid thread access,堆栈指向某个控件方法,看不出是哪个线程调的。

原因:某处后台线程直接改了 UI 对象。常见的是定时器回调里顺手写了series.setYSeries,或者CompletableFuture的thenAccept默认跑在 ForkJoin 线程上,忘了切回 UI 线程。SWT 的线程检查在部分控件上不一定会立刻触发,所以表现为「偶发」。

解决:全局搜setYSeries、redraw、setRange这些调用点,确认每一个都在asyncExec/syncExec里或者主线程上。工具类里加一个断言方法,在每次更新前打印Thread.currentThread().getName(),跑一轮就能定位。

5.3 导出的 PNG 是白底或者只有半张

现象:导出代码不报错,文件也生成了,打开一看只有背景,或者只有一半坐标轴。

原因:三种可能——chart.getSize()在导出时是 0(窗口还没完成布局)、导出代码跑在非 UI 线程上、chart.print之前没有触发过一次完整绘制。

解决:导出前先shell.layout(true, true),打印chart.getSize()确认为非零再继续;导出逻辑强制放在display.asyncExec里;print之前加一句chart.update()把待处理的绘制刷掉。高 DPI 环境下还要检查Image的尺寸是否按缩放比放大了,没放大的话贴到文档里会糊。

5.4 窗口关了进程不退,跑久了越来越卡

现象:点关闭按钮窗口消失,任务管理器里 java 进程还在;连续跑几个小时,内存曲线一路上扬。

原因:Color、Font、Image这些 SWT 图形资源是操作系统级的句柄,不受 JVM GC 管理,创建了不dispose就一直占着;定时器和采集线程没有在窗口关闭时停下来,还在往一个已销毁的Display里投递asyncExec。

解决:颜色和字体集中在一个资源管理器里创建、复用,窗口dispose时统一释放;Image用完立刻dispose,别指望 finalizer;shell.addDisposeListener里把running标志置 false,采集线程的循环条件读它,Thread设成 daemon 或者显式join。压测时用top -H看线程数,跑一小时数量不涨才算过关。

5.5 Y 轴一直在跳,曲线像在呼吸

现象:实时曲线每到新数据,Y 轴范围就重算一次,整条线上下浮动,读数根本没法看。

原因:每帧调adjustRange(),轴范围跟着当帧数据的最小最大值走。数据本身有噪声时,最小值最大值每帧都在变,视觉上就是轴在抖。

解决:监控场景直接固定 Y 轴量程,用setRange(new Range(0, 100))锁死,超量程的数据用变色或告警标出来,比让轴自动缩放更有价值;确实需要自适应的,加迟滞——只有当数据超出当前范围的 10% 以上才重算一次,且重算后至少保持 5 秒不再动。

6. 边界与验证:把 SWTChart 放进产品前的几道自检

用 SWTChart 做内部工具和生产客户端,验收标准不一样。内部工具能画出线就行,生产客户端要考虑跨平台、长时间运行、导出给别人看是否失真。我一般在上线前跑一遍下面这张表,任何一项不过就先别发版。

检查项具体做法不过的典型表现
跨平台 nativeWindows 与 Linux 各打一次包,干净机器上启动启动即崩,UnsatisfiedLinkError
高 DPI系统缩放调到 150% 和 200% 各看一次字体重叠、曲线发虚
长时间运行连续跑 8 小时,记录内存与线程数内存单调上涨、线程数只增不减
大数据量单系列 5 万点、10 个系列各 1 万点拖动窗口明显卡顿、重绘超过 200ms
导出导出 2 倍图,贴进 Word 看是否清晰白底、缺曲线、边缘锯齿

数据量那一项要提前有预期:SWTChart 是即时模式绘制,每帧把所有点重新画一遍,单系列超过 5 万点之后就明显吃力,10 个系列各 1 万点在中端机器上重绘一次接近 150ms。真要做高频大数据,正确做法是在进系列之前做降采样,比如每 10 个点取一个最大值一个最小值,点数降到原来的五分之一,视觉上几乎看不出差别。这个降采样逻辑放在采集线程侧,别放在asyncExec里,否则等于把计算量搬到了 UI 线程上。

还有一个我踩过的坑:图表窗口最小化之后,SWT 会跳过绘制,但asyncExec队列还在继续收数据。等窗口还原时,队列里积压的几百次更新会在一瞬间全部执行,界面直接僵住几秒。处理办法是在提交前加一层判断,chart.isVisible()为 false 时只更新缓冲不提交 UI,还原后再补一次全量刷新。

从那以后我每次接实时曲线需求,都强制先在采集侧把节流和降采样这两件事做完,再去调样式,顺序反了后面全是返工。这套流程在几个采集类项目上跑下来,稳定性比一开始「先画出来再说」的做法好太多。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进