☰
Java Swing万年历记事本:单文件课设源码解析与优化
2026/11/2 12:16:25 网站建设 项目流程

简介:这份JAVA万年历源码面向Java初学者与课程设计实践者,围绕日期时间处理、Swing图形界面与事件驱动编程展开,可用于完成课程设计或巩固Java基础。压缩包内共1个doc文件,约317KB,以课程设计报告形式呈现,涵盖总体设计、类与方法说明、运行效果展示及完整源代码,便于对照理解项目结构。已有185人学习下载,适合需要参考实现思路与代码组织的读者。内容涉及Calendar与java.time日期操作、JFrame与JComboBox等界面组件、ActionListener与MouseListener事件处理、文件读写保存日记、闰年判断与月份天数计算等知识点,并附有布局管理与异常处理说明。读者可从中获取一个可运行的万年历记事本案例,理清从需求分析到编码实现的完整流程,也可借鉴其类设计、方法划分与测试调试思路,用于自己的课程作业或小型桌面应用开发。

1. 单文件 Swing 万年历记事本:一份能跑起来的 Java 课设源码

翻课程设计仓库时看到一份 2012 年的 Java 课设,整个项目就一个calendar.java,却把万年历和按日期记事本塞进了同一个窗口。左边是 7×7 的日期网格,右边是当天日记的文本区,点某一天就能写、能存、能删。它解决的不是什么高深问题,而是“给定年月日,算出这个月第一天是星期几、这个月有几天、把 42 个格子填对”这一串日期计算,外加一套 Swing 事件分发和本地文件读写。适合两类人:正在做 Java 课设、想找一份结构完整又不臃肿的参考实现的学生;以及想快速复习Calendar计算、ActionListener/MouseListener事件模型和FileWriter/FileReader落盘的老手。源码是单类、无第三方依赖,JDK 装好就能编译,这一点在课设场景里比什么框架都实在。

2. 拆开这个单类:成员变量、方法与日期计算骨架

2.1 一个类扛下全部:成员变量与职责划分

这份源码最反直觉的地方是“只有一个类”。calendar继承JFrame,同时实现ActionListener和MouseListener,界面构建、事件响应、日期计算、文件读写全在一个文件里。对课设来说这是优点——交作业时不用解释包结构;对想复用的人来说这是坑——所有逻辑耦合在构造函数里,改一处容易牵动全局。

成员变量大致分四组,先理清楚再动手改:

分组代表变量作用
日期状态year、month、day当前显示的日期,构造函数传入
查询暂存yearafterquery、monthafterquery、prioryear、priormonth用户输入框/下拉框选中的年月,与当前显示解耦
界面组件LeftPane、RightPane、ShowDays[42]、MonthCombobox、CenterText左日历右记事本,42 个JTextField承载日期格
状态标志ischange、ischange_priornext、key标记是否切换过、当前日记文件名键

key是整个记事本功能的锚点,格式是年_月缩写_日,比如2012_Jun_25。保存、读取、删除都靠它拼文件名,所以日期一换,key必须同步更新,否则会出现“看着 6 月 25 日,存进了 6 月 24 日的文件”这种血泪 bug。

2.2 日期计算三件套:闰年、当月天数、起始星期

万年历的核心不是界面,是三个方法:IsLeapYear、GetNumOfDaysInMonth、GetStartDay。前两个是标准写法,第三个是这份源码里最值得看也最容易翻车的部分。

// 闰年判断:能被400整除,或能被4整除但不能被100整除 public boolean IsLeapYear(int year) { if ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)) return true; else return false; } // 当月天数:大月31、小月30、二月看闰年 public int GetNumOfDaysInMonth(int year, int month) { if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) return 31; if (month == 4 || month == 6 || month == 9 || month == 11) return 30; if (month == 2) return IsLeapYear(year) ? 29 : 28; return 0; }

逻辑说明:IsLeapYear的优先级不能写错,&&高于||,所以year % 4 == 0 && year % 100 != 0会先结合,再和year % 400 == 0做或运算,结果正确。GetNumOfDaysInMonth用两个if兜住 31 天和 30 天的月份,二月单独判断,最后return 0是防御性写法,正常调用不会走到。

