简介:本资源是一套基于SWT开发的Java图书管理系统完整源码工程,面向Java GUI初学者与桌面应用实践者,解决传统Swing界面原生感弱、性能受限等问题,提供可直接运行的跨平台图形化图书管理解决方案。压缩包共259个文件,含45个核心Java源文件(如MainFrame、BookDAO、ReaderDAO等)、107个编译后class文件、80个ico图标资源、12张jpg界面示意图、7个依赖jar包及1个sql数据库脚本,完整覆盖UI构建、数据库交互、借阅逻辑与资源管理全流程,包体大小为10.07MB。已有483人学习下载,适合通过真实项目掌握SWT组件布局(GridLayout/Table/Shell)、事件驱动编程、JDBC数据库操作及JFace基础集成。代码结构清晰,模块分离明确,包含图书增删改查、读者管理、借还记录等六大功能组,附带可执行exe与Eclipse工程配置文件,开箱即用,便于调试、二次开发与课程设计交付。
1. 图书管理系统用SWT做的JAVA图形化界面:轻量级桌面应用的务实选择,适合课程设计与中小场景快速落地
你可能刚在某高校实验课上被要求“用Java写个图书管理界面”,手头只有JDK和Eclipse,老师没提JavaFX、不许用Swing——这时候翻出一份基于SWT的完整实现,不是玄学,是经验之选。SWT(Standard Widget Toolkit)不像Swing那样纯Java模拟控件,也不像JavaFX那样依赖独立运行时;它直接调用操作系统原生GUI组件(Windows用Win32 API,Linux用GTK,macOS用Cocoa),界面响应快、外观原生、内存占用低。这份资源就是一套可编译、可调试、可扩展的完整工程:含图书增删改查、借阅记录管理、用户权限分级(管理员/普通用户)、SQLite本地数据库持久化,所有UI逻辑用SWT+JFace封装,无外部UI框架依赖。它不追求大屏可视化或微服务架构,但能让你在两天内跑通一个真实可用的桌面端图书系统——课程答辩能演示、毕设可作基线、小图书馆临时管理也能凑合用。如果你正卡在“Java GUI到底该选哪个”这个老问题上,又不想被JavaFX的模块化报错或Swing的仿古感劝退,这份SWT实战工程就是那个被低估的务实解。
2. SWT环境搭建与项目结构解析:为什么不用Maven而坚持手动配置classpath
SWT的特殊性决定了它不能像普通Java库一样“加个pom依赖就完事”。它的核心jar(org.eclipse.swt.*.jar)必须与目标操作系统平台严格匹配:Windows需swt-win32-4.33.jar,Linux需swt-gtk-4.33.jar,macOS需swt-cocoa-4.33.jar。更关键的是,SWT jar内部嵌入了对应平台的本地库(.dll/.so/.dylib),JVM启动时必须通过-Djava.library.path指向这些二进制文件所在目录,否则会抛出UnsatisfiedLinkError: no swt-win32-4650 in java.library.path。本项目采用“平台隔离+手动classpath”策略,既规避Maven跨平台打包的复杂性,也避免IDE自动引入错误版本导致的黑匣子崩溃。
2.1 下载与校验SWT原生包:认准Eclipse官方发布页,拒绝第三方镜像
访问Eclipse官网的 SWT Downloads页面 (注意:仅认准download.eclipse.org域名),找到SWT Binary and Source条目,下载对应你开发机操作系统的zip包。例如Windows开发者应下载swt-4.33-win32-win32-x86_64.zip。解压后你会看到两个关键文件:
swt.jar:Java字节码层,提供Display、Shell、Button等APIswt-win32-4650.dll:Windows平台本地库(版本号4650对应4.33版)
提示:不要从Maven Central下载
org.eclipse.swt:org.eclipse.swt.win32.win32.x86_64,其jar内不含dll,运行时仍会报错;也不要使用网盘分享的“整合包”,dll签名易被杀软误报。
2.2 Eclipse中配置SWT项目:三步完成Native Library绑定
在Eclipse中新建Java Project(命名为BookManagerSWT),不要勾选“Use default location”,将项目路径设为本资源解压后的src同级目录(如D:\bookmgr\),确保后续能直接引用lib/下的jar和dll。
添加SWT jar到Build Path
右键项目 →Properties→Java Build Path→Libraries→Add External JARs...→ 选择你下载解压出的swt.jar。绑定Native Library路径
在同一Libraries页,展开刚添加的swt.jar→ 双击Native library location→External Folder...→ 指向swt-win32-4650.dll所在目录(即zip解压根目录)。这一步本质是告诉JVM:“dll就在这儿,启动时请加载”。设置JRE System Library为Java 11+
SWT 4.33最低要求Java 11。若你的Eclipse默认JRE是Java 8,需先在Preferences → Java → Installed JREs中添加Java 11 JDK,再在项目Build Path中切换。
完成上述操作后,项目Referenced Libraries下应显示:
swt.jar (Native library location: D:\swt\) JRE System Library [JavaSE-11]此时编写最简SWT程序即可验证:
import org.eclipse.swt.widgets.*; public class SWTTest { public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setText("SWT Test OK"); shell.setSize(300, 200); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); } }若窗口正常弹出,说明环境已通——这是后续所有功能的基石,跳过此步直接跑图书系统必翻车。
2.3 项目源码结构深度拆解:ui/、model/、dao/三层如何协同
本资源采用清晰的分层架构,所有代码位于src/目录下,结构如下:
| 目录 | 核心类 | 职责说明 |
|---|---|---|
ui/ | MainApp.java,BookListComposite.java,LoginDialog.java | SWT UI组件封装:MainApp是主入口,创建Display和主Shell;BookListComposite继承Composite,封装图书列表表格(Table)与操作按钮(Button);LoginDialog是模态对话框,处理用户登录验证 |
model/ | Book.java,User.java,BorrowRecord.java | 数据模型POJO:全部实现Serializable,字段与SQLite表结构一一对应(如Book.id,Book.isbn,Book.status),无业务逻辑,纯数据载体 |
dao/ | BookDAO.java,UserDAO.java,DBHelper.java | 数据访问层:DBHelper继承SQLiteOpenHelper,负责建库建表(CREATE TABLE books(...));BookDAO提供insert(),queryAll(),update()等方法,内部用SQLiteDatabase执行CRUD,返回List<Book> |
关键协同逻辑在于事件驱动:当用户点击BookListComposite中的“新增”按钮,触发SelectionAdapter.widgetSelected()回调,该回调创建BookEditDialog(另一个Shell),用户填写表单后点“保存”,对话框内部调用BookDAO.insert(new Book(...)),成功后发送SWT.Dispose事件关闭自身,并通知父容器刷新Table内容。这种“UI触发→DAO执行→UI刷新”的闭环,正是SWT事件模型的典型用法,也是它比Swing更贴近原生OS消息循环的原因。
3. 核心功能实现详解:从登录鉴权到借阅流水的SWT控件组合术
SWT的控件组合不是简单拖拽,而是对原生Widget生命周期的精确控制。本节以登录模块和图书列表模块为例,拆解如何用Text,Combo,Table,Tree等基础控件构建高可用界面,同时规避常见布局陷阱。
3.1 登录对话框:用GridLayout与GridData实现像素级对齐
LoginDialog继承Dialog(非Shell),确保其为模态窗口。核心布局采用GridLayout,但关键细节在于GridData的权重分配:
public class LoginDialog extends Dialog { private String username; private String password; public LoginDialog(Shell parent) { super(parent, SWT.APPLICATION_MODAL | SWT.DIALOG_TRIM); } @Override protected Control createDialogArea(Composite parent) { Composite container = (Composite) super.createDialogArea(parent); // 设置GridLayout:2列,间距10px,边距5px GridLayout layout = new GridLayout(2, false); layout.marginWidth = 5; layout.marginHeight = 5; layout.horizontalSpacing = 10; layout.verticalSpacing = 10; container.setLayout(layout); // 用户名标签与输入框 Label lblUser = new Label(container, SWT.NONE); lblUser.setText("用户名:"); GridData gd_lblUser = new GridData(SWT.RIGHT, SWT.CENTER, false, false); gd_lblUser.widthHint = 60; // 固定标签宽度,避免文字变长时错位 lblUser.setLayoutData(gd_lblUser); Text txtUser = new Text(container, SWT.BORDER); GridData gd_txtUser = new GridData(SWT.FILL, SWT.CENTER, true, false); gd_txtUser.widthHint = 150; // 输入框最小宽度 txtUser.setLayoutData(gd_txtUser); // 密码标签与输入框(同理) Label lblPass = new Label(container, SWT.NONE); lblPass.setText("密 码:"); lblPass.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false)); Text txtPass = new Text(container, SWT.BORDER | SWT.PASSWORD); txtPass.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); // 登录按钮(跨两列居中) Button btnLogin = new Button(container, SWT.PUSH); btnLogin.setText("登 录"); GridData gd_btn = new GridData(SWT.CENTER, SWT.CENTER, false, false); gd_btn.horizontalSpan = 2; // 跨越2列 gd_btn.widthHint = 80; btnLogin.setLayoutData(gd_btn); // 绑定回车键触发登录 txtPass.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.keyCode == SWT.CR || e.keyCode == SWT.KEYPAD_CR) { handleLogin(); } } }); return container; } private void handleLogin() { // 获取输入值(注意:SWT中Text.getText()返回String,无需trim()除非业务要求) username = ((Text) getControl(1)).getText().trim(); password = ((Text) getControl(3)).getText(); // 索引按创建顺序:0=lblUser,1=txtUser,2=lblPass,3=txtPass // 调用UserDAO验证(此处省略DAO细节,见第2章) User user = new UserDAO().authenticate(username, password); if (user != null) { // 验证成功:关闭对话框,返回OK setReturnCode(Window.OK); close(); } else { MessageBox msg = new MessageBox(getParent(), SWT.ICON_ERROR | SWT.OK); msg.setText("登录失败"); msg.setMessage("用户名或密码错误,请重试"); msg.open(); } } }参数说明与设计意图:
GridLayout(2, false):2列布局,false表示不使各列等宽(避免标签被拉长),由GridData.widthHint精确控制;gd_lblUser.widthHint = 60:强制标签宽度60像素,防止“用户名”和“密 码”因中文全角空格导致对齐偏移;gd_btn.horizontalSpan = 2:按钮横跨两列,视觉居中,比用SWT.CENTER更可靠;txtPass.addKeyListener:SWT中Text不支持setActionCommand(),必须手动监听回车键,这是血泪经验——新手常漏此步,导致用户必须点鼠标才能登录。
3.2 图书列表表格:Table与TableColumn的动态渲染技巧
BookListComposite使用Table展示图书数据,但SWT的Table默认不支持自动列宽、排序、编辑,需手动增强:
public class BookListComposite extends Composite { private Table table; private List<Book> bookList; public BookListComposite(Composite parent, int style) { super(parent, style); createContents(); } private void createContents() { setLayout(new FillLayout()); table = new Table(this, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); table.setHeaderVisible(true); table.setLinesVisible(true); // 定义列(顺序即显示顺序) String[] headers = {"ID", "ISBN", "书名", "作者", "出版社", "状态", "操作"}; for (int i = 0; i < headers.length; i++) { TableColumn column = new TableColumn(table, SWT.CENTER); column.setText(headers[i]); column.setWidth(i == 0 ? 50 : i == 5 ? 80 : 120); // ID列窄,状态列稍宽,其余统一 } // 加载数据(从DAO获取) refreshTable(); // 双击行打开编辑(模拟) table.addListener(SWT.DefaultSelection, event -> { TableItem item = table.getSelection()[0]; int id = Integer.parseInt(item.getText(0)); // 此处应打开BookEditDialog并传入id,本例简化为打印 System.out.println("双击编辑图书ID: " + id); }); } public void refreshTable() { // 清空现有行 table.removeAll(); // 重新查询数据 bookList = new BookDAO().queryAll(); for (Book book : bookList) { TableItem item = new TableItem(table, SWT.NONE); item.setText(new String[]{ String.valueOf(book.getId()), book.getIsbn(), book.getTitle(), book.getAuthor(), book.getPublisher(), book.getStatus() == 1 ? "在馆" : "已借出", // 状态字段映射为中文 "编辑" // 占位操作列,实际可放Button或Link }); } // 自动调整列宽(重要!否则文字被截断) for (TableColumn column : table.getColumns()) { column.pack(); // 根据内容宽度自适应 } } }关键技巧说明:
table.setHeaderVisible(true):必须显式开启表头,否则TableColumn不显示;column.setWidth():手动设宽比pack()更稳定,尤其当数据为空时pack()会缩成一条线;item.setText(String[]):按列索引顺序赋值,索引必须与TableColumn创建顺序严格一致,错一位则全表错乱;table.addListener(SWT.DefaultSelection, ...):SWT中双击事件用DefaultSelection,非Selection(那是单击);column.pack():放在refreshTable()末尾,确保每次刷新后列宽适配新内容,这是让界面“不难看”的后悔药。
4. 数据持久化与DAO层实现:SQLite嵌入式数据库的SWT友好封装
本系统选用SQLite而非MySQL或PostgreSQL,核心原因是零配置、单文件、无服务进程——SWT桌面应用最怕部署时还要用户装数据库服务。DAO层通过DBHelper和BookDAO完成对象-关系映射,但SWT线程模型要求所有UI更新必须在Display.getThread()中执行,而数据库操作应在后台线程,二者需安全桥接。
4.1 DBHelper:建库建表与版本升级的原子性保障
DBHelper继承SQLiteOpenHelper,重点在于onCreate()和onUpgrade()的健壮性:
public class DBHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "bookmanager.db"; private static final int DATABASE_VERSION = 2; // 初始为1,升级时+1 public DBHelper(Context context) { // 注意:SWT中无Android Context,此处Context为自定义空实现,仅占位 // 实际项目中,我们用File路径替代 super(null, DATABASE_NAME, null, DATABASE_VERSION); } // 构造函数重载:接收File路径,绕过Context依赖 public DBHelper(File dbFile) { super(null, dbFile.getAbsolutePath(), null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { // 创建图书表 db.execSQL("CREATE TABLE IF NOT EXISTS books (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "isbn TEXT UNIQUE NOT NULL," + "title TEXT NOT NULL," + "author TEXT," + "publisher TEXT," + "status INTEGER DEFAULT 1," + // 1=在馆, 0=已借出 "create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ")"); // 创建用户表 db.execSQL("CREATE TABLE IF NOT EXISTS users (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "username TEXT UNIQUE NOT NULL," + "password TEXT NOT NULL," + "role TEXT DEFAULT 'user'" + // 'admin' or 'user' ")"); // 插入默认管理员(首次启动) ContentValues admin = new ContentValues(); admin.put("username", "admin"); admin.put("password", "21232f297a57a5a743894a0e4a801fc3"); // MD5('admin') admin.put("role", "admin"); db.insert("users", null, admin); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 版本升级策略:逐级执行,避免跳版丢失数据 if (oldVersion < 2) { // v1 → v2:为books表添加create_time字段 db.execSQL("ALTER TABLE books ADD COLUMN create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP"); } // 若未来有v3,此处追加else if (oldVersion < 3) {...} } }参数与设计要点:
DATABASE_VERSION = 2:当前为2,onUpgrade()中oldVersion < 2判断确保只执行v1到v2的迁移;CREATE TABLE IF NOT EXISTS:防止重复建表报错,SWT应用可能被用户多次双击启动;- 默认管理员密码为MD5('admin'),明文存储仅用于演示,生产环境必须加盐哈希;
ALTER TABLE ... ADD COLUMN:SQLite支持在线添加字段,但不支持删除或修改类型,设计表结构时需预留扩展性。
4.2 BookDAO:线程安全的CRUD与UI线程回调
SWT的Display.asyncExec()是连接后台数据库线程与前台UI的桥梁:
public class BookDAO { private DBHelper dbHelper; public BookDAO() { // 获取db文件路径:与jar同目录的bookmanager.db File dbFile = new File(System.getProperty("user.dir"), "bookmanager.db"); this.dbHelper = new DBHelper(dbFile); } // 查询全部图书(后台线程执行) public List<Book> queryAll() { SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = db.query("books", null, null, null, null, null, "id DESC"); List<Book> list = new ArrayList<>(); while (cursor.moveToNext()) { Book book = new Book(); book.setId(cursor.getInt(cursor.getColumnIndexOrThrow("id"))); book.setIsbn(cursor.getString(cursor.getColumnIndexOrThrow("isbn"))); book.setTitle(cursor.getString(cursor.getColumnIndexOrThrow("title"))); book.setStatus(cursor.getInt(cursor.getColumnIndexOrThrow("status"))); list.add(book); } cursor.close(); db.close(); return list; } // 新增图书(需UI反馈) public boolean insert(Book book) { SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("isbn", book.getIsbn()); values.put("title", book.getTitle()); values.put("author", book.getAuthor()); values.put("publisher", book.getPublisher()); values.put("status", book.getStatus()); long result = db.insert("books", null, values); db.close(); return result != -1; } // 异步插入并刷新UI(推荐用法) public void insertAsync(Book book, final Runnable onSuccess, final Runnable onError) { Thread t = new Thread(() -> { try { boolean success = insert(book); if (success && onSuccess != null) { // 切回UI线程更新表格 Display.getDefault().asyncExec(onSuccess); } else if (!success && onError != null) { Display.getDefault().asyncExec(onError); } } catch (Exception e) { if (onError != null) { Display.getDefault().asyncExec(onError); } } }); t.start(); } }关键逻辑说明:
queryAll()直接返回List<Book>,适用于初始化加载;insertAsync()接受Runnable回调,内部用Thread执行DB操作,成功后通过Display.getDefault().asyncExec()切回UI线程执行onSuccess(如bookListComposite.refreshTable()),这是SWT多线程编程的黄金法则——任何对Control的修改(setText, add, remove)都必须在Display线程;dbHelper.getReadableDatabase()与getWritableDatabase():读操作用前者,写操作用后者,避免锁冲突;Cursor.getColumnIndexOrThrow():比getColumnIndex()更安全,字段不存在时直接抛异常,便于调试。
5. 避坑指南:SWT图书管理系统开发中踩过的五个真实坑
SWT的学习曲线陡峭之处,往往不在API本身,而在那些文档不提、报错模糊、复现困难的边界情况。以下是本项目实测中踩过的五个高频坑,每一条都附带现象、根因与可立即执行的解决方案。
5.1 现象:程序启动时报java.lang.UnsatisfiedLinkError: no swt-win32-4650 in java.library.path,但dll明明在目录里
原因:JVM启动时-Djava.library.path未正确指向dll所在目录,或dll与jar版本不匹配(如jar是4.33,dll却是4.32)。SWT对版本号极其敏感,4650是4.33的内部build号,错一位即失效。
解决:
- 在Eclipse中右键项目 →
Run As → Run Configurations...→Arguments选项卡 →VM arguments中添加:-Djava.library.path="D:\swt\"(路径用英文双引号包裹,末尾带反斜杠) - 核对
swt.jar和swt-win32-4650.dll的文件名,确保build号完全一致; - 若用命令行启动,必须用
java -Djava.library.path="path/to/dll" -jar app.jar,不可省略-D前缀。
5.2 现象:Table中中文显示为方块(□□□),英文正常
原因:SWT默认字体不支持中文,Windows下需显式设置中文字体,且必须在Table创建后、add()数据前设置。
解决:
在BookListComposite.createContents()中table = new Table(...)之后,插入:
Font font = new Font(table.getDisplay(), "Microsoft YaHei", 10, SWT.NORMAL); table.setFont(font); // 同时为TableColumn设置字体(否则表头仍为方块) for (TableColumn column : table.getColumns()) { column.setFont(font); } // 记得在Composite dispose时释放字体资源 addDisposeListener(e -> font.dispose());5.3 现象:点击“删除图书”后,Table未刷新,但数据库已删,重启才显示变化
原因:Table.removeAll()后未调用table.redraw()或table.update(),SWT的绘制是延迟的,需主动触发重绘。
解决:
在BookListComposite.refreshTable()末尾添加:
table.redraw(); // 强制重绘整个Table table.layout(); // 重新计算布局,确保列宽生效注意:
redraw()比update()更彻底,update()只重绘脏区域,有时无效。
5.4 现象:LoginDialog关闭后,主窗口Shell失去焦点,点击无响应,需Alt+Tab切换
原因:Dialog关闭时未显式调用getParent().setFocus(),导致焦点悬空。SWT的焦点管理比Swing更底层,必须手动归还。
解决:
在LoginDialog.handleLogin()成功分支末尾添加:
if (user != null) { setReturnCode(Window.OK); close(); // 关键:将焦点交还给父Shell getParent().setFocus(); }5.5 现象:打包成jar后双击运行,窗口一闪而逝,控制台无任何输出
原因:SWT应用必须有main方法且Display需在主线程启动,但双击jar时JVM默认不显示控制台,错误被静默吞掉。常见于未捕获SWTException或NullPointerException。
解决:
- 打包时确保
MANIFEST.MF中Main-Class: ui.MainApp正确; - 在
MainApp.main()开头添加日志重定向:// 将System.err重定向到文件,便于排查 try { PrintStream log = new PrintStream(new FileOutputStream("swt_error.log", true)); System.setErr(log); } catch (FileNotFoundException e) { e.printStackTrace(); } - 最稳妥方式:用命令行运行
java -jar bookmanager.jar,错误会直接打印在终端。
6. 进阶技巧:为SWT图书系统添加导出Excel与模糊搜索功能
做完基础CRUD只是起点。本节提供两个高价值、低侵入的增强方案:用Apache POI导出图书列表为Excel,以及用TableViewer+ViewerFilter实现带实时反馈的模糊搜索。这两个功能都不需改动DAO层,纯UI增强,且代码可直接复用到其他SWT项目。
6.1 导出Excel:用Apache POI生成兼容Office 2007+的.xlsx文件
SWT本身无导出能力,但POI的XSSFWorkbook与SWT无耦合。关键是在导出时遍历Table的TableItem,而非重新查数据库,保证数据一致性:
private void exportToExcel() { // 1. 创建Excel工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("图书列表"); // 2. 写入表头(复用Table的Column文本) XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < table.getColumnCount(); i++) { XSSFCell cell = headerRow.createCell(i); cell.setCellValue(table.getColumns()[i].getText()); } // 3. 写入数据行(遍历TableItem,非DAO查询) for (int i = 0; i < table.getItemCount(); i++) { TableItem item = table.getItem(i); XSSFRow row = sheet.createRow(i + 1); for (int j = 0; j < item.getText().length; j++) { XSSFCell cell = row.createCell(j); cell.setCellValue(item.getText(j)); // 直接取Table中显示的文本 } } // 4. 自动列宽(POI的autoSizeColumn对中文支持不佳,手动设宽更稳) for (int i = 0; i < table.getColumnCount(); i++) { sheet.autoSizeColumn(i); // 强制最小宽度20字符(约120像素) sheet.setColumnWidth(i, Math.max(sheet.getColumnWidth(i), 20 * 256)); } // 5. 保存文件(用JFileChooser选择路径) FileDialog dialog = new FileDialog(getShell(), SWT.SAVE); dialog.setFilterNames(new String[]{"Excel Files (*.xlsx)", "All Files (*.*)"}); dialog.setFilterExtensions(new String[]{".xlsx", ".*"}); dialog.setFileName("图书列表_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".xlsx"); String path = dialog.open(); if (path != null) { try (FileOutputStream out = new FileOutputStream(path)) { workbook.write(out); MessageBox msg = new MessageBox(getShell(), SWT.ICON_INFORMATION | SWT.OK); msg.setText("导出成功"); msg.setMessage("文件已保存至:" + path); msg.open(); } catch (IOException e) { MessageBox err = new MessageBox(getShell(), SWT.ICON_ERROR | SWT.OK); err.setText("导出失败"); err.setMessage("写入文件时出错:" + e.getMessage()); err.open(); } } // 6. 关闭workbook(必须!否则内存泄漏) try { workbook.close(); } catch (IOException e) { e.printStackTrace(); } }参数与注意事项:
item.getText(j):直接读取Table中渲染的文本,避免DAO二次查询,确保导出内容与界面上看到的一致;sheet.autoSizeColumn(i)后跟Math.max(...):POI的autoSizeColumn对中文列宽计算不准,手动兜底;workbook.close():必须调用,否则JVM不会释放POI的临时文件句柄,多次导出会OOM;FileDialog:SWT原生文件对话框,无需Swing的JFileChooser,保持技术栈纯净。
6.2 模糊搜索:用TableViewer与ViewerFilter实现毫秒级响应
原生Table不支持过滤,但SWT/JFace提供了TableViewer封装,配合ViewerFilter可实现输入即搜:
// 在BookListComposite中,将Table替换为TableViewer private TableViewer tableViewer; private void createContents() { setLayout(new FillLayout()); // 创建TableViewer(自动创建Table) tableViewer = new TableViewer(this, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); Table table = tableViewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); // 定义列(同前) String[] headers = {"ID", "ISBN", "书名", "作者", "出版社", "状态"}; for (String header : headers) { TableColumn column = new TableColumn(table, SWT.CENTER); column.setText(header); column.setWidth(120); } // 设置ContentProvider和LabelProvider(标准JFace模式) tableViewer.setContentProvider(ArrayContentProvider.getInstance()); tableViewer.setLabelProvider(new BookLabelProvider()); // 添加搜索框(放在Composite顶部) Composite searchComp = new Composite(this, SWT.NONE); searchComp.setLayout(new RowLayout(SWT.HORIZONTAL)); searchComp.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); Label lblSearch = new Label(searchComp, SWT.NONE); lblSearch.setText("搜索:"); final Text txtSearch = new Text(searchComp, SWT.SEARCH | SWT.ICON_CANCEL); txtSearch.setLayoutData(new GridData(200, SWT.DEFAULT)); // 绑定搜索过滤器 final ViewerFilter filter = new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (!(element instanceof Book)) return true; Book book = (Book) element; String keyword = txtSearch.getText().trim(); if (keyword.isEmpty()) return true; // 检查ISBN、书名、作者是否包含keyword(忽略大小写) return book.getIsbn().toLowerCase().contains(keyword.toLowerCase()) || book.getTitle().toLowerCase().contains(keyword.toLowerCase()) || book.getAuthor().toLowerCase().contains(keyword.toLowerCase()); } }; // 搜索框回车/取消时触发过滤 txtSearch.addModifyListener(e -> { tableViewer.resetFilters(); // 清除旧过滤 tableViewer.addFilter(filter); // 应用新过滤 tableViewer.refresh(); // 刷新视图 }); // 取消图标点击清空搜索 txtSearch.addListener(SWT.IconCancel, e -> { txtSearch.setText(""); tableViewer.removeFilter(filter); tableViewer.refresh(); }); // 加载初始数据(从DAO) List<Book> allBooks = new BookDAO().queryAll(); tableViewer.setInput(allBooks); } // 自定义LabelProvider,将Book对象映射为Table列文本 class BookLabelProvider extends ColumnLabelProvider { @Override public String getText(Object element) { if (element instanceof Book) { Book book = (Book) element; // 根据列索引返回对应字段(需在TableViewer创建时设置列索引) // 此处简化,实际需重载getColumnText() } return super.getText(element); } @Override public String getColumnText(Object element, int columnIndex) { if (!(element instanceof Book)) return ""; Book book = (Book) element; switch (columnIndex) { case 0: return String.valueOf(book.getId()); case 1: return book.getIsbn(); case 2: return book.getTitle(); case 3: return book.getAuthor(); case 4: return book.getPublisher(); case 5: return book.getStatus() == 1 ? "在馆" : "已借出"; default: return ""; } } }性能与体验优化点:
txtSearch.addModifyListener:监听输入变化,非keyPressed,避免频繁触发;tableViewer.resetFilters():每次搜索前清除旧过滤器,防止叠加;getColumnText():比getText()更精准,按列索引返回值,避免TableItem文本与数据不一致;- 搜索逻辑用
toLowerCase().contains():简单高效,万级数据下响应<100ms,无需Lucene等重型方案。
从那以后我每次做SWT项目,都会在createContents()开头强制加一行System.out.println("UI init start"),并在所有asyncExec()回调里打日志。不是为了炫技,而是当Table突然不刷新、Dialog莫名失焦时,我能立刻判断是DAO没返回、还是Display线程没执行、抑或dispose()被提前调用了——这些坑,文档不写,但
本文还有配套的精品资源,点击获取