首页 / 资讯中心 / 文章详情

SWT 阶段练习复盘:原生控件、GridLayout、TableViewer 与打包分发

SWT 阶段练习复盘:原生控件、GridLayout、TableViewer 与打包分发 ★ FEATURED ARTICLE
我之前带过几批刚接触 Java GUI 的同学做 SWT 的阶段练习最常见的画面是窗口在开发机上跑得又快又稳打包发给别人双击之后只弹出一行 UnsatisfiedLinkError或者界面能开点几下就白屏、卡死控制台刷出一串 Widget is disposed。SWT 这套东西和大多数人熟悉的 Swing、JavaFX 差别不在于“控件名字叫什么”而在于它一开始就把自己绑在了操作系统的原生控件上于是生命周期、线程、平台依赖这些问题会比别的 GUI 框架更早、更硬地砸到你脸上。这篇东西就是我给的阶段练习复盘用一个能真正跑起来的小工具把 SWT 的 Display/Shell/Widget 心智模型、swt.jar 的平台差异、GridLayout 的布局逻辑、TableViewer 的数据驱动、资源回收和打包分发这一条链走完适合刚学完 SWT 基础控件、正打算做第一个完整练习的人也适合写惯了 Swing、想换到原生控件方案的人拿来对照。1. 阶段练习别一上来就堆控件先把 SWT 的心智模型立住1.1 原生控件不是一句宣传语它决定了你后面所有的坑SWT 的每一个控件背后都是一次对操作系统原生窗口组件的调用这一点不是概念差异而是会直接影响你写代码方式的事实。Swing 是纯 Java 自绘你自己管一套轻量组件树界面长什么样由 Java 决定SWT 则通过一层本地绑定去创建真正的系统控件按钮是系统的按钮表格是系统的表格。带来的好处很直观界面观感、输入法、系统主题适配、无障碍支持这些几乎免费拿到启动速度也漂亮。但代价同样直接每个控件都会持有操作系统资源你必须显式释放控件只能由创建它的那个线程碰不同平台的实现库不一样你的 jar 包天然带平台属性。我见过太多同学在练习阶段把 SWT 当成“换了个包名的 Swing”来写结果就是三类问题反复出现。第一类是把界面元素塞进成员变量到处传窗口关了资源没释放任务管理器里进程挂着一堆句柄第二类是在按钮回调里直接开线程改表格内容一跑就抛线程访问异常第三类是在这台机器上编译完直接把工程目录压缩发给别人对方一运行就报本地库加载失败。这三类问题的根都不在具体 API 上而在心智模型上——你得先接受“SWT 的界面对象是有寿命、有归属、有平台属性的实体”后面的代码才写得对。所以阶段练习的第一目标不是“画出一个好看的界面”而是把这三个约束练成肌肉记忆谁创建、谁释放、谁在哪个线程上碰它。界面美不美观在这个阶段真不重要我甚至建议前面两三天就故意用最朴素的默认外观把所有注意力放在结构上。1.2 练习选题的判定标准小、闭环、有真实数据选题这件事比很多人想的更影响练习效果。太大比如一上来就做“带数据库的多用户管理系统”你会把时间全花在 JDBC、连接池、权限模型上SWT 本身反而练不到太小比如“点一下按钮弹个对话框”你又碰不到表格、布局、线程、资源回收这些真正有价值的部分。我一般给三条判定标准。第一界面里必须同时出现输入控件、列表型控件和状态反馈这三样凑齐你才不得不处理布局、数据绑定和异步更新。第二数据必须有落盘或导出的出口有了出口你就一定会碰到文件对话框、后台任务、异常处理。第三功能范围能在一周左右的业余时间里跑通闭环增删改查、保存加载、搜索过滤做完能自己天天用一下。符合这三条的题目其实很多本地待办清单、书签整理器、素材归档表、账单记账小工具、配置文件对照器都行。反过来说我不建议在阶段练习里碰的东西也很明确多窗口 MDI 那种复杂窗口管理、复杂的自绘图表、需要联网同步的功能、需要内嵌浏览器组件的功能。这些每一块都能单独吃掉你一周时间而且和 SWT 的核心机制关系不大。1.3 一个能练到位的题目本地资料清单工具我这次拿来做示范的题目是“本地资料清单工具”说白了一个加强版待办表。它的界面分区很典型顶部一条工具条放新增、删除、标记完成三个按钮工具条下面一行搜索框加一个“只看未完成”的勾选框中间主体是一张四列表格列分别是标题、优先级、截止日期、状态表格右侧用一条可拖动的分隔条挂一个详情面板选中某一行时在详情面板里编辑备注最底下一条状态栏显示总条数和当前筛选结果数。数据模型就是一个简单的条目类字段包括标题、优先级枚举、截止日期、完成标记、备注。持久化我用最土的办法一份用制表符分隔的文本文件放在用户目录下程序启动时读、退出时写另外提供一个“导出为文本”的按钮走文件对话框。为什么不用数据库因为练习阶段的重点不是数据层而且纯文本文件方便你直接打开看出问题时肉眼就能定位比翻 SQLite 文件高效得多。这个题目的好处是每一块都能对上 SWT 的一个知识点工具条对 ToolBar 和图片资源搜索框对 ModifyListener 和 ViewerFilter表格对 TableViewer 和 JFace 的提供器体系详情面板对多行文本框和选择事件状态栏对标签刷新和异步更新导出按钮对 FileDialog 和后台线程关窗时对资源释放。一个题目串起一条链比做五个互不相干的小 Demo 有效得多。2. swt.jar 是平台相关的依赖与运行环境这一步最容易翻车2.1 为什么不能像别的库那样只放一个 jar绝大多数 Java 库是纯字节码一个 jar 走遍天下SWT 不是。它的主体确实也是 Java 类但底下那层本地实现是按平台编译的Windows、Linux、macOS 各有一套甚至 macOS 还分 x86 和 ARM 两种架构。所以你在依赖里引入的其实不是“SWT”而是“某个平台上的 SWT”。这个事实在开发阶段容易被掩盖你在自己机器上加了依赖跑起来了就以为成了直到换机器或者打包才暴露。我建议在练习一开始就把这件事摆到台面上做法就是在依赖声明里把平台写清楚而不是用一个模糊的通用坐标。以 Maven 为例Windows 64 位下的写法大致是这样dependency groupIdorg.eclipse.platform/groupId artifactIdorg.eclipse.swt.win32.win32.x86_64/artifactId version3.124.0/version /dependency同样的版本号下Linux 的 GTK 版本、macOS 的 Cocoa 版本是另外的 artifact 名。版本号这块不用死记去中央仓库搜一下当前主线的 3.12x 系列挑一个和你 JDK 匹配的就行重要的是记住它是平台绑定不是“随便哪个都行”。2.2 用 profile 把多平台依赖管起来如果你只在 Windows 上练习写死一个依赖完全够用。但只要你有第二台机器比如一台 Linux 笔记本或者同事的 Mac就值得花十分钟把 Maven profile 配好。我的做法是按操作系统自动激活profiles profile idswt-windows/id activationosfamilywindows/family/os/activation dependencies dependency groupIdorg.eclipse.platform/groupId artifactIdorg.eclipse.swt.win32.win32.x86_64/artifactId version3.124.0/version /dependency /dependencies /profile profile idswt-linux/id activationosfamilyunix/familynameLinux/name/os/activation dependencies dependency groupIdorg.eclipse.platform/groupId artifactIdorg.eclipse.swt.gtk.linux.x86_64/artifactId version3.124.0/version /dependency /dependencies /profile /profiles这套配置的实际价值不在于“看起来专业”而在于它会逼你在换环境时立刻发现问题而不是等到交付前夜。我第一次做这个练习的时候就是因为懒本地跑通就直接把代码推到共享仓库另一位同学拉下去编译能过、运行就崩最后排查半小时才发现是依赖里躺着 Windows 的实现包。注意swt.jar与jface的版本要成对。混用不同版本的 SWT 和 JFace最常见的表现是编译期正常、运行期在 Viewers 相关类上抛 NoSuchMethodError这种错误信息指向的位置往往离真正原因很远。2.3 启动参数和 JDK 版本要提前对齐macOS 上有个历史遗留问题SWT 要求在启动时把 UI 相关的工作放在主线程上如果直接用java -jar或者某些构建工具启动需要在启动参数里加上-XstartOnFirstThread。这不是什么可选优化缺了它就是启动即崩或者窗口出不来。练习阶段如果班上有人用 Mac这一条一定要提前说能省掉大量“为什么只有我不行”的困惑。JDK 版本也要看一眼。较新的 SWT 版本普遍要求 Java 11 以上有些主线版本要求 17。我在练习群里见过因为 IDE 里默认用了一个更老的 JDK编译能过但运行时在模块访问上出问题的情况。最简单的核对办法是先写一个只有主窗口的空壳跑通再说业务代码把环境问题和代码问题彻底分开。3. 三段式骨架Display、事件循环、资源回收3.1 main 方法的标准骨架一次写对省很多事SWT 程序的主入口有一套几乎固定的三段式结构创建 Display、装配并打开 Shell、跑事件循环直到窗口销毁。我要求所有同学的第一个练习文件就按这个骨架写别省步骤public class TodoApp { public static void main(String[] args) { Display display new Display(); try { Shell shell new Shell(display); shell.setText(本地资料清单); new MainPanel(shell); // 界面装配交给独立类 shell.setSize(960, 620); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } } finally { display.dispose(); } } }这段代码里有两个容易被省略掉的细节。一个是try/finallydisplay.dispose()必须无条件执行否则内部的系统资源会挂着反复启动调试的时候尤其明显。另一个是把界面装配放到独立类里主入口只留骨架。我强烈建议这么做因为练习做下去你会不断往界面上加东西全塞在 main 方法里三天之后你自己都不想看那个文件。readAndDispatch和sleep这两句的作用值得说清楚。readAndDispatch会从系统事件队列里取一个事件并派发出去返回 true 表示还处理了事件返回 false 表示当前没事件了于是调用sleep让线程挂起等待直到有新事件进来。如果把它写成while (!display.readAndDispatch()) {}这种忙等形式CPU 会直接飙到一个核满载风扇立刻响这是新手练习里非常常见的一个“性能问题”实际上只是事件循环写错了。3.2 UI 线程只有一条跨线程改界面必须绕回来SWT 对线程的限制是硬性的控件只能在创建它的线程上被访问和修改那条线程就是 UI 线程也就是跑事件循环的线程。任何回调——按钮点击、表格选中、文本修改——本身都已经在 UI 线程上了所以你在回调里改控件是安全的。真正的问题出在你开了后台线程之后。练习项目里的第一个真实需求就会出现导出文件、加载大文本、批量重命名这些操作要花时间如果放在 UI 线程上界面会立刻假死用户点哪都没反应。正确做法是把耗时逻辑放后台线程算完之后用display.asyncExec把界面更新投递回 UI 线程button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e - { statusLabel.setText(正在导出...); new Thread(() - { String message; try { message Exporter.export(todos); } catch (Exception ex) { message 导出失败 ex.getMessage(); } String finalMessage message; if (!display.isDisposed()) { display.asyncExec(() - statusLabel.setText(finalMessage)); } }, export-worker).start(); }));这段代码有三个点值得注意。后台线程里绝对不能碰任何控件哪怕只是读一个文本框的值也应该在启动线程之前先读出来传进去。捕获异常要在后台线程里做完别让异常一路抛上去后台线程的异常不会自动显示到界面上只会悄悄打印到控制台用户看到的就是“点了没反应”。最后那个isDisposed判断不是多余的如果用户在导出过程中直接把窗口关了asyncExec投递的任务会在一个已经销毁的 Display 上执行直接抛异常。还有一个更隐蔽的坑我给每个线程都起了名字上面那个export-worker。这个习惯是被逼出来的线上排查线程问题时线程转储里全是Thread-7、Thread-8这种名字你根本分不清哪个是自己开的。练习阶段就养成习惯成本为零。3.3 资源回收挂在关闭事件上别散落各处SWT 里 Color、Font、Image、Cursor 这些都属于必须手动释放的资源。它们不是普通对象底下握着系统句柄不释放就会泄漏。麻烦在于它们的寿命通常比创建它们的方法长你在装配界面时创建了一个颜色给标签用这个颜色对象必须活到标签销毁为止所以不能像普通对象那样用完就扔。我推荐的做法是在装配类里维护一个资源池创建资源时统一登记窗口关闭时一次性释放public class ResourcePool { private final ListResource items new ArrayList(); public Color color(Device device, int r, int g, int b) { Color c new Color(device, r, g, b); items.add(c); return c; } public Font font(Device device, FontData data) { Font f new Font(device, data); items.add(f); return f; } public void disposeAll() { for (Resource r : items) { if (r ! null !r.isDisposed()) { r.dispose(); } } items.clear(); } }然后在装配入口挂一句ResourcePool pool new ResourcePool(); shell.addDisposeListener(e - pool.disposeAll());这样做的好处是回收逻辑只有一个出口你能一眼看出程序里创建了哪些长生命周期的资源。我自己的经验是练习项目做到第三四天界面上的颜色字体图标会越来越多如果每个地方都单独new Color而不管释放一次调试运行能泄漏几十个句柄跑久了界面开始出奇怪的绘制问题你还找不到原因。提示SWT 生态里有个专门追踪资源创建与释放的小工具原理是包一层设备类来记录每次 new 和 dispose。练习阶段可以用它做一次体检看看到底有多少资源没被释放比自己肉眼翻代码有效得多。4. 布局才是 SWT 的分水岭GridLayout 与 GridData 的配合4.1 GridLayout 的列数怎么定SWT 的布局管理器和 Swing 的思路接近但用起来的感受差别很大Swing 的布局管理器通常能“自动”给你一个还算能看的排布SWT 的布局你要是没给每个控件配上布局数据它们会全部挤在左上角看起来像界面写坏了。这不是 bug是 GridLayout 的设计它按列排布每个控件占一个格子控件具体怎么填、占几格、能不能拉伸全部由 GridData 决定。列数怎么定我的经验是先画分区再数每行最多几个控件。上面那个清单工具顶部工具条是一行三个按钮可以单独用一个布局为GridLayout(3, true)的 Composite中间搜索区是一行“搜索框 勾选框”用GridLayout(2, false)主体是表格加详情面板用GridLayout(2, false)但要让两列按比例分空间。分开用多个 Composite 各自布局比在一个大布局里用跨列硬拼要清晰得多也更好改。只有一件事要小心GridLayout 的makeColumnsEqualWidth参数。设成 true 会让所有列等宽这在工具条那种按钮等宽的场景很合适但在“搜索框 小勾选框”这种场景就会很难看勾选框会被拉得很宽。这个参数新手特别容易顺手写 true然后抱怨控件显示得怪。4.2 GridData 每个字段到底在干什么GridData 的字段不少但练习里真正高频的就那么几个我把它们整理成一张表方便你直接对着抄字段作用练习中的典型取值widthHint/heightHint给控件一个初始宽高建议值搜索框 220按钮 90horizontalSpan/verticalSpan横向或纵向跨几个格子状态栏跨 2 列horizontalAlignment/verticalAlignment控件在自己的格子内侧怎么对齐SWT.FILL、SWT.CENTERgrabExcessHorizontalSpace窗口变宽时是否吃掉多余空间表格 true按钮 falsehorizontalIndent左缩进像素用于视觉层级详情区内的小标签缩进 8minimumWidth/minimumHeight拉伸时的下限详情面板 260这里最容易搞混的是widthHint和grabExcessHorizontalSpace的关系。前者只是建议初始宽度后者决定窗口拉伸时谁变宽。我练习时的做法是凡是希望跟着窗口一起变宽的控件grabExcessHorizontalSpace一定设 true并且horizontalAlignment设SWT.FILL固定用途的按钮一律 false。搜索结果表格和详情面板都要跟着变宽所以两个都设 true但给详情面板加一个minimumWidth避免窗口缩小时它被压成一条线。还有一个非常实用的细节GridData不必每个字段都显式写。常用的组合可以封装成一个工具方法比如fillHorizontal(boolean grab)、fixed(int width)练习代码里会干净很多。写到后面你会发现界面结构的复杂度大半耗在布局数据上能省一点是一点。4.3 嵌套 Composite 与可拖动分隔条主体区“表格 详情面板”这种左右结构很多人第一反应是用SashForm。它确实好用直接给你一条可以拖的分隔条用户能自己调整两边的宽度。但要注意它内部用的也是权重机制setWeights传的是相对比例两边的控件仍然要各自的布局数据配合否则拖起来会出现一边铺满一边挤扁的情况。我建议的装配顺序是这样的先在 Shell 上建一个纵向的 GridLayout放工具条 Composite、搜索 Composite、主体 SashForm、状态栏标签每个 Composite 自己再定布局。一层一层往下拆每层只关心自己这一行怎么排。这种写法在后期加功能时特别友好——想加一行筛选条件只在搜索区那个 Composite 里加控件、把列数改一下就行不会牵动整个界面。4.4 窗口缩放自测清单布局写完一定要做的事是拖窗口边缘从最小拖到最大再拖回来。我在练习评审时就看这一个动作能看出八成问题。常见的失败表现有控件重叠、按钮被压成一条缝、表格下半部分被裁掉、状态栏跑到屏幕外、详情面板宽到把表格挤没。这些现象背后的原因基本都能在布局数据里找到某个控件忘了设 grab 导致窗口变大时其他控件把空间吃光了容器没设布局导致子控件挤在左上表格所在的父容器没有垂直方向的 grab导致表格拿不到剩余高度。我给自己的检查清单是四句话窗口最大化时是否所有元素都在窗口最小化尺寸时是否还有能用的最小宽度中间拖动过程中有没有出现闪烁或重叠表格这类滚动型控件的高度是否会跟着窗口变。5. 用 TableViewer 把界面变成数据驱动5.1 裸 Table 能跑但会把你拖进手工同步的泥潭SWT 的Table控件本身完全够用你可以直接new TableItem、setText做增删改。练习前几天我也建议先这么写一版因为你需要亲手感受一次“数据改了但界面没跟上”的痛苦才知道 Viewer 到底解决了什么。痛苦具体是这样的你有一个 List 存数据界面上有一个 Table 显示数据每次新增一条你要同时改 List 和 Table每次删除你要算出被删的那行在 Table 里的索引再删每次修改字段你要找到对应的 TableItem 再 setText。三处逻辑分散任何一处漏掉就是数据和界面不一致而且这种 bug 非常难复现因为界面上看起来“就是少了一条”你不知道是数据没了还是显示没了。TableViewer 的价值就在这你只管把数据源交给它界面怎么显示由提供器决定你改完数据调一次刷新界面自动对齐。练习项目里我要求表格一律用 Viewer不是为了显得高级而是为了让练习重点落在“数据与视图的关系”上而不是“怎么手工同步两套状态”。5.2 三件套内容提供器、标签提供器、输入一个最小的表格需要三样东西凑齐才能出效果缺一样就是空表。内容提供器负责回答“这一层有哪些元素”标签提供器负责回答“某个元素在第几列显示什么文字”输入负责把数据源交给 Viewer。TableViewer viewer new TableViewer(parent, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); Table table viewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); String[] titles { 标题, 优先级, 截止日期, 状态 }; int[] widths { 260, 80, 120, 80 }; for (int i 0; i titles.length; i) { TableViewerColumn col new TableViewerColumn(viewer, SWT.NONE); col.getColumn().setText(titles[i]); col.getColumn().setWidth(widths[i]); col.getColumn().setAlignment(i 1 ? SWT.CENTER : SWT.LEFT); } viewer.setContentProvider(ArrayContentProvider.getInstance()); viewer.setLabelProvider(new TableLabelProvider()); viewer.setInput(items.toArray());TableLabelProvider是我自己实现的一个类实现ITableLabelProvider接口在getColumnText里按列索引返回对应字段。这种写法比每一列单独挂一个ColumnLabelProvider稍微集中一点练习阶段我更推荐后者因为一列一个匿名类更直观不容易把列索引对应错TableViewerColumn titleCol new TableViewerColumn(viewer, SWT.NONE); titleCol.getColumn().setText(标题); titleCol.setLabelProvider(new ColumnLabelProvider() { Override public String getText(Object element) { return ((Todo) element).getTitle(); } });新手最容易踩的一个坑是setInput传的必须和内容提供器匹配。用ArrayContentProvider就要传数组传个 List 进去界面一片空白也不报错你盯着代码看半天找不到问题。这个错误我自己也犯过后来养成的习惯是——只要表格没内容先看输入类型。5.3 刷新与过滤两个高频误区第一个误区是 setInput 居然不刷新。Viewer 在设置输入时会比较新旧输入是否相等相等就直接返回什么都不做。所以如果你的数据在同一个 List 上原地修改然后把同一个引用再 setInput 一次界面不会有任何变化。解决办法有两个一是每次都传新数组list.toArray()每次都是新对象所以一定触发刷新二是数据改完直接调viewer.refresh()或refresh(element)。我在练习里统一用后者语义更明确。第二个误区是过滤条件叠加。viewer.addFilter(...)是往列表里加不是替换。你在搜索框的修改事件里每次都 add 一个匿名过滤器输入十个字就加了十次所有过滤条件同时生效结果就是越搜越空。正确写法是把过滤器保存成一个字段每次搜索时先判断是否已经加过或者干脆先remove再addprivate ViewerFilter keywordFilter; private void applyFilter(String keyword) { if (keywordFilter ! null) { viewer.removeFilter(keywordFilter); } final String kw keyword.trim().toLowerCase(); keywordFilter new ViewerFilter() { Override public boolean select(Viewer v, Object parent, Object element) { if (kw.isEmpty()) return true; Todo t (Todo) element; return t.getTitle().toLowerCase().contains(kw) || t.getNote().toLowerCase().contains(kw); } }; viewer.addFilter(keywordFilter); viewer.refresh(); }顺带一提数据量上到几千行之后建议打开viewer.setUseHashlookup(true)它让 Viewer 内部用哈希表定位元素刷新单条和选择定位会快很多。练习数据小的时候感觉不到等你导入一个几千行的文件就明显了。5.4 列宽、排序与选中项取值列宽这件事有个小经验把设置列宽的代码放在setInput之后执行一次。有些情况下表格在填充数据后会把列宽重新按内容估算导致你在前面设的宽度被覆盖表现是“我明明设了 260跑起来却是 100 多”。我自己遇到过两次之后一律在数据加载完再设一遍。排序用不上自己写比较器循环Viewer 提供了setComparator配一个按列切换的监听就能实现点击表头排序。练习阶段做个“按截止日期升序”就够了重点是理解排序是作用在 Viewer 展示层上的你的原始 List 顺序不变这对后面“导出时按什么顺序写出”是有影响的别搞混。选中项取值也要注意IStructuredSelection sel viewer.getStructuredSelection(); Todo current (Todo) sel.getFirstElement(); if (current null) return; // 空表或未选中必须判空表格为空或者刚刷新完还没选中任何行时getFirstElement()返回 null直接强转就会空指针。这个判断在详情面板的刷新逻辑里尤其重要我练习时的做法是详情面板整体做一个“无选中则禁用”的处理比到处判空干净。6. 练习中最容易翻车的五类报错与排查链路6.1 Invalid thread access错误信息很明白原因往往很绕这个异常的提示是Invalid thread access字面意思就是线程不对但新手往往觉得自己明明没开线程。我的排查链路是这样的先看异常栈里报错的那一行确认是哪个控件被碰了然后往上找看这个调用链是不是从某个回调开始的——回调本身一定在 UI 线程上所以一定是途中经过了什么异步机制最后去找代码里所有的new Thread、定时器、后台任务提交点。有几类隐蔽来源。一类是定时刷新用后台线程每隔几秒读一次文件再更新标签忘了绕回 UI 线程。一类是第三方库的回调某些 IO 或网络库的回调线程不是 UI 线程在回调里直接改界面就炸。还有一类最阴——在构造函数里就启动了一个线程去初始化数据界面还没建完线程就开始改控件了。我自己的规矩是所有后台任务只做纯计算和 IO产出结果对象绝不接收控件引用。这条守住了这类问题基本绝迹。6.2 Widget is disposed窗口关了活儿还在跑窗口已经关掉但某个后台任务或者延迟执行的任务还在跑跑完去更新控件就会抛Widget is disposed。它和上面那个异常的区别是一个是线程不对一个是对象已经不存在了。排查时先看报错的是哪个控件再看它属于哪个 Shell然后去找有没有在窗口关闭后仍然会执行的任务。常见的来源有三个后台线程跑完asyncExec回来时窗口已关用display.timerExec设置的延时任务没有在窗口关闭时取消事件监听器里做了耗时操作用户在操作过程中把窗口关了。处理办法很直接所有投递回 UI 线程的任务执行体第一行判display.isDisposed()和控件isDisposed()用timerExec的地方把返回的任务 id 存起来在 Shell 的销毁监听里调display.timerExec(id, null)取消。练习阶段加上这几行能省掉大量莫名其妙的崩溃。6.3 资源泄漏不报错但是会越来越慢这是最不容易被察觉的一类问题因为它不抛异常。表现是程序开一会之后开始变慢界面切换时有残影反复打开关闭详情窗口后内存涨上去不回落。根因就是 Color、Font、Image 这类资源没释放。我的排查办法分两层。第一层是代码审查把所有new Color、new Font、new Image搜出来看每一个是否登记进了资源池或者在dispose监听里释放。第二层是运行时观察用系统自带的资源监视工具看进程的句柄数反复执行“打开详情窗口再关闭”这个动作二十次如果句柄数一直往上走那就是有泄漏。常见的泄漏点很集中在绘制回调里new Color每帧新建一个颜色对象这是最经典的泄漏写法在表格的标签提供器里每次getText都创建一个 Font图片资源只创建不释放尤其是工具条按钮上的图标。// 错误写法每次绘制都新建 public void paintControl(PaintEvent e) { e.gc.setForeground(new Color(display, 200, 40, 40)); } // 正确写法资源池里取生命周期跟随窗口 Color warnColor pool.color(display, 200, 40, 40); public void paintControl(PaintEvent e) { e.gc.setForeground(warnColor); }6.4 中文显示与高 DPI 缩放在别人机器上才暴露中文的问题一般出在字体上。SWT 默认会用系统字体多数情况下中文没问题但如果你手动给某个控件设了字体而那个字体在目标机器上没有中文字形就会显示成方块。我练习时的做法是不手动设字体族只调整字号和加粗字号通过FontData基于系统默认字体改这样跨机器基本不会出问题。高 DPI 是另一回事。较新的 SWT 版本在 Windows 上已经能自动处理缩放但你自己塞进去的图片不会自动变清晰。表现是在高分屏上界面文字很清楚唯独工具条图标糊成一团。解决办法是给图片提供多倍图或者用ImageDataProvider按缩放比例动态生成。练习阶段最简单的做法是用矢量风格的图标或者干脆用文字按钮避开这个问题等基础功能跑通再来处理。还有个小现象值得一提同一份代码在不同系统上窗口的实际大小不一样。这是原生控件的度量方式差异不是你算错了。我练手时会给主窗口设一个初始尺寸然后用shell.setMinimumSize卡一个下限避免在小屏上开出来就挤成一团。6.5 打包之后就跑不起来本地库的问题开发阶段一切正常打包之后就崩多半是本地库没跟着走。SWT 的平台实现包里带着本地库文件普通的“把所有依赖打进一个 jar”的做法运行时未必能正确解出本地库尤其是在某些构建工具的默认配置下。现象可能的根因定位手段处理方向启动即抛本地库加载失败打包时本地库未解出或架构不匹配检查产物里是否含目标平台库文件换用平台专用启动器或安装包工具能启动某个控件创建时报错SWT 版本与 JFace 版本不匹配打印两者版本核对依赖树版本对齐到同一主线Mac 上窗口出不来缺少主线程启动参数加参数后重试启动脚本里补-XstartOnFirstThread别人机器上字体错乱手动设了目标机器没有的字体换回系统默认字体族只调字号不调字体名我自己的习惯是练习项目在第三天就先打一次包自己拷到另一台机器上跑一遍。早暴露比晚暴露好等到功能都做完了再处理打包问题你会分不清是功能 bug 还是环境 bug。7. 从练习到能给别人用几个提升完成度的细节7.1 窗口图标、标题与单实例这些东西不影响逻辑但极大影响“这东西像不像个正经程序”。窗口图标一行代码的事注意图标文件要跟着资源目录走不要写绝对路径打包之后绝对路径必然失效。标题里带个版本号调试的时候一眼能看出对方跑的是哪一版这个习惯在给别人发测试版时特别有用。单实例这块练习阶段可以用一个简单办法启动时在用户目录下尝试创建一个锁文件创建失败就说明已经有一个在跑直接退出并给个提示。更严谨的做法是让已经运行的实例把窗口提到前台但那需要进程间通信练习阶段不必做到那一步。我自己第一次做这个功能时踩的坑是程序异常退出后锁文件没删下次启动直接起不来。所以锁文件要么在启动时判断持有进程是否还活着要么加一个“强制启动”的启动参数。7.2 配置持久化别用硬编码路径窗口大小、上次打开的目录、列宽、筛选状态这些“用户偏好”都值得存下来下次打开还是原样用起来的感觉完全不同。存哪里不要存在程序目录那个目录可能没有写权限。用系统提供的用户目录接口拿到当前用户的主目录在里面建一个以程序名命名的隐藏目录配置文件放里面。存什么格式练习阶段没必要上复杂的配置框架一个简单的键值文件就够。要注意的是读写都要包异常处理配置文件损坏或者手改坏了不能让程序起不来最差也要能回退到默认值继续运行。我练习时写过一个小工具类读配置时任何异常都返回默认值写配置时异常只打印日志不中断这个“宽容”策略在实际使用中救过我好几次。窗口位置恢复也有个细节多显示器环境下上次关闭时窗口在第二块屏幕上这次只插了一块屏幕恢复位置后窗口就在可见区域之外了。稳妥的做法是恢复前先判断保存的坐标是否落在当前任何一块屏幕的范围内不在就用默认位置居中。7.3 让练习项目真正被用起来这一步听起来虚但它是阶段练习里最有价值的一环。我给自己定的规矩是做完的练习工具必须连续用一周每天用把用起来别扭的地方记下来。你会发现真正的问题往往和你写代码时以为的问题完全不一样——比如我原以为表格列宽是最重要的实际用起来最烦的是“新增一条之后光标没落在标题输入框里”我以为导出功能很关键实际一周只用了一次反倒是“按截止日期排序”天天用。这一周的记录比再多写三个 Demo 都值钱。因为界面的可用性问题只有真的用才会暴露而这类问题恰恰是 SWT 这种原生控件框架最能发挥优势的地方——系统的输入法、右键菜单、快捷键习惯都是现成的你只要把交互流程理顺用户上手几乎零成本。最后分享一个我自己一直在用的小技巧练习项目的代码里凡是“看起来能省一行”的地方我都留着不省比如显式写try/finally、显式判空、显式给线程起名字。这些写法在业务代码里可能显得啰嗦但在 SWT 这种和系统资源打交道的场景里它们就是你的安全网。等这套骨架写到不用想就能写对的时候你再去做真正的桌面工具会发现难点已经只剩下业务本身了。
阅读完成 · 觉得有帮助?
咨询建站