Java Swing桌面记账本开发实战:从数据模型到打包部署
2026/9/4 2:07:11 网站建设 项目流程

简介:这是一份面向Java初学者与桌面应用开发入门者的实战项目资源,基于J2SE技术栈实现轻量级个人记账管理功能,聚焦GUI编程、数据库集成与事件驱动开发等核心能力训练。资源包共173个文件,含46个可读性良好的Java源码(覆盖DAO层、UI面板、工具类等)、49个编译后class文件、42个运行依赖jar包(含SQLite-JDBC驱动)、16个界面图标png及2个SQLite数据库文件,整体29.45MB,结构清晰,便于逐模块理解Swing组件布局、数据持久化逻辑与MVC风格代码组织。已有743人学习下载,项目注释详实,包含RecordPanel、SpendPanel、ChartUtil、SQLUtil等典型模块,特别适合通过调试HistoryListListener、CategoryDAO等关键类,掌握用户交互响应、收支分类统计与图表辅助分析的完整链路,是贯通Java基础语法、Swing界面设计与SQLite嵌入式数据库操作的优质练手案例。

1. 项目概述与核心价值

最近在整理自己的技术项目库,翻到了一个几年前写的“小小记账本”。别看它名字简单,当时可是帮我解决了大问题。那会儿刚工作,总感觉钱花得稀里糊涂,市面上现成的记账App要么功能太臃肿,要么广告满天飞,要么就是数据安全让人不放心。作为一个Java开发者,我琢磨着,为什么不自己动手写一个呢?既能满足自己“记录每一笔收支”的核心需求,又能把Java SE那套东西(Swing GUI、集合框架、文件I/O)实实在在地练一遍,还能把数据完全掌握在自己手里。这个项目麻雀虽小,五脏俱全,从需求分析、界面设计、数据存储到异常处理,走完了一个完整桌面应用的生命周期。对于Java初学者或者想找个综合练手项目的朋友来说,它是个绝佳的“磨刀石”。今天,我就把这个项目的设计思路、实现细节,以及我踩过的那些坑,毫无保留地分享出来。

2. 项目整体设计与技术选型

2.1 需求分析与功能拆解

做任何项目,第一步都是想清楚要做什么。我对“小小记账本”的核心定位是:轻量、离线、快速记录。基于这个定位,我拆解出以下几个核心功能模块:

  1. 账目记录:这是核心中的核心。需要能记录每一笔收支的金额、类型(收入/支出)、分类(如餐饮、交通、工资等)、时间、备注。
  2. 账目管理:包括对已有记录的增、删、改、查。特别是“查”,需要支持按时间范围、按分类进行筛选。
  3. 数据统计:以直观的方式展示财务状况,比如每日/每月支出柱状图、收支分类饼图,让用户一眼看清钱花哪儿了。
  4. 数据持久化:所有记录必须能保存到本地,关闭程序再打开,数据不能丢。

2.2 技术栈选型与理由

确定了功能,接下来就是选择实现技术。作为一个纯粹的桌面练习项目,我选择了最经典、最纯粹的Java SE技术组合。

  • 核心语言:Java 8。这是当时(乃至现在很多环境)最稳定、应用最广泛的版本,其Lambda表达式和Stream API能让数据处理的代码更简洁。
  • 图形界面(GUI)Java Swing。为什么不选JavaFX?对于这个量级的项目,Swing完全够用,而且它是Java标准库的一部分,无需额外依赖,部署极其简单。更重要的是,Swing的MVC(模型-视图-控制器)思想非常清晰,TableModelListModel等组件与数据绑定方便,非常适合展示账目列表。
  • 数据存储序列化(Serialization) + 文件。为什么不选数据库(如SQLite)?考虑到记账数据量不会特别大(一个人几年的记录可能也就几万条),结构也非常固定,使用数据库有点“杀鸡用牛刀”,还会引入额外的驱动和复杂度。将List<Record>整个序列化到一个.dat文件,实现起来最简单,读写效率也足够。当然,这也有缺点,后面会讲到。
  • 图表绘制JFreeChart。这是一个非常成熟的Java开源图表库,虽然风格有点“复古”,但功能强大,绘制饼图、柱状图等绰绰有余,而且文档齐全。
  • 开发工具:IntelliJ IDEA。这个没啥好说的,写Java的利器。

