本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:用Java Swing做的纯桌面版火车票管理程序,不用装Tomcat也不用连真实数据库,所有数据存在dbFile.txt里,启动就能用。界面有车次查询框、余票实时显示列表、购票弹窗、退票确认流程,操作逻辑清晰。项目结构规整,src下按cn.火车票售票管理系统1分包,image文件夹放按钮图标,bg.png是主窗口背景图,还附带Oracle驱动classes12.jar(兼容老版本JDK),init_db.py可重置测试数据。配套的wenzhang.doc是完整课程设计报告,含需求分析、类图说明、MVC模块划分、关键事件监听代码解释和实际运行截图。Eclipse直接导入.project和.classpath就能编译运行,适合Java GUI初学者练手、交课设作业或理解Swing组件布局与ActionListener响应机制。

1. 项目概述:为什么一个“土味”火车票系统,反而成了Java GUI教学的黄金样本?

你可能第一眼看到这个标题会皱眉:“都2024年了,还用Swing写火车票系统?不是早该上Web和Spring Boot了吗?”——这话没错,但恰恰是这种“看似过时”的技术选型,让它在高校Java教学场景里稳坐C位十年不倒。我带过六届Java课程设计,每年都有至少三分之一的学生选它,不是因为炫技,而是因为它把GUI编程最核心、最易卡壳的痛点,全摊开在阳光下让你亲手调试。它不依赖Tomcat,不连真实数据库,所有数据存进一个叫dbFile.txt的纯文本文件里;它不用Maven花哨的依赖管理,就靠一个classes12.jar(Oracle老驱动)和Eclipse原生配置就能跑起来;它的包名甚至带着中文cn.火车票售票管理系统1——这在企业开发里是红线,在课堂作业里却是救命稻草:学生不用花三天搞懂Maven坐标冲突,也不用被JDBC连接池配置绕晕,上来就能点开MainFrame.java,盯着JTable怎么刷新余票、JOptionPane弹窗怎么拦截退票确认、ActionListener怎么把“查票”按钮点击变成一串FileReader读取逻辑。

关键词里“Java Swing”“火车票系统”“课程设计源码”三个词,其实暗含三层教学意图:Swing是载体,火车票是业务外壳,课程设计才是本质目标。它要训练的不是“做一个能上线的购票App”,而是“理解事件驱动如何改变UI状态”“明白数据如何从文件流经Model层再渲染到View”“掌握GridLayoutBorderLayout混搭时组件尺寸为何突然消失”。比如那个bg.png背景图,表面看只是美化,实则逼你去查JLabel设背景的坑——直接setBackground()无效,必须用setOpaque(false)配合paintComponent()重绘;而image目录下的图标,又牵扯到ImageIcon路径加载失败时的NullPointerException排查。这些细节,在Spring Boot+Vue的工程里会被层层封装掩盖,但在这里,它们就是你编译报错的第一行堆栈。所以别小看这个“土味”系统:它没用一行注释告诉你“此处为MVC分层”,但当你把TicketService.java里的queryTickets()方法断点打进去,看着ArrayList<Ticket>dbFile.txt逐行解析出来,再被DefaultTableModel塞进表格——那一刻,MVC不是概念,是心跳。

2. 整体架构与设计思路:文本文件当数据库,不是偷懒,是精准教学

2.1 为什么放弃SQLite或H2,死磕dbFile.txt?

看到dbFile.txt,新手常误以为是“偷工减料”。实则这是经过反复验证的教学最优解。我对比过三种方案:
- 真实Oracle数据库:需装服务端、配TNS、处理ClassNotFoundException(驱动未加载)、学生电脑防火墙拦截端口……一节课光解决环境问题就超时;
- 嵌入式H2:虽免安装,但jdbc:h2:mem:testdb内存库重启即失数据,学生做退票测试时发现“刚买的票不见了”,以为代码有bug,实际是H2默认配置问题;
- 文本文件模拟dbFile.txt每行一条车次记录,字段用|分隔(如G101|北京南|上海虹桥|08:00|12:30|560|120),解析逻辑仅需String.split("\\|")。学生能用记事本直接修改余票数,立刻看到界面刷新——这种“所见即所得”的反馈,对建立编程信心至关重要。

