基于SWT的Java桌面图书管理系统实战:从选型到二次开发
简介:这份资源是一套基于SWT构建的Java图书管理系统图形化界面项目,面向具备Java基础、希望掌握桌面端GUI开发与数据库联动的学习者。项目围绕图书信息管理、借阅归还、读者维护等常见业务展开,涵盖SWT组件使用、事件驱动编程、布局管理、表格与对话框设计,以及JDBC数据库操作等实践要点,适合作为课程设计或毕业设计的参考案例。压缩包共259个文件,约10.07MB,包含45个java源文件、107个class编译文件、80个ico图标、12个jpg图片、7个jar依赖包,另附sql建库脚本、doc说明文档与exe可执行程序,源码、资源与运行环境相对完整。目前已有484人学习下载,读者可从中获取可运行的界面原型、模块划分思路与数据库脚本,便于对照理解SWT项目结构并在此基础上二次开发。
1. 桌面端图书管理:用 SWT 把 Java 图形界面做扎实
很多团队做图书管理这类内部工具时,第一反应是上 Web 框架,结果部署一套环境比写业务还费劲。这个项目反其道而行,用 SWT 直接做桌面客户端,双击就能跑,不依赖浏览器和额外服务,特别适合学校图书室、小型藏书机构这类场景。它把图书录入、查询、借还、分类维护这些高频操作收进一个原生窗口里,界面响应快,操作路径短。如果你正在找一个能直接跑起来、又能顺着改的 Java 图形化图书管理项目,这套 SWT 实现值得拆开看——它把桌面端最容易翻车的线程模型和表格刷新都处理得比较干净。
2. 技术选型与工程骨架:SWT 为什么比 Swing 更值得改
2.1 选 SWT 而不是 Swing 的现实理由
SWT 是 Eclipse 基金会维护的原生控件库,它调用操作系统自身的窗口组件,所以在 Windows 上跑出来的按钮、表格、输入框就是系统原生样式,不会出现 Swing 那种“一眼 Java 味”的观感。对图书管理这种要给非技术馆员天天用的工具来说,观感直接影响接受度。另一个实际好处是 SWT 的表格控件Table、TableViewer在处理几百上千条书目时性能稳定,配合虚拟表还能扛住更大的数据量。
代价也有:SWT 依赖本地库,打包时要带上对应平台的 jar,跨平台分发比纯 Java 麻烦一点。这个项目面向的是单机或局域网内固定几台机器,所以这个代价可以接受。常见做法是把平台相关的 SWT jar 单独放一个lib目录,启动脚本里按操作系统拼 classpath,避免把多个平台的库混在一起导致加载失败。
2.2 目录结构长什么样
一个能跑起来的 SWT 图书管理工程,骨架大致是这样组织的:
book-manager-swt/ ├── lib/ │ ├── swt-win32-x86_64.jar # 平台相关本地库 │ └── sqlite-jdbc.jar # 若用 SQLite 持久化 ├── src/ │ └── com/example/book/ │ ├── App.java # 入口,创建 Display 与主窗口 │ ├── ui/ │ │ ├── MainWindow.java # 主界面与菜单 │ │ ├── BookForm.java # 图书录入/编辑对话框 │ │ └── BorrowPanel.java # 借还操作面板 │ ├── model/ │ │ ├── Book.java # 图书实体 │ │ └── BorrowRecord.java # 借阅记录实体 │ ├── dao/ │ │ ├── BookDao.java # 图书增删改查 │ │ └── RecordDao.java # 借还记录读写 │ └── util/ │ └── DbUtil.java # 连接与建表 └── run.bat / run.sh # 启动脚本这个分层不是摆设:ui只负责控件和事件绑定,dao只碰数据库,model是纯数据对象。改需求时你基本只需要动其中一层,比如加一个“按出版社筛选”,改BookDao的查询方法和MainWindow的筛选控件即可,不会牵一发动全身。
2.3 入口与 Display 生命周期
SWT 程序的入口和 Swing 不一样,必须先有Display,再有Shell,事件循环靠readAndDispatch驱动。这是整个项目最容易写错的地方,先看入口代码:
public class App { public static void main(String[] args) { Display display = new Display(); // 每个线程一个 Display Shell shell = new Shell(display); // 主窗口 shell.setText("图书管理系统"); shell.setSize(900, 600); new MainWindow(shell).build(); // 构建菜单与内容区 shell.open(); while (!shell.isDisposed()) { // 标准 SWT 事件循环 if (!display.readAndDispatch()) { display.sleep(); // 无事件时让出 CPU } } display.dispose(); // 释放本地资源 } }Display是 SWT 与操作系统之间的桥梁,一个线程只能有一个,UI 线程就是创建它的线程。readAndDispatch从系统事件队列取事件并分发,返回 false 表示当前没有待处理事件,这时调用sleep让线程挂起,避免空转吃满 CPU。shell.isDisposed()是循环退出条件,用户关窗后Shell被销毁,循环自然结束。最后display.dispose()必须调用,否则本地资源不释放,反复启动调试时容易残留句柄。
提示:所有 UI 操作都必须在创建
Display的那个线程里执行。后台线程要更新界面,必须通过display.asyncExec或syncExec切回来,直接跨线程改控件会抛Invalid thread access。
3. 核心模块拆解:从图书录入到借还闭环
3.1 主窗口布局与菜单组织
主窗口承担导航职责,通常用Menu加TabFolder的组合:菜单放“图书管理”“借还管理”“系统”这些一级入口,TabFolder把不同业务面板并排,馆员点一下就能切换。布局用GridLayout或FillLayout,前者更灵活,能控制每列的grabExcessHorizontalSpace让表格随窗口拉伸。
public class MainWindow { private final Shell shell; public MainWindow(Shell shell) { this.shell = shell; } public void build() { shell.setLayout(new FillLayout()); TabFolder tabs = new TabFolder(shell, SWT.NONE); TabItem bookTab = new TabItem(tabs, SWT.NONE); bookTab.setText("图书管理"); bookTab.setControl(new BookPanel(tabs).create()); // 图书列表与操作 TabItem borrowTab = new TabItem(tabs, SWT.NONE); borrowTab.setText("借还管理"); borrowTab.setControl(new BorrowPanel(tabs).create()); createMenuBar(); } private void createMenuBar() { Menu bar = new Menu(shell, SWT.BAR); shell.setMenuBar(bar); MenuItem fileItem = new MenuItem(bar, SWT.CASCADE); fileItem.setText("系统"); Menu fileMenu = new Menu(shell, SWT.DROP_DOWN); fileItem.setMenu(fileMenu); MenuItem exitItem = new MenuItem(fileMenu, SWT.PUSH); exitItem.setText("退出"); exitItem.addListener(SWT.Selection, e -> shell.close()); } }TabFolder的每个TabItem通过setControl挂一个Composite,面板自己负责内部控件,主窗口不关心细节,这是 SWT 里做模块化的标准手法。菜单用SWT.CASCADE表示有下拉子菜单,SWT.PUSH是普通可点击项。退出项直接shell.close(),会触发窗口销毁并让入口的事件循环退出,不需要额外维护状态标志。
3.2 图书表格与数据绑定
图书列表是使用频率最高的区域,用TableViewer比裸Table省事,它自带列管理、排序和内容提供器。核心是把Book列表喂给ArrayContentProvider,再用TableViewerColumn定义每列怎么取值。
TableViewer viewer = new TableViewer(parent, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); viewer.getTable().setHeaderVisible(true); viewer.getTable().setLinesVisible(true); viewer.setContentProvider(ArrayContentProvider.getInstance()); String[] headers = {"编号", "书名", "作者", "出版社", "库存"}; int[] widths = {80, 220, 140, 180, 80}; for (int i = 0; i < headers.length; i++) { TableViewerColumn col = new TableViewerColumn(viewer, SWT.NONE); col.getColumn().setText(headers[i]); col.getColumn().setWidth(widths[i]); final int idx = i; col.setLabelProvider(new ColumnLabelProvider() { @Override public String getText(Object element) { Book b = (Book) element; switch (idx) { case 0: return b.getId(); case 1: return b.getTitle(); case 2: return b.getAuthor(); case 3: return b.getPublisher(); default: return String.valueOf(b.getStock()); } } }); } viewer.setInput(bookDao.findAll()); // 首次加载SWT.FULL_SELECTION让整行可选中,SWT.MULTI支持批量删除。ArrayContentProvider把集合直接当数据源,ColumnLabelProvider负责把对象字段映射成单元格文本。setInput是刷新入口,增删改之后重新查库再setInput一次即可,TableViewer会自动重建行。数据量大的话可以换成LazyContentProvider配合虚拟表,但几百条书目用当前写法完全够。
3.3 借还操作的业务闭环
借还模块要处理库存扣减和记录写入,这两步必须在一个事务里,否则会出现“记录写了但库存没减”的脏数据。DAO 层用 JDBC 事务包住:
public boolean borrow(String bookId, String borrower) { Connection conn = null; try { conn = DbUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 int stock = bookDao.queryStock(conn, bookId); if (stock <= 0) { conn.rollback(); return false; // 库存不足,直接拒绝 } bookDao.decreaseStock(conn, bookId); // 库存 -1 recordDao.insert(conn, bookId, borrower, new Date()); conn.commit(); // 两步都成功才提交 return true; } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} throw new RuntimeException("借书失败", e); } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }setAutoCommit(false)之后,decreaseStock和insert共享同一个连接,任何一步抛异常都走rollback,保证库存和记录一致。库存检查放在事务内,避免并发下两个请求都读到“还有 1 本”然后各扣一次。还书逻辑对称:库存 +1、记录标记归还时间。UI 层拿到false就弹提示框,拿到异常就提示“操作失败,请重试”,不要把 SQL 异常原文甩给馆员看。
3.4 后台任务与界面刷新
导入书目、批量导出这类耗时操作不能放在 UI 线程,否则窗口直接卡死。SWT 的做法是开一个普通线程干活,干完用display.asyncExec回到 UI 线程刷新:
new Thread(() -> { List<Book> imported = excelImporter.parse(file); // 耗时解析 bookDao.batchInsert(imported); // 批量入库 display.asyncExec(() -> { // 切回 UI 线程 viewer.setInput(bookDao.findAll()); MessageDialog.openInformation(shell, "完成", "成功导入 " + imported.size() + " 条书目"); }); }).start();asyncExec把 Runnable 投递到 UI 线程的事件队列,不阻塞后台线程;如果后续逻辑依赖界面更新结果,用syncExec同步等待。这里的关键是后台线程绝不碰任何控件,只做数据准备,刷新动作统一交给asyncExec里的代码。踩过这个坑的人都知道,跨线程直接viewer.setInput在部分平台上不报错但界面不刷新,排查起来很费时间。
4. 跑起来与避坑:环境、依赖与常见故障
4.1 环境准备与启动步骤
运行前确认 JDK 版本和 SWT jar 的平台匹配。SWT 的本地库和操作系统、位数强绑定,Windows 64 位就得用 win32-x86_64 那个包,拿错了启动直接报UnsatisfiedLinkError。
# 1. 确认 JDK(建议 8 或 11,与 SWT 版本匹配) java -version # 2. 编译,把 lib 下所有 jar 加进 classpath javac -encoding UTF-8 -cp "lib/*" -d out $(find src -name "*.java") # 3. 运行(Windows 示例) java -cp "out;lib/*" com.example.book.AppLinux 下 classpath 分隔符是冒号,启动脚本里要区分。如果项目用 Maven,pom.xml里 SWT 依赖要带classifier指定平台,比如win32-win32-x86_64,否则拉下来的是空壳。数据库若用 SQLite,首次启动会自动建表,建表语句放在DbUtil.initSchema里,用CREATE TABLE IF NOT EXISTS保证重复启动不报错。
4.2 常见问题排查
现象:启动报Invalid thread access。原因是在非 UI 线程里创建或修改了控件,常见于在后台线程里直接new Label(...)或setText。解决是把所有控件操作收敛到 UI 线程,后台只准备数据,刷新走display.asyncExec。
现象:表格数据改了但界面没变。原因是只改了内存里的List,没重新setInput,或者Book对象没有触发查看器刷新。解决是每次增删改后重新查库并viewer.setInput(...),简单直接,别指望查看器自动感知集合变化。
现象:窗口能开但按钮点了没反应。多半是事件监听器加在了错误的控件上,或者addListener的SWT.Selection写成了别的常量。检查监听器绑定的控件实例和事件类型,Button用addSelectionListener更直观。
现象:打包后换台机器就起不来。原因是 SWT 本地库没跟着打进去,或者打成了别的平台版本。解决是按目标平台单独出包,lib里只放对应平台的 jar,启动脚本里 classpath 写相对路径。
现象:借书后库存对不上。原因是库存扣减和记录写入没在同一事务里,中途异常导致只执行了一半。解决是按 3.3 的写法用setAutoCommit(false)包住两步,异常统一回滚。
注意:调试 SWT 程序时如果进程退不干净,先确认
display.dispose()有没有执行,再检查是否有非守护线程还在跑。残留的Display会让下一次启动行为异常。
5. 二次开发:把单机工具改成能扩展的形态
拿到这套代码后,最值得先动的是持久层和界面解耦这两处。当前 DAO 直接写 SQL,换数据库要改多处;一个稳妥的改法是抽一个BookRepository接口,SqliteBookRepository实现它,UI 只依赖接口。这样以后要接 MySQL 或换成内存实现做测试,加一个实现类就行,BookPanel一行不用改。接口方法按业务语义命名,比如findByKeyword、decreaseStock,别把 SQL 细节漏到接口签名里。
第二个改造点是查询条件。现在多半是“输入框 + 查询按钮”的固定组合,扩展性差。可以引入一个BookQuery对象,把书名、作者、出版社、库存状态都塞进去,DAO 按非空字段动态拼WHERE。这样加筛选条件只改BookQuery和拼 SQL 的方法,界面加个输入框即可。动态拼 SQL 时用PreparedStatement占位符,别字符串拼接,避免注入和转义问题。
第三个是导出功能。馆员迟早会要“把当前列表导成 Excel”,在TableViewer旁边加个按钮,取viewer.getInput()的集合,用 POI 写文件。导出放后台线程,完成提示走asyncExec,和 3.4 的导入对称。这里有个小技巧:导出前先viewer.getTable().getSelection()判断有没有选中行,有选中就导选中,没有就导全部,交互上更符合直觉。
验证改造是否成功,我一般按三步走:先跑一遍原有借还流程确认没回归,再构造边界数据(库存为 0、书名含特殊字符、超长作者名)看界面和数据库是否一致,最后换一台没装过开发环境的机器按启动脚本跑一遍。第三步最能暴露依赖和平台问题,血泪经验是别只在自己机器上验证。
从那以后我每次改这类桌面项目,都强制先把“换机器能跑”走一遍再谈功能,后悔药没处买。希望帮到你。
本文还有配套的精品资源,点击获取