当前位置: 首页 > news >正文

Java Swing开发的轻量记账桌面程序,本地文件存数据,带登录验证和收支图表

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

简介:直接运行就能用的Java桌面记账工具,纯Swing编写,不装数据库,所有数据——包括用户名密码(pwd.txt)、收支明细(MyData.txt)——都存在本地文本文件里。启动先登录,验证通过后进入主界面,支持新增、修改、删除每一笔收入或支出,还能按日期范围、收支类型快速筛选查询。内置柱状图报表功能,基于jfreechart绘制,直观展示月度/类别收支对比。界面做了视觉优化,配有多个背景图(如BalEdit.jpg、bar.jpg、about.jpg等)和功能图标(alarm.jpg、clock.jpg、logoff.png),操作更清晰友好。项目提供完整可编译源码、清晰运行截图(1.png至6.png、Main_frame.png等)、详细README说明文档,实测只需运行src下MoneyManager类的main方法即可启动。配套jar包已全部集成:swingx-core、jfreechart、jcommon、gnujaxp,开箱即用,零环境配置压力,特别适合Java GUI入门练习、课程设计或小型个人财务记录需求。

1. 项目概述:一个“能跑、能看、能改”的Java桌面记账工具

你有没有过这种体验:想做个简单的个人记账小工具,但一打开IDEA就开始纠结——要不要搭Spring Boot?要不要配MySQL?要不要搞个Tomcat?结果折腾半天,连第一笔“早餐花了8块钱”都没记进去。这个Java Swing记账程序,就是专门来终结这种“过度设计焦虑”的。它不碰网络、不连数据库、不依赖任何服务端组件,整个程序就一个JAR包,双击就能运行;所有数据——从你的登录密码到上个月的房租支出——全存在本地两个文本文件里(pwd.txtMyData.txt),打开记事本就能查、就能改、就能备份。这不是一个“教学Demo”,而是一个真实可交付的轻量级桌面工具:启动先弹登录框,输对密码才进主界面;主界面左侧是功能按钮区(新增/编辑/删除/查询/图表),中间是带滚动条的收支明细表格,右侧是动态更新的柱状图;点击“按月统计”,图表立刻变成当月收入 vs 支出的对比柱子;点“按类别”,又自动切到餐饮、交通、娱乐等分类占比。它用的是最基础的Swing组件(JFrame、JTable、JButton),但通过JFreeChart补上了GUI应用最关键的“可视化短板”,再配上十几张精心挑选的背景图和图标(比如BalEdit.jpg用作余额编辑面板背景,bar.jpg作为图表区域底图,alarm.jpg提示重要提醒),让整个界面脱离了传统Swing那种“灰扑扑的Java味”,有了点生活化软件的亲和力。如果你是Java初学者,它是一份能直接编译、调试、修改的GUI实战样本;如果你在赶课程设计或毕设,它提供了完整的结构骨架(登录验证模块、文件IO持久层、MVC风格界面逻辑分离)、可替换的视觉资源、以及开箱即用的图表集成方案;如果你只是想找个干净、无广告、不联网、数据完全自己掌控的小工具来管管零花钱,它同样胜任——毕竟,真正的轻量,不是代码行数少,而是你不需要为它额外付出任何学习成本和运维成本。

2. 整体架构与设计思路拆解:为什么选择“纯文件+Swing”这条路径?

2.1 核心决策:放弃数据库,拥抱文本文件的底层逻辑

很多人看到“记账软件”第一反应就是数据库。但在这个项目里,我们主动放弃了SQLite、HSQLDB甚至内存数据库,坚持用纯文本文件存储全部数据,这背后有三重非常实际的考量,而不是为了标新立异。

第一层是部署极简性。数据库意味着驱动jar包、连接字符串配置、初始化脚本、可能的权限问题。而pwd.txtMyData.txt呢?它们就是操作系统里最普通的文件。程序启动时,FileReader读取,BufferedReader逐行解析;保存时,FileWriter覆盖写入。没有连接池、没有事务回滚、没有SQL语法错误——只有最原始的字符流操作。我实测过,在一台刚装好JRE的Windows 7老笔记本上,双击MoneyManager.jar,3秒内完成登录界面渲染,输入密码后1秒内加载完全部历史记录并绘出图表。这种“零配置启动”能力,是任何嵌入式数据库都难以企及的。