注意:这个技术栈是典型的“教学/练手”型选择。它屏蔽了网络、复杂数据库、Web框架等干扰,让你能聚焦于Java核心语法、面向对象设计、GUI事件驱动编程和文件操作这些基本功。如果你想做一个更“产品化”的记账本,可以考虑换成JavaFX + SQLite + 更现代的图表库,但那又是另一个故事了。

3. 核心模块详细设计与实现

3.1 数据模型设计(Model)

这是项目的基石,设计得好,后面编码就顺畅。我设计了一个Record类来代表单笔账目,它包含所有必要的属性。

import java.io.Serializable; import java.util.Date; /** * 记账记录实体类 * 必须实现Serializable接口,才能被序列化到文件 */ public class Record implements Serializable { private static final long serialVersionUID = 1L; // 序列化版本ID,重要! private int id; // 唯一标识,用于查找和删除 private double amount; // 金额 private RecordType type; // 类型:收入/支出,使用枚举 private String category; // 分类:如“餐饮”、“购物” private Date date; // 日期时间 private String remark; // 备注 // 枚举:账目类型 public enum RecordType { INCOME("收入"), EXPENSE("支出"); private final String description; RecordType(String description) { this.description = description; } public String getDescription() { return description; } } // 构造方法、Getter/Setter、toString() 等方法省略... // 务必重写toString(),方便调试时查看对象内容。 }

关键点解析

  1. implements Serializable:这是Java对象序列化的标记接口。没有它,你的对象无法被写入文件。这是一个非常容易忘记但一忘就报错java.io.NotSerializableException的点。
  2. serialVersionUID:这个字段至关重要。它相当于这个类的“版本号”。如果你修改了Record类的结构(比如增加一个字段),但没有显式声明serialVersionUID,Java会自己计算一个。当你用新程序去读旧版本序列化的文件时,会因为版本号不匹配而反序列化失败。显式声明一个固定的serialVersionUID可以避免这种兼容性问题
  3. 使用枚举RecordType:比直接用字符串“收入”、“支出”好得多。它类型安全,在代码中可以用RecordType.INCOME来引用,避免了拼写错误。

3.2 数据访问层设计(DAO)

数据访问对象(DAO)模式将数据操作逻辑封装起来,让业务逻辑层(如界面控制)不用关心数据具体怎么存、怎么取。

import java.util.List; /** * 记账记录的数据访问接口 */ public interface RecordDao { /** 添加一条记录 */ boolean addRecord(Record record); /** 根据ID删除记录 */ boolean deleteRecord(int id); /** 更新记录 */ boolean updateRecord(Record record); /** 获取所有记录 */ List<Record> getAllRecords(); /** 根据条件查询记录(如时间范围、分类) */ List<Record> queryRecords(Date start, Date end, String category); /** 统计总收入 */ double getTotalIncome(); /** 统计总支出 */ double getTotalExpense(); }

然后,我们实现一个基于文件序列化的具体DAO类RecordDaoImpl。它的核心就是两个方法:loadFromFile()saveToFile()

import java.io.*; import java.util.ArrayList; import java.util.List; public class RecordDaoImpl implements RecordDao { private List<Record> recordList = new ArrayList<>(); private static final String DATA_FILE = "account_book.dat"; // 构造方法中加载数据 public RecordDaoImpl() { loadFromFile(); } @SuppressWarnings("unchecked") private void loadFromFile() { File file = new File(DATA_FILE); if (!file.exists()) { return; // 第一次运行,文件不存在,直接返回空列表 } try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { recordList = (ArrayList<Record>) ois.readObject(); // 反序列化 } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); // 处理文件损坏或格式错误,可以初始化一个空列表,并给用户提示 recordList = new ArrayList<>(); } } private void saveToFile() { try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(DATA_FILE))) { oos.writeObject(recordList); // 序列化整个列表 } catch (IOException e) { e.printStackTrace(); // 保存失败,需要给用户明确的错误提示 } } // 实现接口方法,每个修改数据的方法最后都要调用saveToFile() @Override public boolean addRecord(Record record) { // 生成ID(简单处理:当前最大ID+1) int newId = recordList.stream().mapToInt(Record::getId).max().orElse(0) + 1; record.setId(newId); boolean success = recordList.add(record); if (success) { saveToFile(); } return success; } // deleteRecord, updateRecord 等方法类似,在操作成功后调用saveToFile() }

实操心得与避坑指南

  1. ObjectInputStreamObjectOutputStream:必须成对使用,且写入和读取的顺序、类型必须严格一致。
  2. 异常处理loadFromFile()中的异常处理非常关键。ClassNotFoundException可能意味着你的Record类版本变了(没管理好serialVersionUID);IOException可能是文件损坏。绝不能简单地吞掉异常,至少要在控制台打印,在正式产品中应该弹窗告知用户“数据文件可能已损坏,已初始化新账本”。
  3. 性能考量:每次增删改都序列化整个列表到文件。在数据量小(几千条)时没问题。如果记录达到十万级,这种方式会变慢,并且每次保存都是全量写入,不够高效。这时就该考虑换用数据库(如SQLite,它也是单个文件,但支持增量更新和复杂查询)。
  4. ID生成:这里用了简单的“最大ID+1”。在单机桌面程序里够用。注意并发问题?不,Swing的事件分发线程(EDT)是单线程处理UI事件的,所以这里没有并发冲突。

3.3 图形用户界面构建(View)

界面是用户直接交互的部分。我使用Swing构建了一个主窗口(JFrame),里面主要包含以下几个部分:

  • 顶部:输入面板(JPanel),包含金额文本框(JTextField)、类型单选按钮(JRadioButton)、分类下拉框(JComboBox)、日期选择器(JSpinner绑定SpinnerDateModel)、备注文本框和“添加”按钮。
  • 中部:账目列表展示区,使用JTable。这是Swing的精华之一。
  • 底部:统计信息面板,显示总收入、总支出、结余,以及几个查询按钮(“今日”、“本月”、“按分类筛选”)和一个图表展示按钮。

让JTable显示数据的关键:自定义TableModel直接给JTable设置一个List<Record>是没用的。需要创建一个实现了TableModel接口的类,告诉表格有多少行、多少列,每列叫什么名字,每个单元格的数据是什么。

import javax.swing.table.AbstractTableModel; import java.util.List; public class RecordTableModel extends AbstractTableModel { private final List<Record> recordList; private final String[] columnNames = {"ID", "日期", "类型", "分类", "金额", "备注"}; public RecordTableModel(List<Record> recordList) { this.recordList = recordList; } @Override public int getRowCount() { return recordList.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Record record = recordList.get(rowIndex); switch (columnIndex) { case 0: return record.getId(); case 1: return record.getDate(); // 这里返回Date对象,JTable会默认调用其toString() case 2: return record.getType().getDescription(); // 显示枚举的中文描述 case 3: return record.getCategory(); case 4: return record.getAmount(); case 5: return record.getRemark(); default: return null; } } // 当底层数据变化时,通知表格更新视图 public void setRecordList(List<Record> newList) { this.recordList.clear(); this.recordList.addAll(newList); fireTableDataChanged(); // 关键!触发UI刷新 } }

在主界面中,这样绑定:

RecordDao dao = new RecordDaoImpl(); RecordTableModel tableModel = new RecordTableModel(dao.getAllRecords()); JTable recordTable = new JTable(tableModel); // 将JTable放入JScrollPane,再添加到主窗口

界面布局技巧: Swing布局管理器是个让人又爱又恨的东西。我个人的经验是:

  • 对于简单的行或列排列,用BoxLayoutFlowLayout最省心。
  • 对于稍微复杂点的,比如输入面板这种有标签、有输入框、需要对齐的,用GridBagLayout虽然学习曲线陡,但控制力最强。不过在这个项目里,我用的是GroupLayout(IntelliJ IDEA GUI Designer默认的),配合手写代码,也能达到不错的效果。
  • 一个黄金法则:所有可能动态添加组件或内容长度会变的区域(比如JTable,JTextArea),一定要把它们放在JScrollPane里!否则内容一多,界面就“撑破”或者显示不全。

3.4 控制器与事件处理(Controller)

在Swing中,控制器逻辑分散在各个事件监听器(ActionListener,ListSelectionListener等)中。我们需要把这些监听器组织好,让它们调用DAO的方法操作数据,并更新界面(TableModel)。

“添加”按钮的事件处理

addButton.addActionListener(e -> { // 1. 从输入控件获取数据 double amount; try { amount = Double.parseDouble(amountField.getText().trim()); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(mainFrame, "请输入有效的金额!", "错误", JOptionPane.ERROR_MESSAGE); return; } Record.RecordType type = incomeRadio.isSelected() ? Record.RecordType.INCOME : Record.RecordType.EXPENSE; String category = (String) categoryComboBox.getSelectedItem(); Date date = (Date) dateSpinner.getValue(); String remark = remarkField.getText(); // 2. 创建Record对象 Record newRecord = new Record(); newRecord.setAmount(amount); newRecord.setType(type); newRecord.setCategory(category); newRecord.setDate(date); newRecord.setRemark(remark); // 3. 通过DAO保存 boolean success = dao.addRecord(newRecord); if (success) { // 4. 更新表格模型(重新从DAO加载数据或直接向tableModel添加一行) tableModel.setRecordList(dao.getAllRecords()); // 简单粗暴,重新加载 // 5. 清空输入框 clearInputFields(); // 6. 更新底部统计信息 updateStatistics(); JOptionPane.showMessageDialog(mainFrame, "添加成功!"); } else { JOptionPane.showMessageDialog(mainFrame, "添加失败,请重试。", "错误", JOptionPane.ERROR_MESSAGE); } });

“删除”按钮与表格行选择: 我们需要监听表格的选择事件,当用户选中某一行时,获取该行对应的Record的ID,然后点击删除按钮时进行删除。

// 监听表格行选择 recordTable.getSelectionModel().addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { // 防止事件触发两次 int selectedRow = recordTable.getSelectedRow(); deleteButton.setEnabled(selectedRow != -1); // 有选中行才启用删除按钮 if (selectedRow != -1) { // 获取选中行的ID(假设ID在第一列) int selectedId = (int) tableModel.getValueAt(selectedRow, 0); currentSelectedRecordId = selectedId; // 暂存当前选中的ID } } }); // 删除按钮事件 deleteButton.addActionListener(e -> { if (currentSelectedRecordId != -1) { int confirm = JOptionPane.showConfirmDialog(mainFrame, "确定要删除这条记录吗?", "确认删除", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { boolean success = dao.deleteRecord(currentSelectedRecordId); if (success) { tableModel.setRecordList(dao.getAllRecords()); updateStatistics(); deleteButton.setEnabled(false); currentSelectedRecordId = -1; } } } });

3.5 数据统计与图表展示

统计功能主要在DAO的getTotalIncome()getTotalExpense()中实现,通过遍历列表,用Java 8的Stream API可以写得非常优雅:

@Override public double getTotalIncome() { return recordList.stream() .filter(r -> r.getType() == RecordType.INCOME) .mapToDouble(Record::getAmount) .sum(); }

底部的统计面板就是几个JLabel,在数据变化时(添加、删除后)调用updateStatistics()方法更新文本。

图表展示是另一个亮点。我使用JFreeChart来生成月度支出柱状图和分类支出饼图。

生成月度支出柱状图的示例

private JFreeChart createMonthlyExpenseChart(List<Record> records) { // 1. 创建数据集 DefaultCategoryDataset dataset = new DefaultCategoryDataset(); // 假设我们按月份统计 Map<String, Double> monthlyExpense = records.stream() .filter(r -> r.getType() == RecordType.EXPENSE) .collect(Collectors.groupingBy( r -> new SimpleDateFormat("yyyy-MM").format(r.getDate()), // 按“年-月”分组 Collectors.summingDouble(Record::getAmount) )); // 填充数据集 monthlyExpense.forEach((month, total) -> dataset.addValue(total, "支出", month)); // 2. 创建图表 JFreeChart chart = ChartFactory.createBarChart( "月度支出统计", // 标题 "月份", // 横轴标签 "金额 (元)", // 纵轴标签 dataset, // 数据集 PlotOrientation.VERTICAL, true, // 显示图例 true, // 生成工具提示 false // 不生成URL链接 ); // 3. 可以进一步美化图表(设置颜色、字体等) CategoryPlot plot = chart.getCategoryPlot(); plot.setBackgroundPaint(Color.WHITE); // ... 更多样式设置 return chart; }

然后,将JFreeChart对象放入ChartPanel,这个ChartPanel就是一个Swing组件,可以像普通JPanel一样添加到你的窗口或对话框中。

踩坑记录:JFreeChart的依赖管理。你需要手动下载jfreechart-xxx.jar和它的依赖jcommon-xxx.jar,然后添加到项目的类路径(Classpath)中。如果用Maven管理就简单多了,在pom.xml里添加依赖即可。这是很多Java桌面项目从“玩具”走向“工程化”的第一步——学会使用构建工具管理依赖。

4. 项目打包与部署优化

代码写完了,怎么把它变成一个可以双击运行的.exe.jar文件呢?

4.1 生成可执行JAR包

这是最基本的方式。你需要做两件事:

  1. 在IDE(如IntelliJ IDEA)中,配置Artifact,指定主类(包含public static void main(String[] args)方法的类)。
  2. 将所有依赖(包括JFreeChart的jar包)打包进去。IDEA的“Build Artifacts”功能可以帮你生成一个“胖JAR”(Fat JAR)或“带依赖的JAR”。

命令行运行java -jar your_account_book.jar

潜在问题:如果用户电脑没有安装Java运行环境(JRE),程序是无法运行的。你需要告诉用户:“运行此程序需要先安装Java”

4.2 使用Launch4j或JPackage制作原生安装包

为了让用户体验更好,我们可以把JRE和我们的程序打包在一起,做成一个真正的Windows可执行文件(.exe)或Mac/Linux应用。

  • Launch4j:一个免费工具,可以将JAR包装成Windows的exe文件,并可以捆绑一个JRE。这样用户拿到手就是一个exe,双击就能运行,无需关心Java环境。
  • JPackage (JDK 14+):这是Oracle官方提供的工具,功能更强大,可以生成不同平台(Windows, macOS, Linux)的原生安装包(如MSI, DMG, DEB)。这是目前最推荐的方式,因为它能生成更专业、符合各平台规范的安装程序。

使用JPackage的大致步骤(命令行)

# 假设你的主类为 com.yourapp.Main,且所有依赖jar都在libs文件夹 # 首先,用jlink创建一个自定义的、只包含程序所需模块的JRE(减小体积) jlink --add-modules java.base,java.desktop,java.sql --output myjre # 然后,用jpackage打包 jpackage --name MyAccountBook \ --input ./dist \ # 包含你的主JAR和依赖JAR的目录 --main-jar account_book.jar \ --main-class com.yourapp.Main \ --runtime-image ./myjre \ --dest ./release \ --type msi \ # Windows下生成msi安装包 --win-dir-chooser \ --win-menu \ --win-shortcut

这个过程稍微复杂,但一劳永逸。生成的MSI安装包,用户可以像安装任何其他软件一样安装你的记账本。

5. 功能扩展思路与高级优化

一个基础版本完成后,你可以根据自己的兴趣和需求,为这个“小小记账本”添加更多功能,让它变得更强大。

5.1 数据存储升级:从序列化到SQLite

当记录超过几千条,或者你需要更复杂的查询(如“查询2023年餐饮支出超过100元的所有记录”)时,文件序列化的弊端就显现了。升级到SQLite是顺理成章的一步。

改动点

  1. 引入SQLite JDBC驱动:下载sqlite-jdbc-xxx.jar并添加到项目。
  2. 修改DAO实现RecordDaoImpl中的recordList可以保留作为缓存,但所有增删改查操作都改为执行SQL语句。
  3. 数据库初始化:在程序第一次运行时,创建数据库表和初始数据。
// 在DAO构造方法中初始化数据库连接和表 public RecordDaoImpl() { String url = "jdbc:sqlite:account_book.db"; try (Connection conn = DriverManager.getConnection(url); Statement stmt = conn.createStatement()) { // 创建表 String sql = "CREATE TABLE IF NOT EXISTS records (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "amount REAL NOT NULL, " + "type TEXT NOT NULL, " + "category TEXT, " + "date INTEGER, " + // 存储时间戳 "remark TEXT)"; stmt.execute(sql); } catch (SQLException e) { e.printStackTrace(); } loadFromDatabase(); // 从数据库加载数据到内存列表 }
  1. 使用PreparedStatement防止SQL注入:这是必须养成的安全习惯。

5.2 数据导入导出

增加实用性功能。

  • 导出为Excel:可以使用Apache POI库,将List<Record>写入.xlsx文件,方便用户用Excel进行二次分析或存档。
  • 导入数据:支持从自己导出的Excel,或从其他记账App导出的CSV文件导入。这需要编写解析文件并转换为Record对象的逻辑。

5.3 数据备份与云同步(进阶)

这是让应用更“现代”的功能,但复杂度也急剧上升。

  • 本地备份:定期(如每次关闭程序时)将数据库文件复制到另一个目录,并保留几个历史版本。
  • 云同步:这需要引入网络编程和用户系统。你可以选择:
    • WebDAV:让用户配置一个WebDAV服务器(如坚果云)地址,程序定时将数据库文件上传/下载。实现相对简单,依赖用户自己准备存储。
    • 自建后端:用Spring Boot写一个简单的后端服务,提供用户注册登录和账目数据的RESTful API。桌面端通过HTTP Client与后端交互。这就从一个桌面应用变成了“桌面客户端+云服务”的架构,工程量大了很多,但可玩性也更高。

5.4 界面美化与用户体验

Swing默认的金属(Metal)主题确实有点过时。可以尝试:

  • 更换观感(Look and Feel):使用UIManager.setLookAndFeel()切换到系统默认观感(如Windows的com.sun.java.swing.plaf.windows.WindowsLookAndFeel)或其他第三方观感,如FlatLaf,能让你的程序瞬间现代化。
  • 使用图标:为按钮添加清晰的图标,提升直观性。
  • 快捷键支持:为常用操作(如添加Ctrl+N、删除Delete键)绑定快捷键。
  • 数据验证与提示:在输入金额时,实时验证是否为数字;日期选择提供便捷的“今天”按钮等。

6. 开发中遇到的典型问题与解决方案

在开发这个项目的过程中,我遇到了不少典型问题,这里列出来供大家参考避坑。

6.1 Swing GUI线程阻塞(界面卡死)

问题描述:当执行一个耗时的操作,比如从一个大文件中加载几千条记录,或者生成一个复杂的图表时,整个界面会“卡住”,按钮点不了,窗口也拖不动。

原因分析:Swing的界面更新和事件处理都在一个叫事件分发线程(Event Dispatch Thread, EDT)的单独线程中进行。如果你在EDT中执行耗时操作,就会阻塞这个线程,导致界面无法响应。

解决方案:使用SwingWorker

// 例如,在加载数据时 SwingWorker<List<Record>, Void> worker = new SwingWorker<>() { @Override protected List<Record> doInBackground() throws Exception { // 在后台线程执行耗时操作 return dao.getAllRecords(); // 假设这个操作很慢 } @Override protected void done() { try { List<Record> records = get(); // 获取后台任务结果 tableModel.setRecordList(records); // 在EDT中更新UI } catch (Exception e) { e.printStackTrace(); } } }; worker.execute(); // 启动后台任务

对于不确定是否耗时的操作,养成使用SwingWorker的习惯是很好的。

6.2 表格数据更新后界面不刷新

问题描述:调用dao.addRecord()后,数据确实保存了,但表格显示的还是老数据。

原因分析:你只更新了DAO里的数据列表,但没有通知JTable的模型(TableModel)数据已经变了。

解决方案:在修改了RecordTableModel底层的数据列表(recordList)后,必须调用fireTableDataChanged()fireTableRowsInserted()等通知方法。正如我在RecordTableModel.setRecordList()方法里做的那样。这是Swing MVC模式中,模型(Model)通知视图(View)更新的标准方式。

6.3 数字和日期的格式化显示

问题描述:金额12.5在表格里显示为12.5,但你可能想显示为¥12.50。日期Date对象直接显示为Tue Mar 19 14:30:00 CST 2024这种不友好的格式。

解决方案:自定义TableCellRenderer

// 为金额列设置渲染器 TableColumn amountColumn = recordTable.getColumnModel().getColumn(4); // 假设金额在第5列 amountColumn.setCellRenderer(new DefaultTableCellRenderer() { private final DecimalFormat df = new DecimalFormat("¥#,##0.00"); @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 (value instanceof Double) { setText(df.format((Double) value)); setHorizontalAlignment(SwingConstants.RIGHT); // 右对齐 } return c; } }); // 为日期列设置渲染器 TableColumn dateColumn = recordTable.getColumnModel().getColumn(1); dateColumn.setCellRenderer(new DefaultTableCellRenderer() { private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); @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 (value instanceof Date) { setText(sdf.format((Date) value)); } return c; } });

6.4 文件路径与可移植性

问题描述:代码中写死了数据文件路径"account_book.dat",这在开发时没问题。但如果用户把程序放在不同目录运行,或者程序被打包成JAR后,这个相对路径可能会指向意想不到的地方。

解决方案:使用用户主目录或程序运行目录。

// 方案一:放在用户主目录下的子文件夹中,更通用 String userHome = System.getProperty("user.home"); File appDataDir = new File(userHome, ".myaccountbook"); // 以点开头,在Unix-like系统是隐藏文件夹 if (!appDataDir.exists()) { appDataDir.mkdirs(); } File dataFile = new File(appDataDir, "data.dat"); // 方案二:放在JAR包同级目录(对于便携式应用) // 这需要获取程序启动的路径,稍微复杂一点,可以通过 Main.class.getProtectionDomain().getCodeSource().getLocation() 来获取JAR所在目录。

我推荐方案一,因为它符合大多数桌面应用存储配置和数据文件的习惯(如C:\Users\用户名\AppData\Roaming\MyApp/home/用户名/.myapp)。

这个“小小记账本”项目,虽然功能简单,但它像一面镜子,映照出一个Java桌面应用从构思到实现,再到优化和打包的完整过程。过程中你对Java基础、面向对象、Swing、文件I/O、异常处理乃至简单的软件工程思想都会有更深的体会。它最大的价值不在于功能本身,而在于你动手实现它时,所经历的思考、调试和解决问题的全过程。当你看到自己写的程序能稳定运行,真正帮自己管理账目时,那种成就感是看十遍教程都换不来的。如果让我再优化一点,我可能会优先把数据存储换成SQLite,然后用FlatLaf把界面弄得漂亮点,这两点对用户体验的提升是立竿见影的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询