真正需要盯的是GetStartDay:

public int GetStartDay(int year, int month) { int startday0001 = -32768; // 基准偏移,原作者的经验常量 long totalnumofdays = GetTotalNumOfDays(year, month); return (int) ((totalnumofdays + startday0001) % 7); } public long GetTotalNumOfDays(int year, int month) { long total = 0; for (int i = -32767; i < year; i++) { // 从负年份累加 total += IsLeapYear(i) ? 366 : 365; } for (int i = 1; i < month; i++) { total += GetNumOfDaysInMonth(year, i); } return total; }

参数说明:startday0001 = -32768是一个为了让结果落在 0~6 的校准常量,配合从-32767年开始累加,本质是在算“从某个基准日到目标月 1 号的总天数,再对 7 取模”。这个写法能跑,但有两个边界:一是GetTotalNumOfDays是 O(year) 的循环,年份越大越慢,2012 年无所谓,填 9999 年就要循环一万多次;二是负年份的闰年判断依赖%在负数上的行为,Java 里-4 % 4 == 0成立,所以负闰年也能算对,但可读性极差。

我一般会把它换成java.time的写法,结果一致且不用猜常量:

import java.time.LocalDate; // 返回当月1号是星期几,0=周日,1=周一……6=周六 public int GetStartDayModern(int year, int month) { LocalDate first = LocalDate.of(year, month, 1); return first.getDayOfWeek().getValue() % 7; // DayOfWeek 周一=1…周日=7 }

注意getValue()返回周一=1 到周日=7,而原源码的week[]数组是SUN开头,所以要对 7 取模把周日映射回 0。这一步不换算,整个日历会整体错位一天,属于典型的“看着能跑、日期全错”的玄学问题。

2.3 42 个格子怎么填:PrintMonthBody 的边界处理

日历面板是GridLayout(7,7),第一行放星期标题,剩下 42 个JTextField放日期。PrintMonthBody干三件事:从startday开始填 1 到dayinmonth;把startday之前的格子清空;把startday + dayinmonth之后的格子清空。

public void PrintMonthBody(int startday, int dayinmonth, int day) { for (int i = startday, n = 1; i < startday + dayinmonth; i++) { ShowDays[i].setText("" + n); ShowDays[i].setHorizontalAlignment(0); if (n == day) { // 高亮当前选中日 ShowDays[i].setForeground(Color.green); ShowDays[i].setFont(new Font("TimesRoman", Font.BOLD, 20)); } else { ShowDays[i].setFont(new Font("TimesRoman", Font.BOLD, 12)); ShowDays[i].setForeground(Color.white); } ShowDays[i].setBackground(Color.DARK_GRAY); n++; } for (int i = 0; i < startday; i++) { // 月初空白格 ShowDays[i].setText(""); ShowDays[i].setBackground(Color.DARK_GRAY); } for (int i = startday + dayinmonth; i < 42; i++) { // 月末空白格 ShowDays[i].setText(""); ShowDays[i].setBackground(Color.DARK_GRAY); } }

逻辑说明:第一个循环负责填数字并高亮day那天,后两个循环负责擦除。这里有个隐藏前提——startday + dayinmonth最大是 6 + 31 = 37,小于 42,所以不会数组越界。但如果哪天有人把ShowDays改成 35 个格子,二月起始为周六时就会越界,改数组长度前先算一遍这个上界。

参数怎么改:想换高亮颜色改Color.green;想改字体大小改两个Font构造里的数字;想让非选中日也显示浅色,把Color.white换成Color.LIGHT_GRAY。这些都不影响计算逻辑,属于安全改动区。

3. 事件分发与记事本落盘:从点击到文件的全链路

3.1 ActionListener 与 MouseListener 的分工

这个类同时挂了两种监听器,职责边界要分清,否则事件会互相打架。

ActionListener挂在按钮、下拉框、年份输入框上,统一走actionPerformed。源码里用setActionCommand给按钮打了标记,比如上一月按钮setActionCommand("prior")、下一月setActionCommand("next")、保存"Save"、删除"Delete"。分发时靠e.getActionCommand()判断,比用e.getSource() == 某按钮更稳,因为前者不依赖对象引用。

MouseListener只挂在 42 个日期格上,用来实现“点哪天就切到哪天”。mouseClicked里拿到被点格子的文本,转成day,再刷新右侧记事本和key。这里有个必须处理的细节:空白格的文本是空串,Integer.parseInt("")会抛NumberFormatException,所以点击处理前要先判空。

public void mouseClicked(MouseEvent e) { JTextField cell = (JTextField) e.getSource(); String text = cell.getText(); if (text == null || text.trim().isEmpty()) return; // 空白格直接忽略 int clickedDay = Integer.parseInt(text); this.day = clickedDay; key = year + "_" + SwitchMonth + "_" + clickedDay; // 同步文件名键 NorthMonthDayYear.setText(">>" + year + "," + SwitchMonth + "," + clickedDay + "<<"); ReadRecord(); // 切换即尝试读当天日记 PrintMonth(year, month, day); // 重绘高亮 }

逻辑说明:先判空再解析,是这类“格子即按钮”界面的通用防御。key必须在读文件之前更新,否则ReadRecord会去读上一天的文件。PrintMonth重绘是为了让绿色高亮跟着走,不重绘的话高亮会留在旧格子上。

3.2 日记文件的命名、写入与读取

记事本用最朴素的FileWriter/FileReader,目录固定为相对路径Diary,文件名key + ".txt"。写入前先mkdir,读取时先判exists。

public void WriteRecord() { String content = CenterText.getText(); try { File dir = new File("Diary"); if (!dir.exists()) dir.mkdir(); // 目录不存在就建 File myfile = new File(dir, key + ".txt"); // 用 File 拼接,别手拼反斜杠 FileWriter record = new FileWriter(myfile); record.write(content); record.close(); JOptionPane.showMessageDialog(this, "Save success!"); } catch (IOException ex) { JOptionPane.showMessageDialog(this, "Save failed: " + ex.getMessage()); } }

参数说明:new File(dir, key + ".txt")比源码里"Diary\\" + key + ".txt"更可移植,后者在非 Windows 环境会生成带反斜杠的怪文件名。FileWriter默认覆盖写,想追加就换new FileWriter(myfile, true),但记事本场景覆盖才是对的——用户改完再存,要的就是最新版本。

读取侧源码用JOptionPane.showConfirmDialog问“今天有日志,要读吗”,这个交互在切换日期时很烦,因为每点一天就弹一次。我一般改成静默读取,读不到就显示占位提示:

public void ReadRecord() { File myfile = new File("Diary", key + ".txt"); if (!myfile.exists()) { CenterText.setText("今天还没有记录。"); return; } StringBuilder sb = new StringBuilder(); try (FileReader reader = new FileReader(myfile)) { int ch; while ((ch = reader.read()) != -1) sb.append((char) ch); CenterText.setText(sb.toString()); } catch (IOException ex) { CenterText.setText("读取失败:" + ex.getMessage()); } }

逻辑说明:try-with-resources保证FileReader一定关闭,比源码里手动close更抗异常。用StringBuilder拼字符,避免content = content + (char) b在长文本下的字符串反复创建。

3.3 删除操作为什么不该用 Runtime.exec

源码删除文件用的是Runtime.getRuntime().exec("cmd /c del " + filepath),这是整份代码里最该改的一处。它有三个问题:依赖 Windows 命令行,换系统就废;filepath里一旦有空格或特殊字符,命令会被截断甚至执行意外命令;exec是异步的,弹“删除成功”时文件可能还没删掉。

public void DeleteFile() { File myfile = new File("Diary", key + ".txt"); int n = JOptionPane.showConfirmDialog(this, "确定删除当天记录?", "Confirm", JOptionPane.YES_NO_OPTION); if (n != JOptionPane.YES_OPTION) return; if (myfile.exists() && myfile.delete()) { JOptionPane.showMessageDialog(this, "Delete success!"); CenterText.setText("今天还没有记录。"); } else { JOptionPane.showMessageDialog(this, "文件不存在或删除失败。"); } }

File.delete()返回布尔值,同步执行,跨平台,没有命令注入面。这一处替换属于“改完立刻能感觉到稳”的改动,建议拿到源码第一件事就换掉。

4. 编译运行与界面调优:让这份 2012 年的代码在当下跑顺

4.1 编译运行的最小步骤

源码是单文件、无包声明,编译运行只要三步。先确认 JDK 版本,Swing 在 JDK 8 到 21 都能跑,但javac对老写法的警告会变多。

# 1. 确认 JDK 可用 javac -version # 2. 编译(源码文件名必须是 calendar.java,类名大小写要一致) javac -encoding UTF-8 calendar.java # 3. 运行 java calendar

参数说明:-encoding UTF-8不能省,源码里有中文提示和中文注释,Windows 默认 GBK 编译会乱码甚至报错。如果源码里main方法没写全(课设源码常见截断),需要自己补一个入口:

public static void main(String[] args) { // 用当前日期初始化,避免写死 2012 年 java.util.Calendar now = java.util.Calendar.getInstance(); int y = now.get(java.util.Calendar.YEAR); int m = now.get(java.util.Calendar.MONTH) + 1; // Calendar 月份从 0 开始 int d = now.get(java.util.Calendar.DAY_OF_MONTH); new calendar(y, m, d).setVisible(true); }

注意Calendar.MONTH从 0 开始,+1才是人类月份,这个偏移是 Java 日期 API 的经典坑,漏加就会整体差一个月。

4.2 图片资源缺失与布局微调

源码里new ImageIcon("../images/Handle.gif")、prior.gif、next.gif引用了外部图片,课设压缩包里往往没带。直接跑不会崩,但按钮会变成空白或显示文件名。两种处理:把图片补到images目录并调整相对路径;或者干脆去掉图标,用文字按钮。

// 方案一:去掉图标,纯文字,最省事 ToToday = new JButton("回到今天"); PriorMonth = new JButton("上一月"); NextMonth = new JButton("下一月");

布局上,JSplitPane把左右面板分开,源码里new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, RightPane, LeftPane)的顺序是右在左、左在右,和注释“左边记事本右边日历”相反。想改成左日历右记事本,把两个参数对调即可。分隔条位置可以用split.setDividerLocation(0.6)按比例设,比默认的居中更符合阅读习惯。

4.3 用 java.time 替换手写日期计算

前面提过GetStartDay的常量校准写法可读性差,实际改造时可以把三个日期方法整体替换,界面代码一行不动:

import java.time.LocalDate; import java.time.YearMonth; public int GetStartDay(int year, int month) { return LocalDate.of(year, month, 1).getDayOfWeek().getValue() % 7; } public int GetNumOfDaysInMonth(int year, int month) { return YearMonth.of(year, month).lengthOfMonth(); } public boolean IsLeapYear(int year) { return YearMonth.of(year, 2).lengthOfMonth() == 29; }

逻辑说明:YearMonth.lengthOfMonth()内部已经处理了闰年二月,IsLeapYear用“二月是不是 29 天”反推,语义比取模表达式更直白。替换后GetTotalNumOfDays可以整个删掉,PrintMonth的调用链不受影响。这一步属于“改计算不改界面”的安全重构,适合拿来练手。

5. 避坑排查:这份源码最容易翻车的五个地方

5.1 日期整体错一天

现象:日历能显示,但每个月 1 号落的星期位置和真实日历差一格。原因:GetStartDay的startday0001常量与week[]数组的起始星期不匹配,或者换成java.time后忘了% 7把周日从 7 映射回 0。解决:先用一个已知日期验证,比如 2024 年 1 月 1 日是周一,GetStartDay(2024, 1)应返回 1;对不上就检查取模和数组顺序,别靠肉眼调常量。

5.2 点空白格抛 NumberFormatException

现象:点日历里月初月末的空白格,控制台报数字解析异常,界面无响应。原因:mouseClicked直接Integer.parseInt(cell.getText()),空白格文本是空串。解决:解析前判空并return,见 3.1 的代码。这个坑在课设答辩现场被点到会很尴尬,属于必改项。

5.3 日记存到了错误日期

现象:明明选的是 6 月 25 日,保存后去 6 月 24 日却能读出来。原因:key在切换日期时没同步更新,或者ReadRecord在key更新前被调用。解决:把key的赋值收敛到一个方法里,任何改变year/month/day的路径都调它,读文件前先确认key与界面显示一致。

5.4 中文乱码或编译报错

现象:javac报“编码 GBK 的不可映射字符”,或运行时中文提示变问号。原因:源码含中文,编译未指定编码。解决:编译加-encoding UTF-8;如果源码本身是 GBK 存的,先转成 UTF-8 再编译,别混着来。运行时的字体乱码则要检查Font("TimesRoman", ...)是否支持中文,换成"Microsoft YaHei"或"SimSun"更稳。

5.5 删除按钮在非 Windows 环境失效

现象:点删除弹“成功”,但文件还在,或者直接报错。原因:Runtime.exec("cmd /c del ...")依赖 Windows 命令行。解决:换成File.delete(),见 3.3。这个坑的隐蔽性在于 Windows 上能跑,换台机器演示就翻车,属于“演示前必须过一遍”的检查项。

6. 把它改成能交差的课设:三个可验证的进阶改法

拿到这份源码,直接交只能算“能跑”,想让它经得起追问,建议做三处改动,每处都能当场验证。

第一处是把日期计算换成java.time,然后用一组边界日期做回归。下面这段可以直接当测试用,跑通说明计算层没问题:

public class DateCheck { public static void main(String[] args) { calendar c = new calendar(2024, 1, 1); // 2024-01-01 是周一,期望 1 System.out.println("2024-01 start=" + c.GetStartDay(2024, 1)); // 2024 是闰年,二月期望 29 System.out.println("2024-02 days=" + c.GetNumOfDaysInMonth(2024, 2)); // 1900 不是闰年(能被100整除但不能被400整除),二月期望 28 System.out.println("1900-02 days=" + c.GetNumOfDaysInMonth(1900, 2)); // 2000 是闰年(能被400整除),二月期望 29 System.out.println("2000-02 days=" + c.GetNumOfDaysInMonth(2000, 2)); } }

四个断言覆盖了普通年、普通闰年、整百年非闰、整百年闰四种情况,比肉眼翻日历靠谱得多。

第二处是把日记存储从“一天一个 txt”升级成“一个文件按日期分节”,或者至少给文件名加统一前缀,避免Diary目录里文件散落。改动量不大,但答辩时能讲清楚“为什么这样设计”,比单纯说“能存能读”有内容。

第三处是给切换月份加边界保护。源码里上一月/下一月在 1 月和 12 月附近容易把month弄成 0 或 13,进而让GetNumOfDaysInMonth返回 0,日历整月空白。处理方式是在切换逻辑里做进位借位:

// 上一月:1 月退到上一年 12 月 if (month == 1) { month = 12; year--; } else { month--; } // 下一月:12 月进到下一年 1 月 if (month == 12) { month = 1; year++; } else { month++; }

这三处改完,代码量增加不到一百行,但覆盖了“计算正确、存储合理、边界不崩”三个课设最常被问的点。

最后说个习惯。我早年改这类单类 Swing 项目,总想着“先跑起来再说”,结果每次都在日期错位和文件路径上返工。后来强制自己按“先验证计算层、再动界面层、最后碰文件层”的顺序走,每层用一组固定输入确认输出,返工次数明显下来了。这份 2012 年的源码结构不算漂亮,但胜在完整、可编译、每个方法都能单独拎出来测,拿来练这个顺序正合适。希望帮到你。

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

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

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

立即咨询