简介SWTChart 是一套基于 SWT 的 Java 图表类库体积轻巧、调用简单适合在 Eclipse RCP 或 SWT 桌面应用中嵌入统计图形的开发者使用也可作为学习 SWT 自定义绘图与坐标系实现的参考代码。它支持线图、散点图、堆栈图、柱形图、面积图、步骤图等常用图形并具备对数标度、分类轴、多轴、轴取向与系列标签等细节能力能覆盖多数桌面端数据可视化需求。压缩包共 36 个文件约 287KB其中 24 个 java 源文件承载核心绘图逻辑与示例代码2 个 jar 可直接引入工程另有 project、classpath 等 Eclipse 工程配置以及 html 说明文档和 properties、xml 等元数据文件导入后可对照源码理解渲染机制并运行示例验证效果。目前已有 640 人学习下载适合需要轻量级 SWT 图表方案或希望研读绘图源码的 Java 开发者。1. 从一张卡死的折线图说起SWTChart 是什么、谁该用它几年前接手一个桌面端监控工具界面用 SWT 手写图表那块图省事挂了 JFreeChart结果在 Linux 上开 SWT_AWT 桥接直接变成白块右键菜单和字体渲染也全走样。后来换成 SWTChart 才消停它是纯 SWT 绘制的 Java 图表类库折线、柱状、饼图、面积、散点、气泡这些常见形态都覆盖产出物本身就是个 SWT 控件塞进 Composite 里跟着 FillLayout、GridLayout 一起走不牵扯第二套 UI 线程模型。适合三类人界面已经是 SWT/JFace、不想为几张图引入 Swing 或 JavaFX 的要把图表嵌进 RCP 视图并支持导出的被“图表卡顿”折腾过、想自己控制重绘节奏的。下面按我拆包、写 demo、二次封装的顺序讲中间那段坐标轴和线程模型是重点。2. 选型与骨架SWT 项目里为什么是它最小可跑代码怎么写2.1 三条路的技术账SWTChart、JFreeChart、JavaFX Charts在 SWT 界面里画图本质是在选“谁来当渲染基座”。JFreeChart 走的是 Java2D输出 BufferedImage 或 ChartPanel放进 SWT 只有两条路SWT_AWT 桥接或者把图渲染成图片再塞进 Label。前者在 Linux 上对 GTK 与 AWT 的混用很敏感窗口嵌入、焦点、输入法都容易出玄学问题后者等于放弃了所有交互鼠标悬停、点击选中、框选缩放全没有。JavaFX Charts 能力更现代但意味着打包时要带一套 JavaFX 运行时模块化之后还得处理 jlink 与原生库对一个内部工具来说偏重。SWTChart 的定位很清楚自己继承 Canvas用 GC 直接画坐标轴、图例、网格、系列全部是自家对象模型。好处是零桥接、零额外运行时坏处也要认——它没有内置动画缩放平移这类交互在核心包里要自己写官方另有一个 extensions 包提供可滚动的图表壳子功能才补齐。我一般这样划界静态报表图、实时监控图用核心包足够需要鼠标拖拽缩放、框选区间、多轴联动的直接上 extensions 里的可滚动图表别自己造轮子。维度SWTChartJFreeChartJavaFX Charts渲染基座SWT GCJava2DJavaFX 场景图与 SWT 集成原生控件直接 add 到 Composite需桥接或转图片需独立运行时混用麻烦交互能力核心包较弱extensions 补齐缩放平移中等依赖 ChartPanel较强打包体积只多一个 jar中等偏大上手成本低API 面窄中中高还有一条容易被忽略SWT 的 Color、Font、Image 都是需要手动释放的系统资源。SWTChart 的 API 大量接收 Color 参数如果你习惯性地new Color(display, 255, 0, 0)几千次刷新之后就是句柄泄漏。正确姿势是display.getSystemColor(SWT.COLOR_RED)或者自己建一个调色板单例统一管理退出时统一 dispose。这条在后面避坑章还会再提一次。2.2 最小可跑骨架四十行代码出一张折线图先别急着封装把最小闭环跑通一个 Shell、一个 SwtChart、一条折线系列、一次 adjustRange。这段代码我建议你直接抄进 IDE 里跑一遍后面所有参数都是在这个骨架上加。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.ILineSeries; import org.eclipse.swtchart.ISeries.SeriesType; import org.eclipse.swtchart.LineStyle; import org.eclipse.swtchart.SwtChart; import org.eclipse.swtchart.ISeries; public class ChartSkeleton { public static void main(String[] args) { Display display new Display(); Shell shell new Shell(display); shell.setText(SWTChart 最小骨架); shell.setLayout(new FillLayout()); shell.setSize(880, 520); // 1. 图表控件父容器是 shell样式先给 NONE SwtChart chart new SwtChart(shell, SWT.NONE); // 2. 建系列类型 LINEid 一旦定下就别随便改后面刷新要靠它找回来 ISeries raw chart.getSeriesSet().createSeries(SeriesType.LINE, cpu); ILineSeriesdouble[] series (ILineSeriesdouble[]) raw; // 3. 造数据24 个小时点X 必须是递增的 double 数组 double[] x new double[24]; double[] y new double[24]; for (int i 0; i 24; i) { x[i] i; y[i] 40 25 * Math.sin(i / 3.0); } series.setXSeries(x); series.setYSeries(y); // 4. 线条样式颜色走系统色板避免自建 Color 泄漏 series.setLineColor(display.getSystemColor(SWT.COLOR_DARK_CYAN)); series.setLineWidth(2); series.setLineStyle(LineStyle.SOLID); series.setSymbolType(ISeries.SymbolType.NONE); // 点多了必关否则画符号画到卡 // 5. 轴标题与范围 chart.getAxisSet().getXAxis(0).getTitle().setText(小时); chart.getAxisSet().getYAxis(0).getTitle().setText(负载 / %); chart.getAxisSet().adjustRange(); // 不调这句轴范围不会跟着数据走 shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } }几个关键点值得单独说。SwtChart的构造函数只认 Composite所以它天然能被任何布局管理器接管createSeries返回的是ISeries接口必须根据SeriesType强转成ILineSeries、IBarSeries、IPieSeries转错了运行期直接 ClassCastException编译器不会提醒你。系列 id 是字符串后续getSeriesSet().getSeries(cpu)靠它取回对象所以命名要有约束别用“series1”这种一会儿就撞车的名字。setXSeries与setYSeries分两次调用长度必须一致这是硬约束。adjustRange()是新手最容易漏的一步它按所有可见系列的数据重新算轴范围不调用的话你会看到一条线贴着图边缘或者干脆在可视区外。SymbolType.NONE也建议默认关掉符号绘制是逐点画的几千点以上开销肉眼可见。注意不同发行版本的类名有差异较新版本主类是SwtChart老代码里常见的Chart是同族兼容类常量如IMAGE_FORMAT_PNG定义在图表主类上导入时以 IDE 补全为准不要照抄网上的老片段。3. 坐标轴与主题把默认灰底图调成能交付的样子3.1 轴对象模型主副轴、网格与范围SWTChart 的轴不是“图表的属性”而是一组独立对象通过IAxisSet管理。X 轴默认只有一条Y 轴可以创建多条用来做双单位叠加——比如左轴温度、右轴压力。createYAxis()会返回新的IYAxis索引从 1 开始然后把这个索引塞给系列series.setYAxisId(1)。这一步顺序不能反先建轴再指定否则系列还是挂在 0 号轴上。网格线挂在轴上而不是图表上getGrid()拿到的对象控制该轴对应的那组网格。交付级图表里我通常把 X 轴网格关掉、Y 轴保留虚线视觉上干净很多。轴的位置也有讲究setPosition(AxisPosition.Secondary)能把 Y 轴挪到右侧做双轴时副轴必须挪不然两条轴的刻度会叠在左边糊成一团。IAxisSet axisSet chart.getAxisSet(); // 主 X 轴范围、标题、刻度标签角度 IXAxis xAxis axisSet.getXAxis(0); xAxis.getTitle().setText(采样时间 / s); xAxis.setRange(new Range(0, 3600)); // 固定范围避免实时刷新时轴来回跳 xAxis.getTick().setTickLabelAngle(45); // 标签长了必须转否则互相压字 xAxis.getGrid().setVisible(false); // X 向网格关掉 // 主 Y 轴网格改成虚线颜色调浅 IYAxis yAxis axisSet.getYAxis(0); yAxis.getTitle().setText(温度 / ℃); yAxis.getGrid().setVisible(true); yAxis.getGrid().setStyle(LineStyle.DASH); yAxis.getGrid().setForeground(display.getSystemColor(SWT.COLOR_GRAY)); // 副 Y 轴右置绑定到第二个系列 IYAxis yAxis2 axisSet.createYAxis(); yAxis2.getTitle().setText(压力 / kPa); yAxis2.setPosition(AxisPosition.Secondary); secondSeries.setYAxisId(1); axisSet.adjustRange();setRange传的是Range对象不是两个 double这是 API 上的小门槛。实时图里我倾向于固定范围而不是每帧 adjustRange轴范围一跳整张图会“呼吸”看久了眼睛累而且刷新时重算范围本身有开销。固定范围 数据裁剪是更稳的做法超范围的数值在系列里截掉或者画到边界上。刻度格式由getTick().setFormat(Format)控制参数是java.text.Format所以DecimalFormat、SimpleDateFormat都能直接塞进去。时间轴用SimpleDateFormat(HH:mm)数值轴用DecimalFormat(#,##0.0)比默认的科学计数法友好得多。刻度标签角度 0 到 90 度之间可调超过 45 度之后标签会往上顶记得给底部留白。3.2 主题与配色别在业务代码里硬编码颜色SWTChart 内置了主题机制setTheme(ThemeId.XXX)一次性把背景、网格、轴线的配色换掉枚举里有 CLASSIC、GRAY、GRAPHITE 这几档风格深色面板用 GRAPHITE浅色报表用 CLASSIC 或默认。主题只改“骨架色”系列颜色还是得自己设所以实际项目里我会定义一个小调色板类把 8 到 10 个业务语义色正常、告警、严重、离线映射成系统色或一次性创建并复用的 Color 实例。面积图是个例外ILineSeries打开setEnableArea(true)之后会填充曲线下方区域setAreaColor控制填充色。填充色建议带透明度或者选很浅的色否则多条面积图叠在一起就是灾难。柱状图的系列是IBarSeriessetBarColor、setBarWidth两个参数就够柱宽是相对值不是像素改成 0.5 会明显变细。主题切换有个副作用它会重置部分轴与网格样式。所以正确顺序是先setTheme再设轴细节最后设系列样式。我见过有人先精心调完网格再切主题一切回到解放前然后怀疑是 API 有 bug——其实是调用顺序问题这种坑后面还会重复出现。// 主题先行 chart.setTheme(ThemeId.GRAPHITE); chart.setBackground(display.getSystemColor(SWT.COLOR_WHITE)); // 系列样式后设避免被主题覆盖 series.setLineColor(display.getSystemColor(SWT.COLOR_DARK_CYAN)); series.setLineWidth(2); series.setEnableArea(true); // 面积填充 series.setAreaColor(display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW));饼图比较特殊它不消费坐标轴数据所以建完之后要把两条轴的刻度藏掉否则图旁边会挂着两条毫无意义的空轴IPieSeries pie (IPieSeries) chart.getSeriesSet().createSeries(SeriesType.PIE, share); pie.setDataVector(new double[] {35, 25, 20, 20}); pie.setVisibles(new boolean[] {true, true, true, true}); // 轴隐藏图例保留 chart.getAxisSet().getXAxis(0).getTick().setVisible(false); chart.getAxisSet().getYAxis(0).getTick().setVisible(false); chart.getLegend().setVisible(true);setVisibles的布尔数组用来控制哪些扇区显示配合图例点击可以实现“过滤某类占比”的效果这个交互自己接一下鼠标事件就能做比重新建系列省事。饼图的多系列叠加它支持得一般两块饼摞在一起会互相遮挡真要做对比就并排放两个图表控件。4. 实时刷新与大数据量SWT 线程模型下的三条性能红线4.1 线程红线只有 UI 线程能碰控件SWT 的线程模型比 Swing 更严格任何控件方法都必须在创建它的 Display 线程里调用后台线程直接series.setYSeries(...)会抛SWTException: Invalid thread access。这条错误信息很直白但真正的坑在于它有时不立刻抛——比如你只改了数据没触发重绘程序看着没事等到下一次redraw()才炸堆栈还指在莫名其妙的地方。标准解法是把数据更新包进display.asyncExec(Runnable)。asyncExec是异步投递UI 线程空闲时执行不会阻塞采集线程syncExec是同步等待后台线程会一直卡到 UI 线程跑完这段代码。两者的选择很清楚采集线程只负责塞数据永远用 asyncExec如果后台线程需要等 UI 更新完才能继续比如要读回控件状态才用 syncExec。反过来说如果 UI 线程里调用 syncExec而那个 Runnable 又在等 UI 线程就是自己等自己直接死锁。这种死锁在日志里表现为界面整个冻住、CPU 占用为 0非常难查。// 采集线程只往缓冲里写不碰任何控件 new Thread(() - { while (running) { double value readSensor(); // 假设这是你的数据源 synchronized (bufferLock) { ringBuffer.add(value); // 环形缓冲容量固定 } try { Thread.sleep(20); // 50Hz 采样 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }, sampler).start(); // UI 线程定时把缓冲快照刷进图表 display.timerExec(200, new Runnable() { Override public void run() { if (chart.isDisposed()) { return; } double[] snapshot; synchronized (bufferLock) { snapshot ringBuffer.toArray(); // 一次性拷贝缩短持锁时间 } series.setYSeries(snapshot); chart.redraw(); display.timerExec(200, this); // 自我重排形成固定节奏 } });4.2 刷新节奏别让每个数据点都触发重绘50Hz 采样不等于 50Hz 重绘。redraw()会走一遍完整的绘制流程清背景、画网格、画轴、画系列、画图例几万个像素操作一秒钟调五十次CPU 单核直接吃满。经验值是 5 到 10Hz 的界面刷新就足够肉眼流畅也就是 100 到 200 毫秒一次timerExec正是干这个的它比Thread.sleep循环稳也不会因为异常把定时链断掉。setYSeries每次都会拷贝一份数组所以刷新时传的是快照而不是缓冲引用避免采集线程在绘制过程中改数据导致画到一半内容变了。这个“快照”动作要在锁里完成出了锁再调控件方法把持锁时间压到最短。还有一个隐形开销是adjustRange()。它要遍历所有系列求极值数据量大时不是常数时间。实时图里我通常固定范围只在用户点“自适应”按钮时调一次如果确实需要跟随把频率降到 1Hz 以内别跟着每次刷新走。4.3 大数据量降采样与绘制开关几千点以内SWTChart 基本无压力上到几万点就能明显感觉到窗口拖动时的迟滞十万点以上如果不做处理鼠标划过都会卡。可用的手段按性价比排序关掉符号绘制SymbolType.NONE、关掉系列抗锯齿、关掉不需要的网格、把线宽降到 1、用降采样把点数压到可视宽度的两倍以内。降采样最简单的做法是等间隔抽稀但会丢掉尖峰。监控场景我更常用的是“分段极值法”把数据按像素宽度分段每段保留最大值和最小值两个点这样毛刺不会消失视觉上也不失真。数据点数建议处理说明 5k直接绘关闭符号大多数监控够用5k ~ 50k抽稀到屏幕宽度的 2 倍每像素两三个点足够 50k分段极值 关闭网格与抗锯齿保留尖峰牺牲平滑需要缩放平移换 extensions 的可滚动图表它内部做了视口裁剪抗锯齿这件事有点看版本部分版本在图表级提供setAntialias开关没有的话就在主题里找对应项或者接受默认。判断方法很简单画一万个点拖动窗口看是否掉帧掉了就逐个关开关做对比一次只改一个变量这种排查方式比猜快得多。5. 避坑与排查五个让我返工的细节5.1 Invalid thread access 出现在“看起来是 UI 线程”的地方现象定时器里更新图表没问题换成消息回调就偶发崩堆栈指向setYSeries。原因回调来自网络或串口线程不是创建 Display 的那个线程。解决所有跨线程更新统一走display.asyncExec并且封装成一个UiExec.run(Runnable)工具方法业务代码不许直接调控件。判断当前线程是否 UI 线程用Display.getCurrent() ! null但别拿它当分支逻辑直接投递更省心。5.2 createSeries 强转类型失败现象ClassCastException提示LineSeries cannot be cast to BarSeries。原因SeriesType和强转目标不一致或者复用了一个已被其他类型占用的 id。解决系列 id 与类型一一对应命名上加前缀比如line_cpu、bar_disk建系列时立刻强转并保存到成员变量不要每次用的时候从 SeriesSet 里取出来再强转。5.3 数据变了图却纹丝不动现象日志打印数据是对的界面还是老曲线。原因有三层按概率排一是忘了redraw()二是没调adjustRange()新数据落在轴范围外看不见三是定时链断了timerExec抛出异常后没有重新排期。解决刷新方法里按setYSeries → adjustRange → redraw三件套固定顺序写timerExec外面包 try-catch 并在 finally 里重排。5.4 跑一晚上内存涨了几百兆现象长时间运行后内存持续上涨GC 后也降不下来。原因循环里new Color(display, r, g, b)、new Font(...)、new Image(...)没有 disposeSWT 资源不进 Java 堆只进 Native 句柄。解决颜色字体做常量或单例随图表控件一起在dispose时释放用display.getSystemColor尽量替代自建 Color。血泪经验是这类泄漏不看堆快照要看系统句柄数。5.5 导出的 PNG 是空白或者被裁掉一半现象界面上好好的save出来的图空白、分辨率不对或者右边缘缺一块。原因导出时机太早控件还没完成布局拿到的 bounds 是 0 或者旧尺寸也可能是把 save 放在了非 UI 线程。解决导出前先chart.layout()或者等paint事件走完在 UI 线程里调用需要固定尺寸就用带 Rectangle 参数的重载显式指定宽高别依赖控件当前大小。提示SWT 原生库加载失败no swt-gtk in java.library.path跟图表 API 无关是运行环境缺平台依赖先在最小空窗口上验证 SWT 本身能起来再排查图表代码能省一半时间。6. 导出与封装一个能复用的图表工厂外加两个导出参数图表代码写到第三个界面就开始重复了轴标题、角度、网格、系列样式每次抄一遍。我一般收口成一个工厂类把“图表骨架”和“系列样式”分开工厂负责建控件、设主题、设轴加系列的方法只负责颜色、线宽、符号。这样换主题只改一处加新图只写两行。public final class ChartFactory { private ChartFactory() {} /** 统一骨架主题、轴标题、标签角度、网格 */ public static SwtChart lineChart(Composite parent, String xTitle, String yTitle) { SwtChart chart new SwtChart(parent, SWT.NONE); chart.setTheme(ThemeId.GRAPHITE); chart.getAxisSet().getXAxis(0).getTitle().setText(xTitle); chart.getAxisSet().getYAxis(0).getTitle().setText(yTitle); chart.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(45); chart.getAxisSet().getXAxis(0).getGrid().setVisible(false); chart.getAxisSet().getYAxis(0).getGrid().setStyle(LineStyle.DASH); return chart; } /** 加一条折线颜色由调用方传避免工厂里硬编码业务语义 */ public static ILineSeriesdouble[] addLine(SwtChart chart, String id, Color color, int width) { ILineSeriesdouble[] s (ILineSeriesdouble[]) chart.getSeriesSet().createSeries(SeriesType.LINE, id); s.setLineColor(color); s.setLineWidth(width); s.setSymbolType(ISeries.SymbolType.NONE); return s; } }导出这段有两个参数值得掰开说。第一个是格式常量IMAGE_FORMAT_PNG、IMAGE_FORMAT_JPEG、IMAGE_FORMAT_BMP三选一PNG 无损适合贴文档JPEG 体积小但曲线边缘会有压缩噪点我不建议用。第二个是尺寸不传尺寸就按控件当前像素导出高分屏上会得到一张很大的图插进 Word 直接超页传一个固定Rectangle能让所有导出图尺寸一致报告排版省事。// UI 线程中调用先布局再按固定尺寸导出 chart.layout(); Rectangle bounds new Rectangle(0, 0, 1600, 900); chart.save(/data/report/cpu-trend.png, SwtChart.IMAGE_FORMAT_PNG, bounds);打印走Printer思路一样先在 UI 线程里把图表画到打印 GC 上再printer.print()别忘了在finally里printer.dispose()——打印机对象同样是 SWT 资源漏掉一次就少一个句柄。封装完之后我给自己定了个规矩任何新图表先跑最小骨架确认数据能出来再叠轴和主题最后才接实时刷新。跳过第一步直接写实时逻辑出问题时你分不清是数据、轴范围还是线程的锅。从那以后我每次接新图表需求都强制走一遍这个顺序返工次数少了不止一半。这套顺序和上面那几个参数希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?