简介:这份JAVA万年历源码面向Java初学者与课程设计需求者,围绕日期时间处理、Swing图形界面与事件驱动编程展开,帮助读者在完整项目中理解Calendar与java.time的用法、闰年判断、月份天数计算及文件读写等核心知识点。压缩包内共1个doc文件,约317KB,为Java课程设计报告形式,涵盖总体设计、类与方法说明、运行效果展示及源代码,可对照学习calendar类的成员变量、布局管理与ActionListener、MouseListener事件响应逻辑。目前已有185人学习下载,适合需要参考课程设计结构、梳理万年历与记事本功能实现思路的读者,也可作为Java GUI入门练习的对照材料,便于理解界面组件组织与日记保存、删除等操作的代码组织方式。
1. 单文件 Swing 万年历记事本:一份能跑起来的 Java 课设源码
翻课程设计仓库时看到这份calendar.java,第一反应是"这也太老了"——2012 年的 Swing 代码,单文件、无包名、无构建脚本,连JComboBox都还在用原始类型。但把它丢进 JDK 里编译一遍,窗口弹出来的那一刻我改主意了:左边记事本、右边月历、点日期写日记、按 Save 落盘成 txt,功能闭环完整,逻辑全在一个类里,对想搞懂"GUI 事件驱动 + 日期计算 + 文件读写"三件套的人来说,比那些拆成十几个包的脚手架项目好读得多。
这份 JAVA 万年历源码解决的是具体问题:给你一个不依赖任何第三方库、纯 JDK 就能编译运行的桌面日历记事本原型。适合三类人——正在做 Java 课程设计需要参考完整功能闭环的学生、想复习 Swing 布局与事件监听的老手、以及需要一份"日期算法手写实现"对照材料的人。它不现代,但每一行都能追到出处,这正是它的价值。
2. 拆开这个单文件:类结构、日期算法与事件模型
2.1 一个类扛下所有:成员变量与方法映射
整个系统只有一个calendar类,继承JFrame并同时实现ActionListener和MouseListener。这种"全塞一个类"的写法在今天看是反模式,但对课设场景反而友好——不用在多个文件间跳转,读一遍就能建立完整心智模型。
成员变量按职责可以分成四组,先理清楚再动手改:
| 分组 | 代表变量 | 作用 |
|---|---|---|
| 日期状态 | year、month、day | 当前显示的日期,构造函数传入 |
| 查询暂存 | yearafterquery、monthafterquery、prioryear、priormonth | 用户输入框/下拉框选中的目标年月,与当前显示解耦 |
| 面板容器 | LeftPane、RightPane、LeftNorth、LeftCenter | 左右分栏与内部布局 |
| 日历控件 | ShowDays[42]、TopBarTitle[7]、MonthCombobox | 42 个日期格(6 行 × 7 列)、7 个星期标题、月份下拉 |
ShowDays数组长度固定 42 是关键设计——一个月最多 31 天,加上月初最多 6 个空位,6×7=42 刚好覆盖所有月份排布,不用动态扩容。这个数字不是随便定的,后面所有PrintMonthBody的循环边界都围绕它转。
方法层面,核心链路是:PrintMonth(year, month, day)→GetStartDay算月初是星期几 →GetNumOfDaysInMonth算当月天数 →PrintMonthBody把数字填进ShowDays。记事本链路是:WriteRecord写文件、ReadRecord读文件、DeleteFile删文件,文件名用key = year + "_" + SwitchMonth + "_" + day拼接。
2.2 日期算法:闰年判断与月初星期推算
这套代码没有用java.util.Calendar现成 API,而是手写了全套日期计算,这恰恰是它作为学习材料的意义所在。三个方法构成依赖链:
// 闰年判断:标准格里高利历规则 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; } // 从基准年累加到目标年的总天数 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; }GetStartDay里有个startday0001 = -32768的基准偏移,配合% 7取模得到星期索引。这里要特别注意:Java 的%对负数返回负值,所以当totalnumofdays + startday0001为负时,取模结果可能是负数,直接拿去索引ShowDays会抛ArrayIndexOutOfBoundsException。这是这份源码最隐蔽的坑,后面避坑章节会展开。
参数含义上,GetTotalNumOfDays的循环起点-32767对应int能表示的最小年份附近,属于原作者拍脑袋选的基准,实际使用时年份范围在 1 到 9999 之间就够,不必纠结这个负数起点。
2.3 事件分发:ActionListener 与 MouseListener 如何协作
界面交互靠两个监听器分工。ActionListener挂在按钮和下拉框上,actionPerformed里通过getActionCommand()区分来源——PriorMonth和NextMonth分别设了"prior"和"next"命令字符串,Save和Delete按钮也各自设了命令。这种用字符串路由的写法比e.getSource() == button更清晰,但要注意命令字符串必须和判断分支严格一致,拼错一个字母就静默失效。
MouseListener挂在 42 个ShowDays格子上,mouseClicked里读取被点格子的文本,反推出日期,更新day和右侧标题NorthMonthDayYear,再触发ReadRecord看当天有没有日记。这里有个隐含假设:被点的格子必须有数字文本,空格子(月初月末的填充位)点下去getText()返回空串,Integer.parseInt会直接抛NumberFormatException。源码里没做判空,属于典型的手写 GUI 疏漏。
布局上用了JSplitPane水平分割左右面板,左侧BorderLayout分北(查询区)、中(日历网格)、南(回到今天按钮),日历网格用GridLayout(7, 7)排 7 个星期标题加 42 个日期格。GridLayout保证每格等宽等高,视觉整齐,但窗口拉伸时格子会跟着变形,这是GridLayout的固有特性,不是 bug。
3. 从源码到可运行窗口:编译、资源与文件落盘
3.1 编译运行与图片资源缺失的补救
拿到calendar.java后第一步是确认 JDK 版本。代码用了原始类型JComboBox和new Integer(i),在 JDK 9 以上会有警告但不影响编译,JDK 17 也能跑。编译命令很直接:
# 编译(源码无包名,直接 javac) javac -encoding UTF-8 calendar.java # 运行,注意类名是小写 calendar java calendar但直接跑大概率翻车——源码里ToToday、PriorMonth、NextMonth三个按钮用了new ImageIcon("../images/Handle.gif")这类相对路径图片。图片文件不在,ImageIcon不会报错,按钮会变成空白或只显示文字,功能不受影响但界面残缺。补救方式有两种:一是按路径补上images目录和对应 gif;二是把ImageIcon参数改成null或直接去掉图标构造,只保留文字。我一般选后者,课设演示时文字按钮反而更清楚。
提示:
javac加-encoding UTF-8是因为源码里有中文注释和JOptionPane中文提示,不加在部分系统上会编译报"编码 GBK 的不可映射字符"。
3.2 日记文件的读写路径与 key 命名规则
记事本功能的核心是key变量,它在构造函数里被赋值为year + "_" + SwitchMonth + "_" + day,比如 2012 年 6 月 25 日对应2012_Jun_25。WriteRecord用这个 key 拼出Diary/2012_Jun_25.txt,先mkdir建目录再FileWriter写入:
public void WriteRecord() { String content = CenterText.getText(); try { File Diary = new File("Diary"); Diary.mkdir(); // 目录已存在时返回 false,不抛异常,可安全重复调用 File myfile = new File("Diary/" + key + ".txt"); FileWriter Record = new FileWriter(myfile); Record.write(content); // 直接写整个字符串,比逐字符循环简洁 Record.close(); JOptionPane.showMessageDialog(this, "Save success!"); } catch (IOException ex) { // 原码这里吞掉了异常,建议至少打印 ex.printStackTrace() } }原码用char[]逐字符write,效果一样但啰嗦,我上面简化成整串写入。参数上要注意FileWriter默认覆盖模式,同一天重复保存会覆盖旧内容而不是追加——这符合"编辑当天日记"的语义,但如果你想要追加式日志,得换成new FileWriter(myfile, true)。
ReadRecord的逻辑有个顺序问题:它先new FileReader(myfile)再判断myfile.exists()。文件不存在时FileReader构造就抛FileNotFoundException,直接跳到 catch 把文本区设成 "Today has not logs.",后面的exists判断永远走不到。功能上碰巧正确,但属于"靠异常兜底"的写法,重构时应该先判存在再开流。
3.3 删除操作的跨平台隐患
DeleteFile用了Runtime.getRuntime().exec("cmd /c del " + filepath),这是整份源码里最该改的一处。cmd /c del是 Windows 专属命令,在 Linux 或 macOS 上直接失效;而且字符串拼接路径,一旦路径含空格就会被截断。正确做法是用java.io.File.delete():
public void DeleteFile() { File myfile = new File("Diary/" + key + ".txt"); int n = JOptionPane.showConfirmDialog(this, "Are you sure delete the file?", "Confirm", JOptionPane.YES_NO_CANCEL_OPTION); if (n == JOptionPane.YES_OPTION) { if (myfile.exists() && myfile.delete()) { JOptionPane.showMessageDialog(this, "Delete successed!"); CenterText.setText("Today has not logs."); } else { JOptionPane.showMessageDialog(this, "Delete failured!"); } } }File.delete()返回布尔值表示是否成功,比exec后无法感知结果可靠得多。这个改动不影响任何其他逻辑,属于低风险高收益的重构,建议动手时第一件事就换掉。
4. 避坑与排查:这份老代码最容易翻车的五个地方
4.1 现象:切到某些月份日历整体错位一格
原因:GetStartDay返回的星期索引可能为负。startday0001 = -32768加上累计天数后,对 7 取模在 Java 里保留负号,比如得到-3,PrintMonthBody里for (int i = startday, ...)从-3开始索引ShowDays[-3],要么越界要么把日期填到错误位置。
解决:取模后补正,int start = (int)((totalnumofdays + startday0001) % 7); if (start < 0) start += 7;。改完再验证 1 月、2 月、闰年 2 月三个边界。
4.2 现象:点击月初空白格程序抛 NumberFormatException
原因:mouseClicked里对被点格子直接Integer.parseInt(ShowDays[i].getText()),而月初月末的填充格文本是空串。
解决:解析前判空,String t = ShowDays[i].getText(); if (t == null || t.isEmpty()) return;。这是手写日历网格的通病,凡是"固定格子数 + 动态填充"的结构都要做这个防御。
4.3 现象:Save 后提示成功,但重新打开程序读不到日记
原因:key在构造函数里只初始化一次,之后切换月份、点击日期都没有同步更新key。用户点了 6 月 25 日,day变了但key还是构造时的值,写文件写到了错误日期名下。
解决:把key的赋值抽成方法updateKey(),在mouseClicked更新day之后、WriteRecord和ReadRecord开头都调用一次。这是状态变量与显示不同步的典型,凡是"显示状态"和"持久化键"分离的设计都要警惕。
4.4 现象:中文日记保存后变乱码
原因:FileWriter使用平台默认编码,Windows 中文环境是 GBK,而源码文件本身可能是 UTF-8,读写编码不一致。
解决:显式指定编码,new FileWriter(myfile, StandardCharsets.UTF_8)(JDK 11+)或new OutputStreamWriter(new FileOutputStream(myfile), "UTF-8")。读的时候对称处理,别一边 UTF-8 写一边默认编码读。
4.5 现象:窗口能开但按钮全是空白方块
原因:ImageIcon引用的../images/*.gif不存在,Swing 静默降级为空图标。
解决:要么补齐图片资源,要么把new ImageIcon(...)换成纯文字按钮。排查时可以在ImageIcon构造后打印icon.getIconWidth(),返回-1就是没加载到。
5. 把它改造成能交差的版本:三个低成本增强
原码能跑,但离"拿得出手"还差几步。我一般会做三处增强,每处改动都不超过二十行,却能显著提升演示效果。
第一处是补上"今天"高亮的正确性。原码PrintMonthBody里用if (n == day)判断高亮,但day是构造时传入的固定值,切换月份后高亮还停在旧日期。改成在mouseClicked里更新day后重新调用PrintMonth(year, month, day)重绘,高亮就跟手了。重绘前记得先把 42 个格子setText("")清空,否则上个月的残留数字会叠在新月份上——这是GridLayout复用组件的经典坑。
第二处是给月份下拉框加ActionListener。原码MonthCombobox创建后没挂监听,选了月份不触发任何动作,必须再点 Query 按钮才生效。补一行MonthCombobox.addActionListener(this),在actionPerformed里判断来源是下拉框时直接读getSelectedIndex() + 1刷新日历,交互立刻顺滑。
第三处是文件命名从英文月份改数字。key里用SwitchMonth得到2012_Jun_25,跨语言环境排序和检索都别扭。改成String.format("%04d_%02d_%02d", year, month, day)得到2012_06_25,字典序即时间序,后面想按日期遍历日记文件直接Arrays.sort就行。
| 增强点 | 改动位置 | 收益 |
|---|---|---|
| 高亮跟随点击 | mouseClicked+PrintMonth | 演示时点击即高亮,直观 |
| 下拉框即时切换 | 构造函数 +actionPerformed | 少点一次 Query |
| 文件名数字化 | key赋值处 | 可排序、可批量处理 |
改完这三处,再跑一遍 1 月、2 月、12 月、闰年 2 月四个边界,确认月初空格、月末补位、跨年切换都正常,这份源码就从"能跑"变成"能讲"了。血泪经验是:每次改完PrintMonthBody相关逻辑,一定手动翻到 2 月和 12 月各点几下,这两个月份最容易暴露取模和边界问题。从那以后我每次碰日历类代码,都强制把"闰年 2 月 + 12 月跨年 + 月初空格点击"这三条走一遍再收工。希望帮到你。
本文还有配套的精品资源,点击获取