第二层是数据主权与可维护性。数据库里的数据是二进制blob,你得用专用工具才能查看。而这里的MyData.txt,格式是严格定义的CSV变种:每行一条记录,字段用英文逗号分隔,顺序固定为日期,类型,类别,金额,备注,例如2024-03-15,支出,餐饮,28.5,公司楼下沙县小吃。这意味着什么?意味着你明天想用Excel分析,直接拖进去就行;后天想写个Python脚本导出成PDF报表,三行pandas代码搞定;甚至哪天程序崩溃了,你打开记事本删掉最后一行乱码,数据就恢复了。我在课程设计答辩时,老师当场要求“把上季度所有交通类支出导出成Excel”,我直接打开MyData.txt,Ctrl+F搜“交通”,复制粘贴进Excel,全程10秒——这种“所见即所得”的数据掌控感,是数据库抽象层永远给不了的。

第三层是学习穿透性。对于Java GUI入门者,数据库会瞬间引入太多陌生概念:JDBC驱动加载、Connection对象生命周期、PreparedStatement防SQL注入、ResultSet遍历……这些知识固然重要,但它们会严重稀释对GUI核心逻辑(事件监听、线程安全更新UI、布局管理器协作)的关注。而文本文件IO,是每个学过《Java编程思想》第19章的人都能立刻上手的。pwd.txt的存储更简单:只有一行,格式为用户名:MD5加密后的密码,比如admin:e10adc3949ba59abbe56e057f20f883e。验证逻辑就是读取这一行,用MessageDigest对用户输入密码做MD5,比对字符串。没有框架、没有ORM,只有最朴素的String.equals()。这种设计,让初学者能把全部精力聚焦在“如何让按钮点击后表格刷新”、“如何让图表随筛选条件实时重绘”这些GUI本质问题上,而不是卡在“Class.forName()找不到驱动”这种环境配置陷阱里。

2.2 GUI框架选型:Swing不是过时,而是精准匹配

现在提到Java桌面开发,很多人会说“Swing早淘汰了,该用JavaFX”。但在这个项目里,Swing恰恰是最优解。原因很实在:成熟度、确定性和生态兼容性

JavaFX确实更现代,动画效果华丽,CSS样式灵活。但它有个致命短板:打包发布。JavaFX 11之后,官方不再捆绑JRE,你必须手动集成javafx-controlsjavafx-fxml等十几个模块,还要处理不同平台(Windows/macOS/Linux)的本地库(.dll/.so/.dylib)。而这个项目的目标用户,很多是第一次打包JAR的学生。他们需要的是“右键→发送到→桌面快捷方式→双击运行”。Swing呢?它从JDK 1.2就在,是JRE的绝对内置组件。你用JDK 8编译的Swing程序,在JDK 17上照样完美运行,字体渲染、高DPI适配、系统托盘集成,全都开箱即用。我测试过,同一套代码,在Windows 10、macOS Monterey、Ubuntu 22.04上,除了菜单栏位置略有差异(这是系统规范),其余所有按钮、表格、图表显示完全一致。

更重要的是,Swing的事件模型和线程模型极其清晰SwingUtilities.invokeLater()强制UI更新必须在EDT(Event Dispatch Thread)中执行,这虽然初期有点绕,但一旦理解,就能彻底避免“java.lang.IllegalStateException: Attempt to mutate in notification”这类多线程UI冲突。而JavaFX的Platform.runLater()语义类似,但其内部状态机更复杂,初学者更容易写出“绑定失效”或“Property未正确监听”的bug。这个项目里,所有数据变更(新增一笔支出、删除一条记录)后,都必须调用tableModel.fireTableDataChanged()通知JTable刷新,这个过程在Swing里是显式、可控、可调试的;而在JavaFX的ObservableList里,稍不注意就会漏掉addListener(),导致界面“看起来没变化”。

至于视觉优化,Swing也远非“丑陋”的代名词。项目中使用的swingx-core库,就是Swing的强力增强包。它提供了JXDatePicker(带日历下拉的日期选择器,比原生JTextField+正则校验靠谱十倍)、JXTaskPane(可折叠的功能面板,让主界面左侧按钮区层次分明)、JXStatusBar(底部状态栏,实时显示当前筛选条件和记录总数)。这些组件无缝集成在Swing体系内,无需任何额外的构建步骤,maven依赖一行搞定。它们的存在,让这个Swing程序的界面质感,已经超越了市面上90%的国产小工具软件。

2.3 图表引擎选型:JFreeChart——稳定压倒一切

图表功能是这个记账工具的“画龙点睛”之笔,而选择JFreeChart,是经过反复权衡的务实决定。有人会问:“现在不是有Charts.js、ECharts吗?Web版多酷啊。”但请记住,这是一个纯桌面应用。引入WebView组件(如JavaFX WebView)来渲染JS图表,会瞬间将JAR包体积从5MB膨胀到50MB以上,且首次加载慢、跨平台兼容性差(macOS上WebKit版本碎片化严重)。而JFreeChart,是Java世界里唯一一个历经20年考验、文档齐全、社区活跃、API稳定的纯Java图表库。

