带过几轮刚学完 Java 基础的新人之后我发现一个挺有意思的现象语法题刷得再溜一到写个能双击打开的窗口程序就集体卡壳。大家第一反应是 Swing教材上都是它第二反应是 JavaFX一看打包配置就劝退。中间其实还有一条路——SWT 框架它不自己画控件而是直接调用操作系统的原生控件做出来的界面跟系统里的记事本、资源管理器是一个长相启动快、内存也不难看。这篇阶段练习的复盘就是围绕 SWT 展开的从一个空白窗口开始一步步做出一个有列表、有搜索、有表单校验、有文件导入导出的小工具把控件、布局、事件、资源释放、线程这几块硬骨头啃一遍。适合刚学完 Java SE、想碰一碰桌面开发的初学者也适合从 Swing 转过来、被 SWT 的dispose()和线程访问搞懵过一次的人。我会把每一步为什么这么设计讲清楚参数怎么算、坑长什么样都按我实际踩过的写下来。1. 阶段练习练什么先把目标拆成能验收的清单1.1 为什么这个阶段练习选 SWT 而不是别的选型这件事我一般先问三个问题跑得快不快、学到的知识能不能迁移、出错的时候能不能定位。SWT 在这三点上对初学者比较友好。它是 JNI 调用本地控件界面渲染交给操作系统Java 层只负责组织逻辑所以窗口拖动的跟手程度和系统原生程序没差别控件模型非常规矩每个控件都是父子树的节点父容器释放时子节点跟着走这种树形思维学一次能用很久出错信息也相对直白Widget is disposed、Invalid thread access这类提示虽然吓人但对应的原因基本是唯一的。更重要的是SWT 强制你面对两个真实工程问题资源必须手动释放、UI 只能在一个线程里更新。Swing 里 garbage collector 帮你兜底的东西SWT 全要你自己管。初学者第一次被这两个问题咬到后面写任何 GUI 代码都会长记性。这也是我把 SWT 放在阶段练习位置的原因——它不教你花哨的动画但教你把一个界面程序的骨架搭正。另外提一句 JFace。JFace 是架在 SWT 上面的一层封装主要解决表格、树、列表的数据绑定问题TableViewer就是它的东西。练习阶段我建议先用裸Table手动填数据把增删改查的逻辑写明白等你嫌麻烦了再上TableViewer这时候你才知道它帮你省了多少事。反过来先用 JFace你会觉得它在变魔术出问题就抓瞎。1.2 练习项目的功能边界与验收标准标题写的是阶段练习那就必须有个能验收的成品否则写到一半就没劲了。我给自己定的题目是一个本地联系人管理器左边是分组列表中间是联系人表格右边是编辑表单顶部有搜索框底部是状态栏。功能不多但每一个都能对应到一块知识点。功能点考察的核心能力验收标准主窗口分栏显示SashForm、CTabFolder、混合布局拖动分隔条时两侧控件自适应不出现遮挡表格展示联系人Table / TableColumn、表头与列宽列宽合理选中行能拿到完整数据顶部实时搜索ModifyListener、数据过滤、刷新策略输入一个字符结果即时变化无卡顿右侧表单录入Text、Combo、Button 组合、布局对齐标签与输入框基线对齐窗口拉大不走形输入合法性校验VerifyListener、焦点事件电话号码只允许数字非法字符打不进去保存与导入导出FileDialog、Properties 或简单文本格式用户取消对话框时不抛异常关闭窗口清理DisposeListener、资源枚举反复开关 50 次内存不持续增长把验收标准写出来这一步别省。我见过太多人做到一半开始加功能加着加着自己都不知道要做什么了。清单定死之后做不下去了回头看清单缺哪块补哪块。另外提一个阶段性的做法先做出能跑通主流程的版本能增删改查、能保存再回头补细节校验、快捷键、图标、状态栏提示。一上来就纠结图标尺寸和字体很容易在第一天就放弃。2. 环境搭建与工程骨架先把跑不起来这件事解决2.1 依赖引入与平台包的选择逻辑SWT 和普通 jar 最大的区别是它不是跨平台的单一包。因为它调用本地库所以每个操作系统、每种 CPU 架构都对应一个独立的构件里面带着对应的本地动态库文件。你在 Windows 上引入的包拷到别的机器上跑不了。这就解释了为什么很多人第一次 importorg.eclipse.swt.widgets.Shell就报红——依赖没选对平台。用 Maven 的话坐标大致是这样的版本号去仓库里挑一个和你 JDK 匹配的即可dependency groupIdorg.eclipse.platform/groupId artifactIdorg.eclipse.swt.win32.win32.x86_64/artifactId version3.124.0/version /dependency !-- 需要 TableViewer 的话再补这一层 -- dependency groupIdorg.eclipse.platform/groupId artifactIdorg.eclipse.jface/artifactId version3.30.0/version /dependency这里的win32.win32.x86_64拆开看是窗口系统-操作系统-架构换平台就整段换掉。JDK 版本也要一起看SWT 的 3.12x 系列基本要求 JDK 11 起步如果你还在用 JDK 8那就得往回找老版本不然会在启动时抛UnsupportedClassVersionError。这个错和 SWT 本身没关系纯粹是字节码版本对不上别往框架上赖。非 Maven 的同学直接下那个平台的 jar 丢进 classpath 也行但要记得里面带的.dll或.so、.dylib文件必须能被 JVM 加载到。常见的UnsatisfiedLinkError: no swt-win32-xxxx in java.library.path就是这一环断了——要么 jar 下错了平台要么你把 jar 解压后只拷了 class 没拷本地库。我的建议是老老实实用构建工具它能帮你把这一步做对。2.2 最小可运行骨架Display 与 Shell 到底是什么关系所有 SWT 程序都长一个样先把这段代码背下来比什么都管用import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.layout.FillLayout; public class App { public static void main(String[] args) { // Display 是和操作系统打交道的那一层一个线程只能有一个 Display display new Display(); // Shell 是窗口必须有 Display 才能创建 Shell shell new Shell(display); shell.setText(联系人管理器 - 阶段练习); shell.setSize(900, 600); shell.setLayout(new FillLayout()); shell.open(); // 事件循环没有这段窗口一闪就没了 while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } }理解这段代码的关键是把Display当成一个前台接待员。它负责从操作系统那里取事件鼠标点击、键盘输入、窗口重绘readAndDispatch()就是看看有没有新事件有就派发下去没有就sleep()让线程挂起别空转烧 CPU。while (!shell.isDisposed())保证窗口关掉之后循环能退出。初学者最容易犯的错是写完shell.open()就以为完事了程序直接main返回窗口闪一下就消失。也有人在循环里写成display.readAndDispatch();而不判断返回值结果就是 CPU 单核跑满、风扇狂转。这两个坑我都亲手挖过。还有一个小知识点Display的创建线程就是 UI 线程。SWT 不像 Swing 有明确的 EDT 说法但规则一样——控件只能在创建它的那个线程里操作。这条规则后面第 5 节会专门讲。2.3 工程目录与资源管理的前置约定骨架跑通之后先别急着堆控件花十分钟把目录和人约定好后面能省很多事。我的习惯是这样src/main/java └─ app ├─ App.java 启动与事件循环 ├─ ui/ 界面组装一个类负责一块 │ ├─ MainWindow.java │ └─ EditorPanel.java ├─ model/ 纯数据对象不 import 任何 swt 包 ├─ store/ 读写文件 └─ util/ 常量、格式化、校验 src/main/resources └─ icons/ 图标按 DPI 分目录这里有一条硬规矩model包里不许出现import org.eclipse.swt。听起来有点教条但好处很实在——数据层能单独写单元测试将来真要换 UI 框架数据逻辑一行不动。我见过把TableItem直接当数据模型用的写法结果想加个排序功能发现数据全靠界面控件撑着只能推倒重来。资源目录也一样图标、字体文件、初始数据模板都放在resources下代码里通过getClass().getResourceAsStream()读。别用相对路径new File(icons/add.png)在 IDE 里跑得好好的打成 jar 就找不到文件了这个坑几乎人人踩一次。3. 界面布局SWT 里最容易翻车的一环3.1 五种布局管理器的取舍与适用场景SWT 不自带绝对坐标 自适应那一套控件的摆放完全交给布局管理器。刚上手的人常常直接setBounds()硬编码坐标窗口一拉大就全乱。所以布局是必须过的坎。常用的就五个我把它们的脾气整理成一张表布局类分配逻辑适合什么典型翻车点FillLayout所有子控件等分剩余空间单控件填满、标签页内容放两个以上控件时大小完全一样没法调RowLayout横向或纵向依次排列可设间距一行按钮、一列输入项不给 RowData 宽度时输入框被压成一条缝GridLayout网格按列对齐支持跨列表单、复杂面板首选忘了设grabExcessHorizontalSpace拉大窗口控件不长FormLayout相对定位支持百分比需要精细对齐的少量控件附件关系写错会互相挤没StackLayout多个控件叠放同时只显示一个同一区域的页面切换忘了设topControl什么都看不见选择顺序我的经验是能 GridLayout 就 GridLayout。它虽然参数多但行为最好预测出问题也能一眼看出是列数不对还是 grab 没设。FormLayout 适合做标签贴左上、按钮贴右下这种少数场景控件一多附件关系会变成一团乱麻。FillLayout 我只在两种情况下用单个控件铺满或者放了SashForm、CTabFolder这种自带分配逻辑的容器。StackLayout是个例外它在练习项目里很有用——右侧编辑区可以准备空状态页和表单页两个面板选中表格行就切到表单页取消选中就切回空状态页。切换只要一行stack.topControl formPage; parent.layout();。注意改完topControl必须再调一次父容器的layout()不然界面不会重绘这个点特别容易忘。3.2 GridLayout 的列宽与 grab 规则参数怎么算GridLayout的核心就两个数字列数、是否等宽。new GridLayout(2, false)表示两列、各列宽度由内容决定。列宽的计算规则是每一列取该列中最宽子控件的宽度所以第一列放标签、第二列放输入框天然就是对齐的。窗口变宽时多出来的空间分给谁分给GridData里grabExcessHorizontalSpace true的那些控件。这是整个 SWT 布局里最需要记牢的一句话。看一段实操代码Composite form new Composite(parent, SWT.NONE); form.setLayout(new GridLayout(2, false)); Label lblName new Label(form, SWT.NONE); lblName.setText(姓名); lblName.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false)); Text txtName new Text(form, SWT.BORDER); GridData gdName new GridData(SWT.FILL, SWT.CENTER, true, false); gdName.widthHint 180; // 最小可视宽度防止被压没 gdName.minimumWidth 120; // 窗口缩小时的兜底 txtName.setLayoutData(gdName);GridData的构造函数四个参数依次是水平对齐、垂直对齐、是否抢占水平剩余空间、是否抢占垂直剩余空间。第三位写true的那个控件就是窗口拉伸时的赢家。如果把注释框也设成true两个控件会平分多出来的空间——有时候你想要这个效果有时候不想要得想清楚。还有两个经常用的字段horizontalSpan用于跨列比如一段长备注要横跨标签列和输入列就设成2verticalAlignment建议统一写SWT.CENTER让标签和输入框在垂直方向居中对齐视觉上才不会一高一低。我看到有人用SWT.BEGINNING单行输入框还行一旦旁边是带边框的多行文本标签就会往上顶看着别扭。3.3 混合布局实战SashForm 加 CTabFolder 拆主界面真实界面很少只用一种布局混合使用是常态。我的主窗口结构是这样的最外层FillLayout放一个SashForm可拖动的分隔容器左侧放分组列表右侧放一个CTabFolder两个标签页分别是表格视图和详情视图。这样拆的好处是每个区域职责单一改一处不影响别处。import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabItem; SashForm sash new SashForm(shell, SWT.HORIZONTAL); sash.setLayout(new FillLayout()); // SashForm 内部也要有布局 List lstGroup new List(sash, SWT.BORDER | SWT.SINGLE); CTabFolder folder new CTabFolder(sash, SWT.BORDER); folder.setSimple(false); // false 才有圆角标签效果 CTabItem tabTable new CTabItem(folder, SWT.NONE); tabTable.setText(列表); CTabItem tabDetail new CTabItem(folder, SWT.NONE); tabDetail.setText(详情); // 分栏比例左侧 1 份、右侧 4 份 sash.setWeights(new int[]{1, 4});SashForm有一个小脾气它的子控件不能直接设GridData因为它的布局必须是FillLayout或者RowLayout。如果你在SashForm里塞了一个Composite那这个Composite内部再自己设GridLayout这是正确姿势。反过来把GridData用在SashForm的直接子控件上会静默失效控件大小莫名其妙我第一次遇到排查了半小时。setWeights要在所有子控件加完之后再调中途调用会被后续的添加操作覆盖。另外CTabFolder和TabFolder的区别值得知道TabFolder是系统原生标签页长得朴素CTabFolder是自定义绘制的支持关闭按钮、渐进色、圆角做工具类软件一般选后者。两者用法几乎一样但CTabFolder属于custom包别 import 错了。4. 控件与事件把能点变成能用4.1 常用控件速查与风格常量的组合套路SWT 控件的构造函数第二个参数是风格常量用按位或组合。这套设计有点老派但表达力很强很多行为一个常量就搞定了。常用的组合我列一下基本覆盖日常需求的八成控件常用风格组合效果说明TextSWT.BORDER单行带边框输入框TextSWT.BORDER | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL自动换行的多行文本域TextSWT.BORDER | SWT.READ_ONLY只读展示仍可复制ButtonSWT.PUSH普通按钮ButtonSWT.CHECK复选框ButtonSWT.RADIO单选同一父容器内互斥ButtonSWT.TOGGLE可切换状态的按钮TableSWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI整行选中支持多选ComboSWT.DROP_DOWN | SWT.READ_ONLY只读下拉框防止用户乱输有两个地方新手特别容易漏。第一Table想要显示表头必须显式table.setHeaderVisible(true)光加TableColumn是看不见的想要网格线还得setLinesVisible(true)。第二Combo不加SWT.READ_ONLY用户就可以往里面敲任意文本如果你的后续逻辑默认它一定是预设值之一就会拿到意外数据。这个坑我在做分组下拉框时踩过用户输入了一个不存在的分组名保存时直接报空指针。Button的单选组还有个细节互斥范围是同一个父容器。也就是说你把两组单选按钮放在同一个Composite里它们会串成一组。正确做法是每组单选各给一个子Composite。4.2 事件监听模型为什么 SWT 用监听器而不是回调赋值SWT 的事件模型是控件 监听器 事件对象比如button.addSelectionListener(new SelectionAdapter(){...})。有人会问为什么不像某些框架那样直接button.onClick ...。原因在于一个控件往往需要多个监听一个输入框可能同时要ModifyListener内容变了、VerifyListener输入前拦截、FocusListener焦点进出、TraverseListenerTab 键行为。用监听器列表的方式谁都能加互不覆盖。SelectionAdapter和SelectionListener的关系也值得说一句。接口里有两个方法widgetSelected和widgetDefaultSelected前者是点了/选中了后者是双击了/回车了。用SelectionAdapter只覆写你关心的那个代码干净。但要注意按钮的默认行为是靠widgetSelected触发的如果你不小心覆写错了方法按钮看着能点但没反应排查起来还挺费劲。事件对象里带了很有用的信息。SelectionEvent有widget、item列表或表格里具体哪一项、detail比如勾选框的选中状态。写表格双击编辑功能时e.item就是被双击的那一行取数据比自己去查选中索引靠谱得多。4.3 输入校验VerifyListener 与 ModifyListener 的分工校验是练习项目里最值得花时间的一块也是最容易做错的地方。VerifyListener触发在文本真正写入之前可以修改或否决ModifyListener触发在写入之后用来做事后反应比如实时过滤。两者职责完全不同别混用。电话号码只允许数字正确写法是Text txtPhone new Text(form, SWT.BORDER); txtPhone.addVerifyListener(e - { // e.text 是即将插入的内容粘贴也会走这里 if (!e.text.matches([0-9]*)) { e.doit false; // 否决这次修改 } });这里e.text的关键作用是粘贴也能拦住。很多人只在KeyListener里判断按键是不是数字结果用户一个 CtrlV 就绕过校验了这个漏洞太常见。用VerifyListener从数据入口拦截才是从根上解决。但有一点要注意VerifyListener里不要弹MessageBox。它可能在用户每敲一个字符时触发一次弹框会疯狂闪而且模态框会干扰输入法状态。想给提示就改旁边的Label文字或者把输入框边框颜色变一下。ModifyListener的典型用途是实时搜索。做法是在监听器里取当前文本过滤数据列表然后刷新表格。这里有个性能问题如果数据量大每敲一个字符就重建一次表格项会明显卡顿。我的处理是加一个Display.getCurrent().timerExec(200, ...)做防抖200 毫秒内连续输入只触发最后一次。这个技巧在 SWT 里很实用timerExec是 UI 线程上的定时器不用担心线程问题。private Runnable pendingFilter; txtSearch.addModifyListener(e - { String kw txtSearch.getText().trim(); if (pendingFilter ! null) { display.timerExec(-1, pendingFilter); // 取消上一次待执行 } pendingFilter () - refreshTable(filterByKeyword(kw)); display.timerExec(200, pendingFilter); });timerExec(-1, runnable)是取消任务的写法参数传负的毫秒数。这段代码第一次看我以为是黑魔法其实就是一个很朴素的防抖。4.4 表格裸 Table 与 TableViewer 的取舍判断练习阶段我建议先用裸Table理由前面说过——把增删改查写透。裸Table的操作流程是先建列再建TableItem每行一个 itemitem.setText(int index, String value)填值。刷新时最直接的做法是table.removeAll()再全量重建。数据量几十上百条完全没问题超过几千条才会卡。Table table new Table(tabTable.getParent(), SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); table.setHeaderVisible(true); table.setLinesVisible(true); String[] heads {姓名, 电话, 分组, 备注}; int[] widths {120, 140, 100, 260}; for (int i 0; i heads.length; i) { TableColumn col new TableColumn(table, SWT.LEFT); col.setText(heads[i]); col.setWidth(widths[i]); }列宽这块有个小设计初始宽度我是拍脑袋给的但要在窗口拉伸时保持合理就得在 resize 事件里重新分配。做法是监听父容器的ControlListener把最后一列宽度设为总宽减去前面各列之和让表格始终填满。这比给每列都设死宽度好看得多。TableViewer的优势是数据绑定setInput(list)之后表格自己重建配合ITableLabelProvider决定每列显示什么字段。好处是数据和界面解耦坏处是多了一层概念要学。我的建议是等你在Table里写得手指疼并且开始为排序后怎么保持选中项这种问题发愁时再切过去那时候你会觉得它真香。5. 资源释放与线程模型SWT 最容易埋雷的两件事5.1 谁创建谁释放Color、Font、Image 的生命周期这是 SWT 和 Java 世界其他地方最不一样的地方。Color、Font、Image、Cursor、GC这些持有本地资源的对象GC 管不了必须显式dispose()。不释放的后果是本地资源句柄持续增长跑一阵子就会抛SWTError: No more handles然后整个程序崩掉。规矩很简单谁 new 的谁负责 dispose且在创建它的父容器被销毁之前释放。跟着控件走的一般挂在DisposeListener上final Color warnColor new Color(display, 0xC0, 0x39, 0x2B); Label lblTip new Label(form, SWT.NONE); lblTip.setForeground(warnColor); lblTip.addDisposeListener(e - { if (warnColor ! null !warnColor.isDisposed()) { warnColor.dispose(); } });isDisposed()这个判断别嫌啰嗦。窗口可能被重复关闭或者你在别处也调了dispose()重复释放会抛异常。加上判断就稳了。有一个特别容易犯的错误是在循环里创建 Color。比如按分组给表格行上色如果每行都new Color(...)一百行就是一百个句柄。正确做法是把颜色集中创建在一处做成一个ResourceRegistry或者干脆用常量池// 全局只创建一次窗口关闭时统一释放 private final MapString, Color colorCache new HashMap(); private Color colorOf(Display display, String key, int r, int g, int b) { return colorCache.computeIfAbsent(key, k - new Color(display, r, g, b)); } // 统一释放 shell.addDisposeListener(e - colorCache.values().forEach(c - { if (!c.isDisposed()) c.dispose(); }) );这套写法我在每个稍大的 SWT 项目里都放一份几乎没有例外。5.2 UI 线程与后台任务asyncExec 和 syncExec 怎么选前面提过控件只能在 UI 线程操作。那么问题来了读一个大文件、做一次网络请求当然练习项目里没有耗时操作放 UI 线程里界面就卡住不重绘用户以为程序死了。解决办法是开个普通线程干活干完回到 UI 线程更新界面。SWT 提供了两个入口方法行为使用场景display.asyncExec(Runnable)把任务排到 UI 线程队列立即返回后台任务完成后刷新界面最常见display.syncExec(Runnable)阻塞当前线程等 UI 线程执行完后台线程需要立即拿到界面数据display.timerExec(ms, Runnable)延迟执行可取消防抖、延时刷新具体到代码new Thread(() - { ListContact loaded store.loadAll(); // 耗时操作不碰界面 if (display.isDisposed()) return; // 用户可能已经关窗了 display.asyncExec(() - { if (table.isDisposed()) return; fillTable(loaded); statusLabel.setText(已加载 loaded.size() 条); }); }, loader).start();这段代码里两个判空非常关键。第一个display.isDisposed()防的是窗口全关了、事件循环退出、Display 被释放之后再往队列里塞任务第二个table.isDisposed()防的是任务排进去了但执行前控件被销毁。这两个判断漏掉任意一个都可能抛Widget is disposed而这个异常在用户机器上是随机出现的极难复现。syncExec要慎用。它会让调用线程一直等如果 UI 线程恰好也在等这个后台线程比如用了join()就死锁了。我的原则是默认用 asyncExec除非确有必要拿返回值。5.3 关窗时的收尾顺序与假死排查关窗顺序这件事没什么技术含量但顺序错了会有麻烦。我的做法是监听shell的ShellEvent先拦截关闭请求做收尾再决定要不要真关shell.addShellListener(new ShellAdapter() { Override public void shellClosed(ShellEvent e) { if (dirty !confirmSave()) { e.doit false; // 用户点取消阻止关闭 } } });e.doit false就是取消关闭的开关用来做有未保存修改是否确认退出的提示。这里用MessageBox弹确认框返回值判断SWT.YES还是SWT.NO。至于关窗后进程还在的问题八成是还有非守护线程活着比如你自己开的后台线程没结束或者事件循环没退出。排查方法很简单在main末尾加一句打印看看窗口消失后有没有输出。如果没有说明卡在循环里了——通常是某个Shell没有真正dispose()或者是还有别的窗口没关。6. 数据落地与文件交互让练习项目像个真工具6.1 存储格式选型别在这个阶段引入复杂度数据怎么存直接影响后面能走多远。练习项目里我选的是TSV制表符分隔纯文本理由很实在肉眼可读、出问题能直接打开看、不依赖任何第三方库、几十行代码就能读写完。JSON 更好看但要引依赖序列化最省事但版本一改就全废而且调试时看到一堆乱码出问题完全没法排查。写文件的时候有两个细节要处理。第一字段里可能出现制表符或换行必须先转义再拼private String escape(String s) { if (s null) return ; return s.replace(\\, \\\\) .replace(\t, \\t) .replace(\n, \\n); }第二写文件要先写临时文件再原子替换不然写到一半程序崩了原数据就没了。做法是写data.tsv.tmp写完Files.move(tmp, target, REPLACE_EXISTING, ATOMIC_MOVE)。这两步加起来不到十行代码但能救你一次。文件路径别写死在代码里用System.getProperty(user.home)拼一个应用目录不存在就mkdirs()。我习惯放在user.home/.practice/contacts.tsv这样的位置卸载时用户自己删目录就行不污染系统目录。6.2 FileDialog 的使用与用户取消这个必答题导入导出需要文件选择框SWT 提供FileDialog和DirectoryDialog。用法很直接FileDialog dialog new FileDialog(shell, SWT.SAVE); dialog.setText(导出联系人); dialog.setFilterNames(new String[]{制表符文本, 所有文件}); dialog.setFilterExtensions(new String[]{*.tsv, *.*}); dialog.setFileName(contacts.tsv); String path dialog.open(); if (path null) { return; // 用户点了取消这是最常见的情况 }dialog.open()返回null表示用户取消必须判空。这是新手第一个坑不判的话紧接着new File(path)就空指针了。第二个坑是覆盖确认SWT.SAVE模式下选了一个已存在的文件时对话框会自己弹是否覆盖不需要你手动做。还有一个路径合法性问题。用户可能在一个只读目录里选保存位置或者文件名里带了非法字符。我的做法是保存前先试探性try (OutputStream os new FileOutputStream(file)) {}能打开就继续打不开给个友好的MessageBox提示比直接抛异常堆栈友好一万倍。6.3 提示与反馈MessageBox 的分级使用MessageBox的图标常量基本对应日志级别用对了用户能一眼看出严重程度图标常量使用场景按钮组合SWT.ICON_INFORMATION保存成功、导入完成SWT.OKSWT.ICON_WARNING数据不完整是否继续SWT.YES | SWT.NOSWT.ICON_ERROR文件读取失败、格式错误SWT.OKSWT.ICON_QUESTION确认删除这类需要二次确认的操作SWT.YES | SWT.NO有个细节MessageBox是模态的弹出期间主窗口不响应所以绝对不能在循环里弹。比如批量导入时某几行格式错误不要每错一行弹一次窗用户得点一百次。正确做法是收集错误行号最后一次性汇总提示共 12 行解析失败第 3、7、15 行……。状态栏提示也要利用起来。我把窗口底部放一个Label任何操作都在这里留一行字已保存到 xxx共 23 条记录已按关键词过滤剩余 5 条。这类即时反馈不打断操作用户体验的差别其实很大。7. 常见问题与排查技巧实录7.1 报错速查表把这几个症状背下来SWT 的报错信息不算多来来回回就那么几个。我整理了一份对照表基本能覆盖练习阶段九成的问题现象 / 报错根本原因处理方式No more handlesColor/Font/Image 没释放全局搜new Color、new Font补 disposeInvalid thread access在非 UI 线程操作控件用display.asyncExec包一层Widget is disposed异步任务执行时控件已销毁执行前判isDisposed()UnsatisfiedLinkError: no swt-...平台包选错或本地库没加载换成对应系统的构件控件不显示 / 尺寸为 0父容器没设布局或GridData没给补setLayout和setLayoutData窗口拉伸控件不变grabExcessHorizontalSpace为 false把需要拉伸的控件设为 true中文显示成方块字体名写错或用了不支持中文的字体换成系统自带的中文字体界面发虚、字体模糊高 DPI 缩放与 SWT 版本不匹配更新 SWT 版本或用启动参数调整缩放UnsupportedClassVersionError依赖版本要求的 JDK 高于当前降依赖版本或升级 JDK关闭窗口后进程不退出有非守护线程存活检查自建线程必要时设setDaemon(true)用这张表的时候有个思路先看错误类型再看触发时机。Widget is disposed一定是异步相关Invalid thread access一定是线程相关No more handles一定是资源相关。定位到类别范围就缩小到十几行代码了。7.2 高 DPI、字体与中文显示的实测经验高 DPI 这块我踩过最久。现象是在 125% 或 150% 缩放的屏幕上界面整体放大但文字发虚或者反过来控件被裁掉一块。原因在于 SWT 的自动缩放逻辑在不同版本里行为有差异早期版本需要手动指定缩放比例。现在新版 SWT 自带自动缩放大部分情况下不用管。如果确实遇到问题可以试试在启动参数里加-Dswt.autoScale200之类具体数值按你屏幕的实际缩放比例来。但这个参数不是万灵药不同版本的实现不一样一定要在你自己的机器上实测别照抄别人的配置。字体方面中文界面我一般统一设一个系统自带字体比如微软雅黑 UI或者苹方字号 9 到 10。设置方式是在Display上做全局覆盖Font baseFont new Font(display, Microsoft YaHei UI, 10, SWT.NORMAL); shell.setFont(baseFont); shell.addDisposeListener(e - { if (!baseFont.isDisposed()) baseFont.dispose(); });注意shell.setFont只对没有单独设字体的子控件生效设过的会被覆盖。所以要么统一设、要么别设混着来容易出现这个按钮字大那个字小的情况。中文乱码的另一个来源是源码文件编码。如果代码文件存成了 GBK编译器按 UTF-8 读字符串常量里的中文就会变乱码而且编译不报错运行时才发现。检查方法很简单看源文件里的中文在编辑器里显示是否正常另外构建配置里加一句-encoding UTF-8。7.3 我个人的几条避坑心得第一条每加一个面板就setBackground换个颜色跑一次。我用SWT.COLOR_WIDGET_LIGHT_SHADOW这类浅色给每个Composite上临时底色一眼就能看出布局边界对不对、有没有被压成 0 宽。调完再删掉。这个土办法比任何布局调试工具都直观。第二条窗口最小尺寸一定要设。shell.setMinimumSize(800, 500)。不加的话用户把窗口拖到很窄GridLayout会把输入框压到看不见界面上只剩下一堆标签看着像程序坏了。第三条写代码前先在纸上画控件树。SWT 的一切都是父子关系Composite套Composite每一层的布局各管各的。纸上画清楚哪层用什么布局比在代码里反复试快得多。我现在的习惯是画一棵树每个节点旁边标注布局类型和GridData的关键参数照着敲几乎不用改。第四条数据结构先行。先把Contact类和存储层的读写跑通用手写几条假数据打印出来验证再去接界面。反过来先做界面后想数据中间会有大量临时变量到处飞后期一片混乱。第五条每次改完跑满 10 分钟。SWT 的资源泄漏和线程问题不会立刻暴露往往要操作一段时间才崩。我的验收方式是反复打开关闭窗口、反复导入导出几次、连续切换页面几十次看内存曲线是否平稳。这个过程很枯燥但比上线后被用户投诉强。最后说个小技巧。调试界面时可以给Shell加一个全局快捷键触发时把当前所有控件的边界框打印出来shell.getDisplay().addFilter(SWT.KeyDown, e - { if (e.keyCode SWT.F2) { dumpBounds(shell, 0); } }); private void dumpBounds(Control c, int depth) { System.out.println( .repeat(depth) c.getClass().getSimpleName() c.getBounds()); if (c instanceof Composite comp) { for (Control child : comp.getChildren()) dumpBounds(child, depth 1); } } .repeat(depth)需要 JDK 11 以上用不了就换StringUtils或自己写个循环。这个 dump 出来的树基本等于把布局问题摆在解剖台上哪里宽度是 0、哪里位置跑到负坐标一目了然。我在做那个联系人管理器的表单对齐时靠它省了大半天时间。
阅读完成 · 觉得有帮助?