更关键的是,它强制暴露了数据持久化的本质矛盾:文件I/O是阻塞的,而Swing主线程不能卡住。所以你在TicketService.java里会看到FileWriter外层套着SwingUtilities.invokeLater()——这不是炫技,是教学生理解“为什么更新余票后表格没变,加了这句就灵了”。这种设计,把抽象的“线程安全”概念,转化成一行可调试的代码。

2.2 包结构cn.火车票售票管理系统1:中文包名背后的教学深意

src/cn/火车票售票管理系统1/ 这个路径,企业开发中绝对禁止,但课程设计里它是一把钥匙。原因有三:
1. 破除路径恐惧:学生常因package com.example.ticket报错而崩溃,以为必须联网下载com.example域名。用中文包名,直接告诉他们:“包名只是命名空间,和物理路径强绑定,和网络无关”;
2. 强化MVC具象化:打开该目录,你能清晰看到view/MainFrame.javaBuyTicketDialog.java)、controller/QueryAction.javaBuyAction.java)、model/Ticket.javaTicketService.java)三个子包。view里所有Swing组件创建逻辑,controller里所有ActionListener实现,model里所有数据操作——边界清晰到可以画出类图。我在指导时会让学生删掉controller包,把监听器代码全塞进MainFrame,再对比运行效果:界面卡顿、代码臃肿、修改购票逻辑要翻10个文件……反向证明分层价值;
3. 规避IDE自动纠错干扰:Eclipse对英文包名有过度智能提示(如自动补全com.),反而让学生忽略手动输入过程。中文包名迫使他们逐字敲击,加深路径记忆。

2.3 init_db.py:Python脚本重置数据,跨语言协作的教学伏笔

init_db.py的存在常被忽略,但它埋着重要教学线索。这个脚本只有20行,功能是清空dbFile.txt并写入预设车次数据。表面看是便利工具,实则传递两个信号:
- 数据初始化应独立于业务代码:避免main()方法里硬编码new Ticket("G101",...),体现“关注点分离”;
- 跨语言能力是工程师基本素养:学生用Java写业务,用Python管数据,未来可能用Shell部署、用SQL调优——init_db.py是第一个微小的“多语言协同”实践。我常让学生改写此脚本:把dbFile.txt改成CSV格式,再用OpenCSV库读取,自然衔接到后续“文件格式演进”课题。

3. 核心模块解析与实操要点:从查票到退票,每一行代码都在讲原理

3.1 车次查询模块:JTable动态刷新的底层逻辑

查询功能看似简单,实则是Swing最易翻车的环节。核心在QueryAction.javaactionPerformed()方法:

public void actionPerformed(ActionEvent e) {
    String start = startField.getText().trim();
    String end = endField.getText().trim();
    List<Ticket> tickets = ticketService.queryByStation(start, end); // 1. 数据层查询

    // 2. 模型层更新:关键!不能直接new DefaultTableModel()
    DefaultTableModel model = (DefaultTableModel) table.getModel();
    model.setRowCount(0); // 清空旧数据,非model.setDataVector(null)

    // 3. 视图层填充:逐行添加,非批量
    for (Ticket t : tickets) {
        model.addRow(new Object[]{t.getTrainNo(), t.getStart(), t.getEnd(), 
                                t.getStartTime(), t.getEndTime(), t.getTotal(), t.getLeft()});
    }
}

这里藏着三个必讲知识点:
- 为什么model.setRowCount(0)model.setDataVector(null)更安全?
setDataVector(null)会触发TableModelEvent广播,若表格正在滚动,可能抛ArrayIndexOutOfBoundsException;而setRowCount(0)只清行数,后续addRow()逐条插入,事件触发可控。我在课堂演示时,故意在addRow()前加Thread.sleep(100),让学生观察表格“逐行浮现”的过程,理解事件驱动的异步性。
- JTable列宽自适应失效怎么办?
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF)后,需手动设置列宽:table.getColumnModel().getColumn(0).setPreferredWidth(80)bg.png背景图若尺寸过大,会挤压表格可用宽度,导致列宽设置失效——这就是为什么资源目录里bg.png必须是1024×768,而非手机屏的1920×1080。
- 查询结果为空时的用户体验
tickets.size()==0,代码里没有JOptionPane.showMessageDialog(null, "无匹配车次"),而是让表格显示空行。这是刻意为之:教学生区分“业务逻辑空值”(应提示)和“UI渲染空状态”(应留白)。后续扩展时,可在此处加入EmptyTableModel,继承DefaultTableModel并重写getRowCount()返回1,显示“暂无数据”占位符。