它的优势在于极致的可控性。生成一张月度收支对比柱状图,核心代码只有十几行:

// 创建CategoryDataset数据集 DefaultCategoryDataset dataset = new DefaultCategoryDataset(); dataset.addValue(1200.0, "收入", "2024-03"); dataset.addValue(2800.0, "支出", "2024-03"); // 创建JFreeChart对象 JFreeChart chart = ChartFactory.createBarChart( "月度收支对比", // 图表标题 "月份", // X轴标签 "金额(元)", // Y轴标签 dataset, // 数据集 PlotOrientation.VERTICAL, true, // 显示图例 true, // 显示工具提示 false // 不生成URL链接 ); // 将图表嵌入JPanel ChartPanel chartPanel = new ChartPanel(chart); chartPanel.setPreferredSize(new Dimension(500, 300)); mainPanel.add(chartPanel, BorderLayout.EAST);

这段代码的每一行,你都能在官方文档里找到对应解释,没有任何魔法。当图表需要定制时——比如把Y轴数字格式化为“¥1,200.00”,或者把柱子颜色按“收入绿色、支出红色”区分——只需调用NumberAxis.setNumberFormatOverride()BarRenderer.setSeriesPaint(),API命名直白,参数类型明确。相比之下,一些新兴的Java图表库,要么文档残缺,要么API频繁变动,要么社区支持寥寥。在课程设计这种时间紧、任务重的场景下,“能稳定跑通、能快速查文档、能精准改样式”,比“技术栈最新潮”重要一万倍。

3. 核心细节解析与实操要点:从密码验证到图表渲染的完整链路

3.1 登录验证模块:安全与简洁的平衡术

登录模块看似简单,却是整个程序的第一道防线,也是最容易被初学者写错的地方。这个项目采用“明文密码哈希存储+内存比对”的方案,既保证了基本安全性,又规避了复杂的加盐、迭代等高级密码学操作。

pwd.txt文件的格式被严格限定为单行:用户名:哈希值。例如admin:5f4dcc3b5aa765d61d8327deb882cf99。这里使用的是MD5算法,虽然MD5在密码学上已被认为不够安全(易受彩虹表攻击),但对于一个本地单机记账工具,其威胁模型完全不同:攻击者必须先物理接触到你的电脑,再找到这个文本文件,然后进行离线破解。在这种前提下,MD5提供的“防君子不防小人”级别的保护,已经足够。更重要的是,它的实现极度简单:

public static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] messageDigest = md.digest(input.getBytes()); BigInteger no = new BigInteger(1, messageDigest); return no.toString(16); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }

这段代码没有外部依赖,没有异常处理黑洞,初学者一眼就能看懂原理。登录验证的核心逻辑在LoginDialog类的login()方法里:
1. 读取pwd.txt文件,用Files.readAllLines(Paths.get("pwd.txt"))获取所有行;
2. 对每一行,用line.split(":")分割,得到用户名和哈希值;
3. 对用户在界面上输入的密码,调用上述md5()方法计算哈希;
4. 使用hashFromTxt.equals(md5Input)进行恒定时间比对(虽然MD5本身不推荐,但字符串比对用equals()已足够,无需MessageDigest.isEqual()这种过度设计)。

提示:这里有一个关键细节——pwd.txt文件必须放在程序运行目录下,而不是src/main/resources里。因为Files.readAllLines()读取的是运行时的相对路径。很多学生打包后发现登录失败,就是因为把pwd.txt放错了位置。正确的做法是:在IDEA里,把pwd.txt放在项目根目录(和src同级);打包成JAR后,确保pwd.txtMoneyManager.jar在同一文件夹内。你可以用System.getProperty("user.dir")打印当前工作目录来调试。

3.2 数据持久层:MyData.txt的格式契约与容错解析

MyData.txt是整个记账程序的数据心脏,它的格式设计体现了“人类可读”与“机器可解析”的精妙平衡。格式定义如下:

2024-03-15,支出,餐饮,28.5,公司楼下沙县小吃 2024-03-16,收入,工资,8500.0,税后实发 2024-03-18,支出,交通,4.0,地铁充值

关键约束有四条:
-日期必须为yyyy-MM-dd格式:这是为了后续按日期范围筛选时,能直接用字符串比较(dateStr.compareTo("2024-03-01") >= 0),无需解析为LocalDate对象,极大简化代码。
-类型只能是“收入”或“支出”:在添加记录的对话框里,用JComboBox硬编码这两个选项,杜绝非法输入。
-金额必须是合法数字字符串:在保存前,用Double.parseDouble()尝试转换,捕获NumberFormatException并弹出友好提示“金额请输入有效数字”。
-字段间用英文逗号分隔,字段内不允许出现逗号:这是CSV解析的底线。项目中所有用户输入的“备注”内容,在写入文件前,都会用String.replace(",", ",")(中文逗号)替换掉英文逗号,确保不会破坏行结构。

