资讯动态

医院信息管理系统HIS的Java Swing实战:报告模块开发与避坑指南

发布时间:2026/10/18 14:10:41 来源:尧图企业网站定制
简介医院信息管理系统Java Swing课程设计资源面向高校数据库课设及桌面应用开发学习者。系统完整实现药品类型与药品信息管理、药品入库出库、科室医生病人管理、处方登记及挂号诊断收费等模块数据库层包含十多个表、三个视图、七个触发器、两个存储过程并建立参照完整性约束适合作为课程设计与期末项目的参考模板。包体共279个文件主要包括61个java源码、171个class编译文件、SQL脚本、docx课程设计报告、jar依赖库及png界面截图等压缩包整体10.62MB便于对应查看代码、文档与界面效果。目前已有1621人学习下载。借助该项目可快速理解Swing界面与数据库交互方式可从SQL脚本学习视图、触发器、存储过程的应用也可对照课程设计报告撰写自身项目的设计说明整体结构清晰实用性强。1. 医院信息管理系统为什么还在用 Java Swing定位、场景与「报告」的差异点医院信息管理系统还在用 Java Swing我第一次接手类似项目时也这么问过。但真正看过几家区县级医院的窗口机就会发现Swing 这类老桌面技术不但没消失反而稳稳当当地跑在挂号、收费、药房这些每天开机即用的岗位上。原因不复杂医院内网环境要求的是稳定和可控不是界面多新潮。标题里「包含报告」四个字才是真正的差异点——挂号、收费谁都能写报告这块涉及数据来源、预览排版、打印存档三条链路做不好整个系统就没法验收。这篇内容适合拿它做课程设计的同学也适合接手老项目的维护者。下面直接给能跑的骨架、参数和踩坑记录。2. 搭一个 Swing 版 HIS 的最小骨架登录跳转、数据库连接与后台任务一个能演示的 Swing HIS 最小骨架包含三块登录窗口到主窗口的跳转、数据库访问层、耗时任务的线程处理。很多初学 Swing 的开发者习惯把所有代码塞进按钮事件里结果登录后主窗口卡半天点哪里都没反应。这里给出我一般会用的结构按这个分层写后面加报告模块、患者列表都不会乱。2.1 登录窗体到主窗体用一个静态会话上下文传递用户信息登录后主窗体的工具栏要显示当前医生姓名报告列表要按科室过滤打印操作要记录操作人。如果只用构造器传参遇到对话框、跨窗体回调时会很别扭。常见做法是维护一个静态会话上下文登录成功后写入其它窗口随时读取。// SessionContext.java —— 登录态的全局上下文 public class SessionContext { private static String currentUserId; private static String currentUserName; private static String deptCode; public static void setLoginUser(String userId, String userName, String deptCode) { SessionContext.currentUserId userId; SessionContext.currentUserName userName; SessionContext.deptCode deptCode; } public static String getCurrentUserId() { return currentUserId; } public static String getCurrentUserName() { return currentUserName; } public static String getDeptCode() { return deptCode; } }登录按钮的点击逻辑如下。这里特意用new String(passwordField.getPassword())而不是getText()因为JPasswordField.getText()在较新 JDK 上已经标记废弃密码框返回的是字符数组取完值后最好立即清掉数组内容。// LoginFrame.java —— 登录动作 private void doLogin() { String userId userIdField.getText().trim(); String password new String(passwordField.getPassword()); if (userId.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 账号和密码都不能为空); return; } boolean ok UserDao.checkLogin(userId, password); if (ok) { User u UserDao.findById(userId); SessionContext.setLoginUser(u.getUserId(), u.getUserName(), u.getDeptCode()); dispose(); new MainFrame().setVisible(true); } else { JOptionPane.showMessageDialog(this, 账号或密码错误); } }这段代码有两个细节值得记住。第一登录成功之后先写SessionContext再dispose()当前登录窗避免新窗口打开时读不到用户信息。第二UserDao返回的User对象里不要放密码字段即使放在内存里也是多余风险。课程设计阶段用 SHA-256 加盐存密码就够了生产环境建议直接上现成的登录框架不要在 Swing 里手搓密码学。2.2 JDBC 连接参数驱动、URL 与编码三件套数据库访问层是整个 HIS 的基座。这里以 MySQL 为例但驱动类名和 URL 参数在不同数据库上差别很大SQL Server 是com.microsoft.sqlserver.jdbc.SQLServerDriverOracle 又是另外一套。我习惯把连接参数集中在一个工具类里避免每个 DAO 各写一遍。// DBUtil.java public class DBUtil { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://127.0.0.1:3306/his_db ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER his_app; private static final String PASSWORD his_app_pwd; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(缺少JDBC驱动请检查lib目录); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }参数说明这三个参数踩坑率极高useSSLfalse医院内网环境用不上证书开了反而多一次 SSL 握手局域网里感觉不明显但没必要。serverTimezoneAsia/ShanghaiMySQL 8 的驱动强制要求时区参数不写启动直接报错。characterEncodingutf8中文乱码的头号源头后面避坑章还会展开。连接账号建议单独建一个只有该库权限的账号不要用 root。我见过某课程设计直接把 root 密码写死在代码里提交这种习惯放到医院内网系统里就是事故隐患。DriverManager.getConnection每次调用拿新连接这种写法对演示系统完全够用如果要在门诊高峰期扛并发再换成连接池原理和参数后面单独说。2.3 用 SwingWorker 把慢查询挪出界面线程Swing 的界面更新只能在事件分发线程EDT执行而 JDBC 查询是阻塞操作。如果把查询直接写在按钮回调里窗口会在查询期间完全卡住操作系统甚至会弹「未响应」。SwingWorker 是标准解法它在后台线程执行耗时逻辑再切回 EDT 更新界面。// 主窗口加载待处理报告列表 SwingWorkerListReport, Void worker new SwingWorker() { Override protected ListReport doInBackground() throws Exception { return ReportDao.findPending(SessionContext.getDeptCode()); } Override protected void done() { try { ListReport reports get(); reportTableModel.setRows(reports); } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, 加载待处理报告失败 e.getMessage()); } } }; worker.execute();使用时有两条红线doInBackground()里面绝对不能碰任何 Swing 组件比如不能在这里调用table.setModel()done()方法在 EDT 上执行可以安全更新界面。如果用户在主窗口没加载完就点了关闭done()里再去更新一个已经销毁的组件会抛异常稳妥做法是进入done()先判断MainFrame.this.isDisplayable()。这条判断我每次写都会加后面不知道帮我挡了多少次翻车。3. 把「报告」做成能看能打能存档数据模型、生成预览与打印选型标题里的「包含报告」是验收的硬指标。挂号、收费谁都能做报告模块要把「数据来源、预览排版、打印存档」三条链路打通这四节按这个顺序展开。先设计表结构再讲界面渲染最后落到打印和导出。3.1 检验报告与检查报告一张主表加一张明细表HIS 里的报告通常分两类检验报告是数值型比如血常规、生化一行一个检验项有结果值、参考范围、单位检查报告是描述型比如超声、CT一大段文字结论加几张图。两类数据差异不小但落库可以用一套结构主表存报告头信息明细表存检验项。检查报告不需要明细明细表不写数据就行。-- 报告主表 CREATE TABLE report ( report_id VARCHAR(32) PRIMARY KEY, visit_id VARCHAR(32) NOT NULL COMMENT 关联就诊记录, patient_id VARCHAR(32) NOT NULL, report_type TINYINT NOT NULL COMMENT 1检验 2检查, report_name VARCHAR(128) NOT NULL COMMENT 报告名称如血常规、腹部彩超, doctor_code VARCHAR(32) COMMENT 开单医生, report_content TEXT COMMENT 检查描述或结论, report_status TINYINT DEFAULT 0 COMMENT 0未出 1已出 2已打印, print_count INT DEFAULT 0, create_time DATETIME, update_time DATETIME ); -- 检验明细表 CREATE TABLE report_item ( id INT AUTO_INCREMENT PRIMARY KEY, report_id VARCHAR(32) NOT NULL, item_code VARCHAR(32), item_name VARCHAR(64), item_value VARCHAR(32), ref_range VARCHAR(64), unit VARCHAR(16) );字段设计的几个关键点report_content用 TEXT 而不是 VARCHAR(255)。检查报告经常是一整段描述性文字VARCHAR 长度不够截断后打印出来很难看。report_type用 TINYINT 而不是字符串后续扩展时可以加3病理前端渲染时再映射成中文。print_count用来记录打印次数医院对报告补打有审计要求这个字段配合后面的操作日志能回答「谁在什么时候打了几次」。report_status驱动报告列表的筛选待打印和已打印分开展示窗口科室就不会拿着旧报告去问医生。课程设计里没有外部系统推送报告数据常见做法是做一个报告录入界面医生选择患者和开单申请后手工填结果。真实医院环境里检验报告大多由 LIS 系统自动推送但表结构完全兼容只是数据写入方从人工录入变成接口调用。3.2 用 JTextPane 渲染报告模板HTML 排版比手动 setText 更稳报告预览不能直接拿 JTable 去打印表格打印的排版很难控制分页。我一般把报告渲染成一个只读的 JTextPane内容用 HTML 片段拼出来。Swing 自带的 HTML 渲染支持 table、font、align 这些基础标签足够把一份报告排得像模像样。// ReportView.java —— 报告预览窗格 JTextPane pane new JTextPane(); pane.setContentType(text/html; charsetUTF-8); pane.setEditable(false); pane.setText(buildReportHtml(report, items)); private String buildReportHtml(Report r, ListReportItem items) { StringBuilder sb new StringBuilder(); sb.append(htmlbody stylefont-family:宋体;font-size:12pt); sb.append(h2 styletext-align:center).append(r.getReportName()).append(/h2); sb.append(p患者ID).append(r.getPatientId()).append(/p); sb.append(table border1 cellpadding4 styleborder-collapse:collapse); sb.append(trth项目/thth结果/thth参考范围/thth单位/th/tr); for (ReportItem item : items) { sb.append(trtd).append(item.getItemName()).append(/td); sb.append(td).append(item.getItemValue()).append(/td); sb.append(td).append(item.getRefRange()).append(/td); sb.append(td).append(item.getUnit()).append(/td/tr); } sb.append(/tablebr/); sb.append(p诊断意见).append(r.getReportContent()).append(/p); sb.append(/body/html); return sb.toString(); }这里的关键是pane.setContentType(text/html; charsetUTF-8)一定要带charsetUTF-8否则 HTML 里的中文在某些系统字体下直接变成方框。用 HTML 模板而不是一行行setText拼排版好处是换行、居中、表格边框统一由 HTML 处理后期调字号改一个地方就行。JTextPane 对 HTML 的支持有限不要用 CSS3 或者复杂嵌套保持这份代码的标签级别最稳。3.3 打印的两条路Printable 接口与打印对话框的参数控制Swing 报告打印有两条路一条是直接调JTextPane.print()简单但无法精确控制页边距和缩放另一条是实现Printable接口用Graphics2D自己画。医院报告对版式有要求我一般用 Printable 方案// ReportPrintable.java public class ReportPrintable implements Printable { private JTextPane pane; public ReportPrintable(JTextPane pane) { this.pane pane; } Override public int print(Graphics g, PageFormat pf, int pageIndex) { Graphics2D g2 (Graphics2D) g; g2.translate(pf.getImageableX(), pf.getImageableY()); double scale pf.getImageableWidth() / pane.getWidth(); g2.scale(scale, scale); pane.printAll(g2); return Printable.PAGE_EXISTS; } }调用侧配合PrinterJobPrinterJob job PrinterJob.getPrinterJob(); job.setPrintable(new ReportPrintable(pane)); if (job.printDialog()) { job.print(); ReportDao.markPrinted(reportId); }参数说明translate和scale是把预览窗格的内容等比缩放到打印纸的可打印区域。pane.getWidth()是预览时窗格的像素宽度实际打印时以打印区域宽度为准做缩放。调用printDialog()让用户选打印机和份数不要绕过对话框直接job.print()医院窗口的打印机经常有多台直接打印会打到默认设备上出乱子。打印成功后立刻更新report_status和print_count这一步既是业务要求也是审计留痕的依据。3.4 导出与存档CSV 给明细PDF 给完整报告报告只有打印不够还要能导出存档。检验明细导出 CSV 最稳纯 JDK 代码就能写不引入额外依赖。需要注意 Excel 打开 UTF-8 的 CSV 会乱码解决办法是文件开头写一个 BOM 字符// 检验明细导出 CSV try (OutputStreamWriter fw new OutputStreamWriter( new FileOutputStream(report_ reportId .csv), StandardCharsets.UTF_8)) { fw.write(\ufeff); fw.write(项目,结果,参考范围,单位\n); for (ReportItem item : items) { fw.write(item.getItemName() , item.getItemValue() , item.getRefRange() , item.getUnit() \n); } }CSV 方案够用但不适合整份报告因为检查报告的段落文字里可能包含逗号和换行CSV 转义会让人抓狂。完整报告存档用开源 PDF 库但要注意引入依赖时确认新旧坐标不冲突我见过有人同时引了两套包运行时类加载直接报错。如果项目不打算引入 PDF 库退一步的做法是把 HTML 预览内容原样存成.html文件浏览器随时能打开和打印只是格式约束弱一些。课程设计做到 CSV 加 HTML 存档已经完全够演示生产环境再上 PDF 方案。4. 用 JTable 撑起患者列表和挂号队列渲染、分页与状态着色的三个要点Swing 版 HIS 的界面核心就是 JTable。患者列表、挂号队列、待打印报告列表全部要落在表格上。这一章讲三个必踩的点TableModel 的选择、渲染器的性能边界、分页查询的写法。4.1 自定义 TableModel 而不是硬塞 DefaultTableModel课程设计里最常见的写法是new DefaultTableModel(data, columnNames)快是真快但后续取单元格值时拿到的全是Object还要自己转类型。报告列表需要根据行数据做双击查看、右键打印我通常直接继承AbstractTableModel// ReportTableModel.java public class ReportTableModel extends AbstractTableModel { private ListReport data new ArrayList(); private String[] columns {报告ID, 患者ID, 报告名称, 类型, 状态, 报告时间}; Override public int getRowCount() { return data.size(); } Override public int getColumnCount() { return columns.length; } Override public Object getValueAt(int row, int col) { Report r data.get(row); switch (col) { case 0: return r.getReportId(); case 1: return r.getPatientId(); case 2: return r.getReportName(); case 3: return r.getReportType() 1 ? 检验 : 检查; case 4: return statusText(r.getReportStatus()); case 5: return new SimpleDateFormat(yyyy-MM-dd HH:mm).format(r.getUpdateTime()); default: return ; } } public void setRows(ListReport rows) { this.data rows; fireTableDataChanged(); } }自定义模型的核心收益是列和后台对象直接映射getValueAt里做类型转换一次完成。后面做渲染器时getValueAt返回的已经是易读的字符串不用在渲染器里再判断一次Report类型。setRows末尾的fireTableDataChanged()一定要调否则界面不会刷新这是新手最容易漏的一步。4.2 状态列着色渲染器的高频调用与性能边界报告列表的「状态」列需要颜色区分未出的标红已出的标绿已打印的用默认色。JTable 的单元格渲染器会被高频调用滚动、重绘、鼠标划过都会触发所以渲染器内部绝不能做耗时操作。// 状态列渲染器绑定在表格第5列 table.getColumnModel().getColumn(4).setCellRenderer( new DefaultTableCellRenderer() { Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (!isSelected) { if (未出.equals(value)) { c.setForeground(new Color(0xCC0000)); } else if (已出.equals(value)) { c.setForeground(new Color(0x006600)); } else { c.setForeground(table.getForeground()); } } return c; } });这里有两个容易被忽略的点。第一只在!isSelected时改前景色选中状态交给系统的选中配色否则选了行之后红字和选中背景糊在一起。第二不要在getTableCellRendererComponent里创建新对象比如不要每次 new 一个Color实例颜色值应该提成静态常量更不要在这里查数据库滚动一下列表就会触发几十次查询数据库直接被打满。4.3 SQL 分页比内存分页靠谱LIMIT 参数与关键字转义JTable 本身不带分页要自己实现。内存分页适合几百条数据的小列表但 HIS 的挂号队列一天就好几千条全查出来塞进内存不仅慢还会拖垮界面。我一般用 SQL 分页数据库只返回当前页需要的数据// ReportDao.java —— 分页查询 public ListReport pageQuery(String deptCode, int page, int pageSize, String keyword) { String sql SELECT * FROM report WHERE dept_code? AND (report_name LIKE ? OR patient_id LIKE ?) LIMIT ?,?; String like % keyword.replace(%, \\%).replace(_, \\_) %; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, deptCode); ps.setString(2, like); ps.setString(3, like); ps.setInt(4, (page - 1) * pageSize); ps.setInt(5, pageSize); return mapRows(ps.executeQuery()); } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } }参数说明(page - 1) * pageSize把页码从 1 换成 SQL 的偏移量LIMIT ?,?第一个问号是 offset第二个是行数。关键字里如果包含%或_会把LIKE的通配符带进去导致匹配到意料之外的记录要提前转义成\%和\_MySQL 默认转义字符是反斜杠所以这里的replace写法在通用场景下没问题跨数据库时记得确认对应数据库的转义语法。分页控件的两个参数——页码和每页条数——建议放在表格下方用JSpinner或下拉框控制页大小。页大小我一般设 20报告列表这种场景 20 行一屏刚好翻页也不频繁。页码变化时重新发起查询数据加载用第 2 章说的 SwingWorker避免切换页码时界面卡顿。5. Swing HIS 避坑指南线程阻塞、乱码、内存泄漏与打包部署的 5 个典型问题这一章全是真实项目里反复出现的坑按「现象 → 原因 → 解决」记录。每一条我都亲手踩过有些是课程设计阶段踩的有些是老项目维护时帮别人填的。5.1 界面假死JDBC 查询不能占用 UI 线程现象点了「查询全部患者」按钮窗口转圈十几秒没响应标题栏出现「未响应」任务管理器里 Java 进程的 CPU 占用却不高。原因按钮事件回调里直接执行了 JDBC 查询。Swing 是单线程界面模型EDT 被阻塞后重绘、鼠标事件全部排队界面看起来就是死了。解决所有数据库查询都放进 SwingWorker 的doInBackground()查询结束后在done()里更新表格模型。如果查询中途要弹进度条用publish()和process()配合。还有一个隐藏变体有人把查询放进了TableModel.getValueAt()这比按钮事件更隐蔽滚动表格就触发一堆查询同样会卡死。5.2 中文乱码URL、建表、写文件三处编码要一致现象界面按钮都是正常中文但数据库读出来的中文显示成问号导出 CSV 后 Excel 打开是乱码。原因三处编码不一致。连接 URL 没带characterEncodingutf8建表时字符集不是utf8mb4CSV 文件没写 BOM。解决连接 URL 补上编码参数建表语句里加DEFAULT CHARSETutf8mb4导出 CSV 时先写一个\ufeff字符。还有一个容易被忽略的场景是 JTextPane 的 HTML 渲染setContentType(text/html; charsetUTF-8)里没写 charset 也会乱码。这三个地方一次检查完中文问题基本能根治。5.3 窗口关了进程还在监听器和 Timer 的内存泄漏现象点了窗口右上角的 X任务管理器里 Java 进程不退重复登录几次内存占用一直涨。原因窗口关闭只释放了引用但注册的监听器没有被移除后台 SwingWorker 或 Timer 还在运行EDT 上仍有对窗口的引用垃圾回收无法回收窗体对象。解决在窗口关闭监听器里做清理注销鼠标监听、停止 Timer、取消还在跑的 SwingWorkerframe.addWindowListener(new WindowAdapter() { Override public void windowClosing(WindowEvent e) { if (refreshTimer ! null) { refreshTimer.stop(); } table.removeMouseListener(doubleClickAdapter); frame.dispose(); } });顺带一提课程设计通常不需要这么讲究但如果你要把系统交给医院信息科验收他们拿 VisualVM 一盯内存曲线这个坑躲不过去。5.4 打包后双击没反应jar 内资源与外部配置路径现象在 IDE 里运行一切都正常打成可执行 jar 后双击没反应或者报找不到图片、配置文件。原因IDE 运行时用的是文件系统路径而 jar 内部是压缩包的逻辑路径。代码里如果用new File(config/db.properties)读外部文件jar 内部根本没有这个盘符路径。解决jar 内的资源用getResourceAsStream(/config/db.properties)读取对外部配置放在 jar 同级的config目录用System.getProperty(user.dir)定位当前目录再拼接路径。数据库连接串这种每个医院都不一样的配置必须走外部文件不能写死在代码里重新打包。5.5 隔夜后查询报错数据库空闲连接被回收现象系统前一天晚上还能正常用第二天早上第一个操作就报CommunicationsException或Connection is not available。原因MySQL 的wait_timeout参数默认把空闲超过 8 小时的连接关掉了代码里持有的是早已失效的连接对象。解决最直接的做法是不持有长连接每次查询从DBUtil.getConnection()拿新连接用完马上关闭课程设计阶段这样写完全够。医疗系统上线后要上连接池把maxLifetime设成小于 MySQL 的wait_timeout比如 5 分钟这样连接池会在服务端回收前主动换新连接。注意连接池不是配上就完事这个数值要跟数据库配置对齐否则照样踩坑。6. 从能跑到能上线报告批量导出与内存走查的收尾习惯功能跑通后离「能交给科室用」还差两步批量导出和权限留痕。这两步课堂项目通常不做但验收必查。6.1 批量导出用 SwingWorker 带进度条报告列表支持多选导出导出几十份报告是耗时操作要带进度条。用 SwingWorker 的publish和process组合// 批量导出任务 SwingWorkerInteger, Integer exporter new SwingWorker() { Override protected Integer doInBackground() throws Exception { for (int i 0; i reportIds.size(); i) { exportOne(reportIds.get(i)); publish(i 1); } return reportIds.size(); } Override protected void process(ListInteger chunks) { int latest chunks.get(chunks.size() - 1); progressBar.setValue(latest); progressLabel.setText(已导出 latest / reportIds.size()); } }; exporter.execute();publish可以在后台线程多次调用process在 EDT 上合并执行不会因为频繁刷新而卡界面。6.2 交付前用 VisualVM 盯一轮内存曲线每次交付前我习惯做一轮走查打开报告预览打印关闭窗口连续重复十次以上观察堆内存曲线是否持续上升不回弹。持续上升就说明有监听器泄漏或 SwingWorker 没有正确结束。再检查一遍操作日志表确认打印、导出动作都留了痕。这两件事做完系统才算「能上线」而不是「能演示」。说到打印走查血泪经验是把检验报告、检查报告各打印一份样张确认字体是宋体而不是默认字体页码位置正确超过一页的长报告第二页表头有没有重复。这些细节决定窗口科室愿不愿意真的用你的系统。我自己每次交付 Swing 项目最后一天就只做这两件事把批量导出和日志补完再用 VisualVM 盯二十分钟内存曲线。这两件事帮我挡掉了至少三次验收翻车。打印确认也是同样套路所有报告类型过一遍预览才敢说完工。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