3.2 购票模块:JDialog模态对话框的生命周期管理

BuyTicketDialog.java是Swing事件处理的精华浓缩。它不是一个简单弹窗,而是一个微型状态机:

public class BuyTicketDialog extends JDialog {
    private JTextField countField; // 购票张数
    private JButton confirmBtn;
    private boolean confirmed = false; // 状态标志

    public BuyTicketDialog(Frame owner, Ticket ticket) {
        super(owner, "购票确认", true); // true=模态,阻塞父窗口
        this.ticket = ticket;

        // 关键:窗口关闭时的清理
        setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                confirmed = false; // 明确置为false
                dispose(); // 必须调用,否则内存泄漏
            }
        });

        confirmBtn.addActionListener(e -> {
            try {
                int count = Integer.parseInt(countField.getText());
                if (count > ticket.getLeft()) {
                    JOptionPane.showMessageDialog(this, "余票不足!");
                    return;
                }
                confirmed = true; // 状态变更
                dispose(); // 关闭对话框
            } catch (NumberFormatException ex) {
                JOptionPane.showMessageDialog(this, "请输入有效数字");
            }
        });
    }

    public boolean isConfirmed() { return confirmed; } // 提供状态查询
    public int getBuyCount() { return Integer.parseInt(countField.getText()); }
}

实操中学生最常犯的错:
- 忘记setDefaultCloseOperation(DO_NOTHING_ON_CLOSE):导致点右上角X直接退出,confirmed状态未更新,主窗口误判为“已购票”;
- dispose()调用时机错误:在catch块里没调用,导致异常后对话框卡死;
- isConfirmed()返回前未校验countField:若用户清空输入框直接点确定,getBuyCount()NumberFormatException。我在代码审查时,会要求学生把getBuyCount()改成Optional<Integer>返回,强制处理空值——这是向函数式编程的温和过渡。

3.3 退票模块:事务性操作的文本文件模拟

退票是系统唯一涉及“数据一致性”的场景。TicketService.refundTicket()方法这样实现:

public boolean refundTicket(String trainNo, int count) {
    List<String> lines = readFileLines("dbFile.txt"); // 全量读取
    boolean updated = false;

    for (int i = 0; i < lines.size(); i++) {
        String[] fields = lines.get(i).split("\\|");
        if (fields[0].equals(trainNo)) {
            int left = Integer.parseInt(fields[6]);
            fields[6] = String.valueOf(left + count); // 余票增加
            lines.set(i, String.join("|", fields));
            updated = true;
            break;
        }
    }

    if (updated) {
        writeFileLines("dbFile.txt", lines); // 全量写回
        return true;
    }
    return false;
}

这看似粗糙,却精准复现了数据库事务的ACID特性:
- 原子性(Atomicity):要么全成功(文件重写),要么全失败(IOExceptionlines未修改);
- 一致性(Consistency):通过trainNo唯一索引保证不误改其他车次;
- 隔离性(Isolation):因无并发用户,天然隔离;
- 持久性(Durability)FileWriter写入磁盘即生效。

提示:若想升级为真正事务,只需将writeFileLines()替换为Files.write(Paths.get("dbFile.txt"), lines, StandardCharsets.UTF_8, StandardOpenOption.WRITE),并捕获IOException回滚——这行代码,就是从文件系统迈向数据库的第一步。

4. 实操过程与环境配置:Eclipse导入零踩坑指南

4.1 项目导入四步法:绕过90%的.classpath陷阱

很多学生卡在“导入后一堆红叉”,根源在.classpath文件的路径硬编码。正确流程如下:

第一步:解压后先删.settings目录
该目录含Eclipse专属配置(如JDK版本号1.8),若你用JDK 11,Eclipse会因org.eclipse.jdt.core.javabuilder插件不兼容报错。删除后,Eclipse会自动生成适配当前JDK的新配置。

第二步:导入时选择“Existing Projects into Workspace”
不要选“General Project”——后者不会识别Java构建路径。在向导中勾选“Copy projects into workspace”,避免后续移动文件夹导致路径断裂。