解析MyData.txt的代码,是整个项目里最需要体现“防御性编程”的部分:

List<Record> records = new ArrayList<>(); for (String line : Files.readAllLines(Paths.get("MyData.txt"))) { if (line.trim().isEmpty()) continue; // 跳过空行 String[] parts = line.split(",", -1); // -1参数保留末尾空字段 if (parts.length < 5) { System.err.println("警告:跳过格式错误行 '" + line + "',字段数不足5个"); continue; } try { String date = parts[0].trim(); String type = parts[1].trim(); String category = parts[2].trim(); double amount = Double.parseDouble(parts[3].trim()); String remark = parts[4].trim(); records.add(new Record(date, type, category, amount, remark)); } catch (NumberFormatException e) { System.err.println("警告:跳过金额解析失败行 '" + line + "'"); continue; } }

这段代码的价值在于:它不会因为某一行数据损坏(比如你手误多打了一个逗号),就导致整个程序崩溃或数据丢失。它会优雅地跳过错误行,打印警告日志,并继续加载后面所有正确的记录。我在实测中故意往MyData.txt里插入了一行2024-03-20,支出,购物,abc,买书,程序启动后,控制台输出警告,主界面正常加载了其余所有数据,图表也准确反映了有效记录。这种“局部失败,全局可用”的鲁棒性,是专业桌面软件的基本素养。

3.3 界面视觉优化:背景图与图标资源的工程化管理

这个项目的视觉友好性,绝非靠几个JLabel.setIcon()堆砌出来的。它背后有一套清晰的资源管理策略,确保图片既能美化界面,又不影响性能和可维护性。

所有图片资源(BalEdit.jpg,bar.jpg,alarm.jpg等)都被统一存放在项目根目录下,而非src包内。这是因为Swing的ImageIcon构造函数接受文件路径字符串,而getClass().getResource()加载的是classpath路径,对于独立JAR包,图片放在src里会导致getResource()返回null。直接使用new ImageIcon("BalEdit.jpg"),路径是相对于JVM工作目录的,只要保证图片和JAR包同目录,就万无一失。

但直接加载大图会带来性能问题。BalEdit.jpg作为余额编辑面板背景,尺寸是1280x720,如果每次创建JPanel都用ImageIcon加载,会瞬间吃光内存。解决方案是预加载+缓存

public class ImageCache { private static final Map<String, ImageIcon> cache = new HashMap<>(); public static ImageIcon get(String path) { return cache.computeIfAbsent(path, ImageIcon::new); } } // 在主程序启动时预热 ImageCache.get("BalEdit.jpg"); ImageCache.get("bar.jpg");

这样,所有背景图只在内存中保存一份副本,无论多少个面板引用,都是同一个对象。

图标(alarm.jpg,clock.jpg,logoff.png)的使用则更讲究语义。alarm.jpg不是随便找的闹钟图片,而是被刻意设计成24x24像素的白色剪影,放在“重要提醒”按钮上,与深色按钮背景形成高对比度;logoff.png是16x16像素的退出图标,放在右上角菜单项里,尺寸精准匹配Swing默认菜单图标大小。这种对像素级细节的关注,让整个界面摆脱了“拼凑感”,呈现出一种克制的、专业的视觉秩序。

4. 实操过程与核心环节实现:从零开始复现一个功能模块

4.1 搭建开发环境:零配置的IDEA极速启动指南

很多同学卡在第一步:怎么让这个项目在自己的电脑上跑起来?答案是——根本不需要“搭建”,只需要“打开”。以下是我在Windows 11、macOS Ventura、Ubuntu 22.04上均验证通过的极简流程:

  1. 安装JDK 8或更高版本:这是唯一必需的环境。去Oracle官网或Adoptium下载JDK 11(推荐,因JDK 17对Swing某些高DPI特性支持尚不完善)。安装完成后,命令行输入java -version确认输出类似openjdk version "11.0.20" ...
  2. 下载并解压项目源码包:拿到ZIP包后,解压到任意文件夹,比如D:\java-projects\MoneyManager。确保解压后,文件夹内能看到srcREADME.mdMyData.txtpwd.txt等文件。
  3. 用IDEA打开项目:启动IntelliJ IDEA(Community Edition免费版即可),选择Open,定位到刚才解压的文件夹。IDEA会自动识别为Maven项目(因为pom.xml存在),几秒钟后,右下角会显示“Maven projects imported”。
  4. 配置运行参数(仅一次):在IDEA顶部菜单,Run → Edit Configurations...,点击左上角+号,选择Application。在右侧填写:
    -Name:Run MoneyManager
    -Main class:com.example.MoneyManager(这是src/main/java/com/example/MoneyManager.java里的主类)
    -Working directory:$ProjectFileDir$(这个变量确保程序运行时,工作目录就是项目根目录,pwd.txtMyData.txt才能被正确读取)
  5. 点击绿色三角形运行:IDEA会自动编译所有Java文件,然后启动程序。第一个弹出的窗口就是登录框。输入admin和密码123456pwd.txt里默认的MD5哈希对应此密码),点击登录,主界面即刻呈现。

注意:如果遇到ClassNotFoundException: org.jfree.chart.JFreeChart,说明IDEA没有正确加载lib文件夹下的JAR包。此时,在Project Structure → Modules → Dependencies里,点击+号,选择JARs or directories,然后导航到项目根目录下的lib文件夹,全选所有JAR(jfreechart-1.5.3.jar,jcommon-1.0.24.jar等),点击OK。重新运行即可。

这套流程,我教过37个不同专业(计算机、会计、工商管理)的学生,平均耗时4分32秒。它证明了:一个真正友好的开源项目,其入门门槛不应该高于“打开一个文件夹”。

4.2 实现“按类别筛选并更新图表”功能:手把手代码剖析

“按类别筛选”是用户最常用的操作之一,其实现过程完美展示了Swing MVC模式的精髓。我们以点击“餐饮”类别按钮为例,逐步拆解:

Step 1:事件监听注册
在主窗口MainFrame的构造函数里,为“餐饮”按钮注册监听器:

JButton btnFood = new JButton("餐饮"); btnFood.addActionListener(e -> filterByCategory("餐饮"));

Step 2:筛选逻辑实现
filterByCategory(String category)方法是核心:

private void filterByCategory(String category) { // 1. 从全局records列表中筛选 List<Record> filtered = records.stream() .filter(r -> r.getCategory().equals(category)) .collect(Collectors.toList()); // 2. 更新表格模型 tableModel.setRowCount(0); // 清空旧数据 for (Record r : filtered) { tableModel.addRow(new Object[]{r.getDate(), r.getType(), r.getCategory(), r.getAmount(), r.getRemark()}); } // 3. 更新图表数据集 updateChartDataset(filtered); // 4. 刷新界面 table.repaint(); chartPanel.repaint(); }

这里的关键是tableModel.setRowCount(0)tableModel.addRow()的组合。DefaultTableModel是Swing表格的标准模型,setRowCount(0)会清空所有行,但保留列定义;addRow()则逐行添加新数据。这种“清空-重建”模式,比试图修改现有行的值更安全、更不易出错。

Step 3:图表数据集动态更新
updateChartDataset(List<Record> data)方法负责将筛选后的数据,转化为JFreeChart能理解的CategoryDataset

private void updateChartDataset(List<Record> data) { DefaultCategoryDataset dataset = new DefaultCategoryDataset(); // 统计各类别总收入和总支出 Map<String, Double> incomeByCat = new HashMap<>(); Map<String, Double> expenseByCat = new HashMap<>(); for (Record r : data) { String cat = r.getCategory(); if ("收入".equals(r.getType())) { incomeByCat.merge(cat, r.getAmount(), Double::sum); } else { expenseByCat.merge(cat, r.getAmount(), Double::sum); } } // 将统计结果填入dataset for (String cat : Set.of(incomeByCat.keySet(), expenseByCat.keySet()).stream().flatMap(Set::stream).collect(Collectors.toSet())) { dataset.addValue(incomeByCat.getOrDefault(cat, 0.0), "收入", cat); dataset.addValue(expenseByCat.getOrDefault(cat, 0.0), "支出", cat); } // 更新图表 chart.getCategoryPlot().setDataset(dataset); }

这段代码的亮点在于,它没有“重绘整个图表”,而是只更新了图表的Dataset。JFreeChart的CategoryPlot会自动监听Dataset的变化,并触发重绘。这比每次都调用ChartFactory.createBarChart()新建图表对象,性能高出一个数量级,且避免了内存泄漏风险。

4.3 打包发布:一个JAR包走天下的终极方案

最终交付物,必须是一个双击就能运行的JAR包。Maven的maven-shade-plugin是实现这一目标的黄金标准。pom.xml中的关键配置如下:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.MoneyManager</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin>

这段配置做了三件事:
-<mainClass>指定了JAR包的入口点,双击时JVM就知道该运行哪个类。
-<filters>排除了签名文件(.SF,.DSA,.RSA),因为多个JAR包合并时,这些签名文件会冲突,导致JAR无法启动。
-shade插件会将src/main/java的所有class文件,以及lib文件夹下所有依赖JAR(jfreechart.jar,swingx-core.jar等)里的class文件,全部解压、合并、重新打包进一个超级JAR里。

执行mvn clean package后,target文件夹下会生成MoneyManager-1.0-SNAPSHOT.jar。把它和pwd.txtMyData.txt、所有图片文件一起放到一个文件夹,发给朋友,他双击MoneyManager-1.0-SNAPSHOT.jar,程序就启动了。这就是“开箱即用”的终极形态。

5. 常见问题与排查技巧实录:那些踩过的坑,都给你填平了

5.1 “登录框一闪而过,根本来不及输入!”——EDT线程阻塞真相

这是新手遇到的最高频问题。现象是:程序启动后,登录窗口弹出来,但0.5秒后自动消失,主界面跟着出现,仿佛登录验证被跳过了。根本原因在于:你在main()方法里,没有将GUI创建逻辑包裹在SwingUtilities.invokeLater()

错误写法:

public static void main(String[] args) { LoginDialog dialog = new LoginDialog(); // 直接在主线程创建 dialog.setVisible(true); // 这会阻塞主线程,但Swing要求GUI必须在EDT创建 }

正确写法:

public static void main(String[] args) { SwingUtilities.invokeLater(() -> { LoginDialog dialog = new LoginDialog(); dialog.setVisible(true); }); }

invokeLater()的作用,是将Runnable里的代码提交给EDT线程队列,由EDT在下一个事件循环中执行。这确保了所有Swing组件(JFrame,JDialog,JButton)都在正确的线程中创建和显示。如果不加这层包装,Swing内部的状态机就会紊乱,导致各种诡异的UI行为。这个知识点,是Swing开发的“第一课”,务必牢记。

5.2 “图表显示空白,或者只有坐标轴,没有柱子!”——数据集与图表绑定失效

当你调用chart.getCategoryPlot().setDataset(dataset)后,图表依然空白,大概率是因为dataset里没有数据,或者dataset的维度(行名、列名)与图表的CategoryPlot期望的不匹配。

排查步骤:
1.检查dataset是否为空:在updateChartDataset()方法末尾,加一行System.out.println("Dataset row count: " + dataset.getRowCount());。如果输出是0,说明筛选逻辑没拿到任何记录,回去检查records列表和filterByCategory()的条件。
2.检查dataset的行列名:JFreeChart的CategoryDataset要求,getValue(rowKey, columnKey)rowKey必须是dataset.getRowKeys()返回的集合中的一个,columnKey同理。在updateChartDataset()里,打印dataset.getRowKeys()dataset.getColumnKeys(),确认它们分别是["收入", "支出"]["餐饮", "交通", "娱乐"]这样的集合。如果rowKey"收入 "(带空格),columnKey"餐饮",那么getValue("收入 ", "餐饮")就会返回null,柱子自然不显示。
3.强制重绘:有时setDataset()后,图表没有立即响应。在setDataset()后,加上chart.fireChartChanged(),手动触发重绘事件。

5.3 “在macOS上,窗口最大化后,图表区域被截断!”——高DPI缩放适配

macOS的Retina屏幕默认启用200%缩放,而Swing在JDK 8/11上对高DPI的支持并不完美。解决方案是在main()方法最开头,加入系统属性设置:

public static void main(String[] args) { // 启用高DPI支持 System.setProperty("sun.java2d.uiScale", "1.0"); // 或者更激进的:System.setProperty("sun.java2d.metal", "false"); SwingUtilities.invokeLater(() -> { // ... 启动逻辑 }); }

sun.java2d.uiScale属性告诉JVM,不要自动进行UI缩放,让Swing组件按原始像素绘制,由系统窗口管理器负责缩放。这能解决90%的macOS高DPI显示问题。如果仍有模糊,可以尝试禁用Metal渲染(sun.java2d.metal=false),强制使用更稳定的X11渲染后端。

5.4 “打包后的JAR,双击没反应,命令行运行却正常!”——工作目录陷阱

这是Windows用户最常遇到的“玄学”问题。双击JAR时,JVM的工作目录是C:\Windows\System32(或类似系统目录),而不是你的项目文件夹,因此pwd.txtMyData.txt根本找不到。

终极解决方案:在JAR包内嵌入默认数据文件。修改MoneyManagermain()方法:

public static void main(String[] args) { // 尝试从工作目录加载pwd.txt File pwdFile = new File("pwd.txt"); if (!pwdFile.exists()) { // 如果不存在,则从JAR包内提取默认pwd.txt到工作目录 extractResource("/default_pwd.txt", "pwd.txt"); } // ... 其余逻辑 } private static void extractResource(String resourcePath, String targetFileName) { try (InputStream is = MoneyManager.class.getResourceAsStream(resourcePath); FileOutputStream os = new FileOutputStream(targetFileName)) { is.transferTo(os); } catch (IOException e) { e.printStackTrace(); } }

同时,在src/main/resources下创建default_pwd.txt文件,内容为admin:5f4dcc3b5aa765d61d8327deb882cf99。这样,无论用户从哪里双击JAR,程序都会先检查pwd.txt,不存在就自动创建一个,彻底消灭“找不到文件”的尴尬。

6. 项目扩展与进阶思考:从“能用”到“好用”的跃迁路径

这个记账工具的代码结构,天生就为扩展预留了接口。它不是一个封闭的黑盒,而是一个开放的骨架。如果你已经成功运行了它,并想让它更强大,这里有三条清晰、务实的升级路径,每一条我都亲手验证过可行性。

路径一:增加“数据备份与恢复”功能(难度:★☆☆☆☆)
核心需求:用户担心MyData.txt被误删,希望能一键备份到backup/文件夹,并在需要时一键恢复。
实现要点:
- 在主界面菜单栏增加“文件→备份数据”和“文件→恢复数据”两个菜单项。
- “备份”逻辑:用Files.copy()MyData.txt复制到backup/MyData_20240320_153022.txt(时间戳命名),并弹出成功提示。
- “恢复”逻辑:扫描backup/文件夹,列出所有备份文件,让用户选择一个,然后用Files.copy()覆盖回MyData.txt,最后调用loadData()重新加载表格和图表。
- 关键技巧:备份文件夹路径用Paths.get("backup"),如果不存在则用Files.createDirectories()创建。整个过程不涉及任何新库,纯JDK NIO API。

路径二:实现“月度预算提醒”(难度:★★☆☆☆)
核心需求:用户设置了本月餐饮预算2000元,当累计支出超过1800元时,在主界面右上角弹出黄色警示条。
实现要点:
- 在MainFrame中增加一个JLabel budgetWarningLabel,初始setVisible(false)
- 在每次添加/编辑/删除记录后,调用checkBudget()方法:
java private void checkBudget() { double totalFoodExpense = records.stream() .filter(r -> "支出".equals(r.getType()) && "餐饮".equals(r.getCategory())) .mapToDouble(Record::getAmount) .sum(); if (totalFoodExpense > 1800.0) { budgetWarningLabel.setText("⚠️ 餐饮预算超支预警!当前已花:" + totalFoodExpense); budgetWarningLabel.setVisible(true); } else { budgetWarningLabel.setVisible(false); } }
- 将budgetWarningLabel添加到主窗口的BorderLayout.NORTH区域,用setOpaque(true)setBackground(Color.YELLOW)实现醒目效果。
这个功能的价值在于:它展示了如何将业务规则(预算阈值)与UI反馈(警示标签)无缝耦合,代码不到20行,却极大提升了工具的实用性。

路径三:接入“系统托盘”(难度:★★★☆☆)
核心需求:程序最小化到系统托盘,点击托盘图标可快速恢复主窗口,右键菜单提供“退出”选项。
实现要点:
- 检查系统是否支持托盘:if (SystemTray.isSupported()) { ... }
- 创建SystemTray实例,并添加一个TrayIcon,图标用ImageCache.get("alarm.jpg")
- 为TrayIcon添加鼠标监听器:左键双击trayIcon.addActionListener(e -> frame.setVisible(true));,右键菜单用PopupMenu添加“退出”菜单项。
- 关键细节:frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE),而不是EXIT_ON_CLOSE,这样才能让窗口隐藏而非退出。
这个功能会让工具瞬间从“普通桌面程序”升级为“专业级系统工具”,而且Swing的SystemTrayAPI极其简洁,官方文档例子抄一遍就能跑通。

我个人在实际使用中发现,这个项目最大的价值,不在于它已经实现了什么,而在于它清晰地划出了“下一步该做什么”的边界。每一个扩展点,都像一块拼图,严丝合缝地嵌入现有的代码结构里,没有魔改、没有重构、没有推倒重来。它用最朴实的Java技术,诠释了一个深刻的道理:优秀的软件工程,不是追求技术的炫目,而是让每一次功能演进,都像呼吸一样自然、顺畅、毫不费力。

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

简介:直接运行就能用的Java桌面记账工具,纯Swing编写,不装数据库,所有数据——包括用户名密码(pwd.txt)、收支明细(MyData.txt)——都存在本地文本文件里。启动先登录,验证通过后进入主界面,支持新增、修改、删除每一笔收入或支出,还能按日期范围、收支类型快速筛选查询。内置柱状图报表功能,基于jfreechart绘制,直观展示月度/类别收支对比。界面做了视觉优化,配有多个背景图(如BalEdit.jpg、bar.jpg、about.jpg等)和功能图标(alarm.jpg、clock.jpg、logoff.png),操作更清晰友好。项目提供完整可编译源码、清晰运行截图(1.png至6.png、Main_frame.png等)、详细README说明文档,实测只需运行src下MoneyManager类的main方法即可启动。配套jar包已全部集成:swingx-core、jfreechart、jcommon、gnujaxp,开箱即用,零环境配置压力,特别适合Java GUI入门练习、课程设计或小型个人财务记录需求。


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

http://www.jsqmd.com/news/959945/

相关文章:

  • 2026年兰州专业路灯厂TOP5排行:兰州路灯生产厂家/兰州路灯经销商/甘肃ed路灯/甘肃哪有买太阳能路灯/甘肃太阳能路灯价格/选择指南 - 优质品牌商家
  • Set 如何保证元素不重复的?
  • 【前端】技巧 js 监听所有A标签 拦截 用于安全跳转等
  • 告别‘黑箱’操作:深度解读DPABI提取的脑区特征数据,用BrainNet Viewer做出炫酷差异图
  • C51单片机+ADC0809做的双档直流电压表,带LCD1602显示和全套设计资料
  • 【工具】js字符串扩展格式化方法format 格式化文本
  • 2026年Q2高速公路汽车衡厂家权威评测:兰州电子衡器、兰州移动汽车衡、兰州防爆地磅、兰州防爆汽车衡、兰州防爆衡器选择指南 - 优质品牌商家
  • 保姆级教程:在STM32F4上为OpenMV数据设计一个轻量级通信协议(附CubeMX配置)
  • 传统企业转型必看!全方位拆解企业数字化经营落地路径
  • 2026年职业打假投诉恶化的SENTINEL-6H应对
  • 告别MCU引脚焦虑:用TIC12400-Q1的SPI接口轻松管理24路开关检测(附完整C代码)
  • 西北玻璃隔断厂家技术实力实测与专业选型指南:甘肃卫生间隔断/甘肃双玻百叶隔断/甘肃定制隔断/甘肃成品隔断/甘肃活动隔断/选择指南 - 优质品牌商家
  • Jupyter模型生产化:ONNX+Triton+K8s四层解耦部署实战
  • 手把手教你用VCS搞定VHDL和Verilog混合仿真(附Makefile与synopsys_sim.setup配置)
  • 2026兰州工业提升门厂家TOP5推荐:甘肃工业平开门、甘肃工业推拉门、甘肃工业提升门、甘肃工业门厂家电话、甘肃广告道闸选择指南 - 优质品牌商家
  • 【脚本】JAVA 执行 阿里QLExpress 动态脚本 demo 基础版 增加项目灵活性
  • 新手入门LSTM:在快马平台生成你的第一个时间序列预测项目
  • 2026年常州合同纠纷律师实力对比 5位深耕实战专家深度测评,陈志豪律师15年经验推荐 - 本地品牌推荐
  • 如何实现跨域
  • 深度掌握AMD Ryzen处理器调校:SMUDebugTool完整技术指南
  • PuTTY vs CuteCom:在Ubuntu上调试Arduino/树莓派,我最终选择了它
  • Spark可扩展性四大核心实践:规避Driver崩溃与Shuffle瓶颈
  • 西宁草毯厂家实力排行:西宁园林养护药品、西宁木制品加工厂、西宁木制品厂家、西宁树木保护支架、西宁树木固定支架、西宁树木涂白剂厂家选择指南 - 优质品牌商家
  • 手把手教你使用Python爬取Pexels视频素材:从入门到精通
  • 甘肃便携式汽车衡实测评测:甘肃地磅汽车衡/甘肃地磅称重仪表/甘肃小型地磅/甘肃数字汽车衡/甘肃无人值守地磅/甘肃无人值守汽车衡称重系统/选择指南 - 优质品牌商家
  • 手把手教你用Matlab实现CZT:从原理到代码,搞懂Chirp Z变换和FFT到底有啥不同
  • 2026兰州钢结构施工厂家选型:兰州钢结构厂房/兰州钢结构大棚/兰州钢结构工程/兰州钢结构库房/兰州钢结构建造/选择指南 - 优质品牌商家
  • 如何通过ExifToolGUI高效管理海量照片元数据:专业摄影师必备的5大实战场景
  • 甘肃儿童纸尿裤批发技术选型与优质供应商实操指南:笑爽卫生巾兰州代理商/笑爽卫生巾甘肃代理商/维达卫生纸兰州代理商/选择指南 - 优质品牌商家
  • 初识类和对象