第三步:手动修复classes12.jar引用
.classpath里有<classpathentry kind="lib" path="lib/classes12.jar"/>,但解压包中classes12.jar实际在根目录。操作:
- 右键项目 → Properties → Java Build Path → Libraries → Add JARs…
- 导航到项目根目录,选中classes12.jar → OK
- 回到Libraries列表,选中刚添加的jar → Remove(删掉旧引用)

第四步:设置文本文件编码为UTF-8
dbFile.txt含中文站名,若Eclipse默认GBK,读取时北京南变乱码。操作:
- 右键dbFile.txt → Properties → Resource → Text file encoding → Other → UTF-8
- 同步修改src/cn/火车票售票管理系统1/model/TicketService.javaFileReader构造参数:new FileReader(file, StandardCharsets.UTF_8)

完成这四步,Main.java右键Run As → Java Application,主窗口即弹出。若仍报错,90%是bg.png路径问题:确保bg.pngsrc同级,且MainFrame.java中加载代码为new ImageIcon(MainFrame.class.getResource("/bg.png"))(斜杠开头表示从class路径根开始找)。

4.2 wenzhang.doc设计报告的隐藏价值:不只是交差文档

这份Word报告常被学生当“凑字数附件”,实则它是代码的“思维地图”。重点研读三处:
- 需求分析章节的用例图:图中Actor(用户)与Use Case(查票、购票)连线,标注了<<include>>关系(如“购票”包含“支付”)。这暗示BuyTicketDialog应拆出PaymentPanel子组件——虽当前未实现,但为后续扩展埋了接口;
- 类图中的依赖箭头MainFrame指向QueryActionQueryAction指向TicketService,箭头方向即控制流方向。学生可据此反向追踪:点击查询按钮 → QueryAction.actionPerformed()TicketService.queryByStation()FileReader读文件;
- 运行截图的窗口尺寸标注:截图下方注明“主窗口尺寸:800×600”。这解释了为何MainFrame构造方法中有setPreferredSize(new Dimension(800, 600))——UI设计必须考虑目标设备分辨率,而非盲目填满屏幕。

5. 常见问题与排查技巧实录:那些让我熬夜改了七版的Bug

5.1 经典问题速查表

问题现象 根本原因 一招解决
主窗口背景图bg.png不显示,只显示灰色 JLabel作为背景容器未设setOpaque(false),且父容器JPanel未重写paintComponent() MainFrame中:backgroundLabel.setOpaque(false); contentPane.add(backgroundLabel, BorderLayout.CENTER); 并重写contentPane.paintComponent(g)调用super.paintComponent(g)
查询后表格数据重复出现两遍 DefaultTableModel.addRow()前未清空模型,且queryTickets()方法被调用了两次(如ActionListener注册了两次) 检查MainFrame构造方法中queryBtn.addActionListener()是否重复执行;用System.out.println("Query triggered")打日志定位
输入购票张数为0或负数,程序崩溃 BuyTicketDialog.getBuyCount()未校验输入,Integer.parseInt("")NumberFormatException confirmBtn监听器中:if (countField.getText().trim().isEmpty()) { JOptionPane.showMessageDialog(...); return; }
退票后余票数不变 dbFile.txt文件被其他程序占用(如用记事本打开编辑),FileWriter写入失败但未抛异常 关闭所有编辑dbFile.txt的程序;在writeFileLines()中添加if (!file.canWrite()) throw new IOException("文件不可写");

5.2 独家避坑技巧:来自七届课设辅导的真实经验

技巧1:用JFormattedTextField替代JTextField防输入污染
countField若用普通JTextField,用户可输入abc导致NumberFormatException。升级为:

JFormattedTextField countField = new JFormattedTextField(NumberFormat.getIntegerInstance());
countField.setValue(1); // 默认值
countField.setColumns(5);

NumberFormat.getIntegerInstance()自动过滤非数字字符,输入a1b2只保留12,且getValue()直接返回Integer对象,省去parseInt()

技巧2:JTable双击行触发购票的隐藏逻辑
当前系统需先选中行再点“购票”按钮。学生常问:“能否双击直接购票?”答案是肯定的,且只需3行代码:

table.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getClickCount() == 2) { // 双击
            int row = table.getSelectedRow();
            if (row != -1) new BuyTicketDialog(MainFrame.this, tickets.get(row)).setVisible(true);
        }
    }
});

这行代码揭示了Swing事件的组合艺术:MouseListener监听鼠标,getClickCount()区分单双击,table.getSelectedRow()获取当前行——把多个基础组件能力编织成新交互。

技巧3:init_db.py的跨平台路径兼容
原脚本用open("dbFile.txt", "w", encoding="utf-8"),在Windows下正常,Linux/macOS可能因换行符报错。改为:

import os
with open("dbFile.txt", "w", newline=os.linesep, encoding="utf-8") as f:
    f.write("G101|北京南|上海虹桥|08:00|12:30|560|120\n")

newline=os.linesep确保换行符匹配系统(Windows\r\n,Linux\n),避免dbFile.txt在不同系统读取时最后一行解析失败。

6. 项目延伸与能力跃迁:从课设源码到真实工程思维

这个系统真正的价值,不在它能做什么,而在它为你搭建了通往真实开发的跳板。我带过的毕业生中,有三人凭此项目拿到了大厂实习offer,关键不是代码多炫,而是他们做了这些延伸:

延伸1:用JSON替代文本文件,迈出数据格式升级第一步
dbFile.txt改为db.json,内容变为:

[
  {"trainNo":"G101","start":"北京南","end":"上海虹桥","startTime":"08:00","endTime":"12:30","total":560,"left":120},
  ...
]

引入Jackson库(jackson-databind-2.15.2.jar),TicketService中:

ObjectMapper mapper = new ObjectMapper();
List<Ticket> tickets = mapper.readValue(new File("db.json"), new TypeReference<List<Ticket>>(){});

此举教会学生:数据格式演进是常态,封装解析逻辑(如JsonTicketService继承TicketService)比硬编码更重要

延伸2:添加日志系统,告别System.out.println
log4j2替换所有println

private static final Logger logger = LogManager.getLogger(BuyAction.class);
// 替换 System.out.println("购票成功") → logger.info("购票成功,车次:{},数量:{}", ticket.getTrainNo(), count);

生成log4j2.xml配置文件,指定日志输出到logs/app.log。这让学生第一次理解:生产环境需要可追溯、可分级、可归档的日志,而非控制台一闪而过的文字

延伸3:单元测试覆盖核心逻辑,建立质量意识
用JUnit 5测试TicketService.queryByStation()

@Test
void testQueryByStation() {
    // 准备测试数据:临时写入dbFile.txt
    writeTestDb("G101|北京南|上海虹桥|08:00|12:30|560|120\nG102|北京南|杭州东|09:00|13:15|420|0");

    List<Ticket> result = service.queryByStation("北京南", "上海虹桥");

    assertEquals(1, result.size());
    assertEquals("G101", result.get(0).getTrainNo());
}

这教会学生:可测试性是优秀代码的基因,而测试先行(TDD)能让需求理解更透彻——写测试时,你必须明确“查北京南到上海虹桥,应该返回几条?余票为0的G102该不该包含?”

最后分享一个小技巧:这个系统所有Swing组件都用setPreferredSize()硬编码尺寸,这是教学简化。但真实项目中,你应该用GroupLayoutMigLayout,它们能根据字体大小、系统DPI自动缩放。下次打开MainFrame.java,试着把setPreferredSize(new Dimension(800,600))删掉,然后在contentPane.setLayout(new BorderLayout())后加contentPane.setBorder(BorderFactory.createEmptyBorder(10,10,10,10))——你会发现,界面突然有了呼吸感,组件间距不再僵硬。这微小的改动,就是从“能跑”到“好用”的分水岭。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:用Java Swing做的纯桌面版火车票管理程序,不用装Tomcat也不用连真实数据库,所有数据存在dbFile.txt里,启动就能用。界面有车次查询框、余票实时显示列表、购票弹窗、退票确认流程,操作逻辑清晰。项目结构规整,src下按cn.火车票售票管理系统1分包,image文件夹放按钮图标,bg.png是主窗口背景图,还附带Oracle驱动classes12.jar(兼容老版本JDK),init_db.py可重置测试数据。配套的wenzhang.doc是完整课程设计报告,含需求分析、类图说明、MVC模块划分、关键事件监听代码解释和实际运行截图。Eclipse直接导入.project和.classpath就能编译运行,适合Java GUI初学者练手、交课设作业或理解Swing组件布局与ActionListener响应机制。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