Java万年历记事本课程设计:Swing界面与日期算法实战
简介:这份JAVA万年历源码面向Java初学者与课程设计开发者,以单文件calendar类实现万年历与记事本二合一桌面应用,帮助读者在实战中掌握日期时间处理、Swing界面设计与事件驱动编程。资源包共1个doc文件,约317KB,内容为完整的Java课程设计报告,涵盖总体设计、变量表与方法表、源代码清单及运行效果说明,可对照理解calendar类的成员变量与方法职责划分。项目涉及Calendar与java.time日期操作、JComboBox月份下拉、JButton与ActionListener事件响应、JTextArea日记输入、WriteRecord与ReadRecord文件读写、IsLeapYear闰年判断、GetStartDay与GetNumOfDaysInMonth日期计算,以及FlowLayout、BorderLayout等布局管理器的组合使用,并包含保存、删除确认对话框等异常处理细节。目前已有184人学习,适合需要课程设计参考、想系统梳理Java GUI与文件操作知识点的读者借鉴其代码组织与实现思路。
1. 从一份课程设计需求说起:为什么“万年历+记事本”是Java桌面开发的经典命题
每年到了学期中后段,总有不少同学在找一个能交得出手的 Java 课程设计。要求通常不复杂:有界面、能交互、有数据落地、代码量适中,最好还能体现对 Java 核心知识点的综合运用。但真正动手时才会发现,单纯做一个“能显示日期的窗口”太单薄,做一个“纯文本编辑器”又和日期没关系。于是“万年历记事本”这个组合命题就反复出现——它把日期计算、Swing 界面、事件驱动、文件读写四条线拧在一起,刚好够一个完整的课程设计体量。
这份 JAVA 万年历源码 就是围绕这个命题展开的。它用单个calendar类承载全部逻辑,左侧是万年历面板,右侧是日记编辑区,选中某一天就能写日记、保存、读取、删除。适合正在做 Java 课程设计、需要一份结构清晰且能跑通的参考实现的同学,也适合想快速回顾 Swing 事件模型和日期算法的开发者。下面按“需求→设计→实现→避坑→验收”的顺序,把这份源码拆开讲透。
2. 总体架构设计:单类承载与左右分栏的职责划分
2.1 为什么选择单文件单类结构
这份源码最显眼的特征是:整个系统只有一个calendar类,继承JFrame,同时实现ActionListener和MouseListener。从工程角度看,这当然不是最优解,但对于课程设计场景,它有明确的合理性。
单类结构意味着所有成员变量、方法、事件处理都集中在一个文件里,编译和运行只需要一条javac calendar.java加java calendar。对于需要提交源码、附设计报告、还要在答辩时快速定位代码的场合,这种结构反而降低了阅读成本。评审老师打开文件就能看到完整的变量表和方法表,不需要在多个包之间跳转。
当然,代价是类会变得很长。源码里成员变量接近三十个,方法也有二十多个。如果不做分区注释,后期维护会很痛苦。我一般会建议在类内部用注释块把变量按“日历状态”“界面组件”“文件路径”三组分开,方法按“初始化”“日期计算”“界面刷新”“文件操作”“事件响应”五组排列。这样即使单文件,也能保持可读性。
2.2 左右面板的布局与组件职责
界面采用JSplitPane水平分割,右侧放日历面板RightPane,左侧放记事本面板LeftPane。注意源码里分割顺序是RightPane在前、LeftPane在后,所以视觉上日历在左、记事本在右,和标题描述一致。
日历面板LeftPane用BorderLayout组织:北部LeftNorth放查询区,中部LeftCenter放日期网格,南部放“回到今天”按钮。查询区内部再用GridLayout(3,1)分成三行——第一行提示文字,第二行年份输入框加月份下拉框加查询按钮,第三行上一月按钮、当前年月标签、下一月按钮。
日期网格是GridLayout(7,7),第一行是星期标题SUN到SAT,后面 42 个JTextField用来显示日期数字。为什么是 42 而不是 31?因为一个月最多跨 6 周,6×7=42 刚好覆盖所有可能的月份排列。每个日期格都注册了MouseListener,点击后触发日记读取。
记事本面板RightPane同样用BorderLayout:北部显示当前选中的年月日,中部是JTextArea加JScrollPane,南部是保存和删除按钮。当前选中的日期会拼成一个key,格式是年_月_日,比如2012_Jun_25,这个 key 直接作为日记文件名。
提示:
JSplitPane的分割条位置默认按组件首选大小分配,如果发现左右比例不理想,可以在setDividerLocation()里指定像素值,或者在窗口setSize()之后调用。
2.3 日期计算的核心算法链路
日历显示的核心是三个方法:IsLeapYear、GetNumOfDaysInMonth、GetStartDay。调用链是PrintMonth→GetStartDay+GetNumOfDaysInMonth→PrintMonthBody。
IsLeapYear的判断规则是标准的:能被 400 整除,或者能被 4 整除但不能被 100 整除。源码里写成(year%400==0)||(year%4==0&&year%100!=0),逻辑正确。
GetNumOfDaysInMonth用一串if判断月份:1、3、5、7、8、10、12 月返回 31 天,4、6、9、11 月返回 30 天,2 月根据闰年返回 29 或 28 天。这里没有用switch,也没有用数组,写法比较直白,但功能没问题。
GetStartDay是最容易出问题的地方。源码里的做法是先算从某个基准年到目标年的总天数,再加上目标年从 1 月到目标月的天数,最后对 7 取模。基准年用的是-32767,起始偏移量是-32768。这套算法能跑通,但基准值的选择比较“玄学”,换一个基准年就需要重新推导偏移量。
更稳妥的做法是用java.util.Calendar直接获取:
// 用 Calendar 获取某月第一天的星期偏移 public int getStartDay(int year, int month) { Calendar cal = Calendar.getInstance(); cal.set(year, month - 1, 1); // Calendar 月份从 0 开始 // Calendar.DAY_OF_WEEK 返回 1=周日 到 7=周六 // 转换为 0=周日 到 6=周六 的偏移 return cal.get(Calendar.DAY_OF_WEEK) - 1; }这样就不需要手动累加天数,也不用担心基准年偏移量算错。源码里保留手动算法可以作为算法练习,但实际项目里用Calendar更省心。
2.4 日记数据的文件存储方案
日记存储用的是最朴素的FileWriter和FileReader,没有用缓冲流,也没有用序列化。保存时先Diary.mkdir()创建目录,然后以Diary\年_月_日.txt为路径写入。读取时先判断文件是否存在,存在则弹出确认框询问是否读取,用户点“是”才把内容加载到文本区。
删除操作比较特殊,源码里用的是Runtime.getRuntime().exec("cmd /c del " + filepath)。这在 Windows 上能跑,但跨平台会直接翻车。更通用的做法是用File.delete():
// 跨平台的删除方式 File myfile = new File("Diary/" + key + ".txt"); if (myfile.exists()) { boolean deleted = myfile.delete(); if (deleted) { JOptionPane.showMessageDialog(this, "Delete successed!"); CenterText.setText("Today has not logs."); } }另外,文件路径里的反斜杠\在 Windows 上没问题,但在 Linux 或 macOS 上需要改成/。用File.separator或者直接写/(Java 在 Windows 上也能识别/)会更安全。
3. 关键模块实现:从月份切换到事件响应的完整链路
3.1 月份切换与界面刷新
月份切换有两个入口:下拉框选择和上一月/下一月按钮。下拉框的ActionListener会读取MonthCombobox.getSelectedIndex()+1得到月份,年份从YearText解析。按钮则通过setActionCommand("prior")和setActionCommand("next")区分方向。
切换月份后需要做三件事:更新SwitchMonth字符串(英文缩写)、更新ShowDate标签显示、重新调用PrintMonth刷新日期网格。源码里SwitchMonth方法用switch把 1 到 12 映射成Jan到Dec,注意case 10写成了Qct,这是一个笔误,实际显示会变成Qct而不是Oct。
刷新日期网格时,PrintMonthBody会先清空所有 42 个格子,然后从startday位置开始填入 1 到dayinmonth。如果当前日期等于正在显示的日期,格子会变成绿色大字体加暗灰背景,作为高亮。这个高亮逻辑在PrintMonthBody的两个重载版本里都有,区别只是是否传入day参数。
注意:切换月份时如果不清空旧数据,上个月的日期数字会残留在格子里。源码里用两个
for循环分别清空startday之前和startday+dayinmonth之后的格子,中间部分被新数字覆盖,所以不会残留。
3.2 鼠标点击日期与日记联动
每个日期格子ShowDays[i]都注册了MouseListener。mouseClicked方法里会读取被点击格子的文本,解析成日期,然后更新key和NorthMonthDayYear标签,最后调用ReadRecord尝试加载对应日记。
这里有一个细节:ShowDays数组是JTextField,虽然setEditable(false)禁止了键盘输入,但鼠标点击仍然会触发事件。源码里没有区分左键和右键,也没有判断格子是否为空。如果点到空白格子,getText()返回空字符串,Integer.parseInt会抛NumberFormatException。稳妥的做法是在解析前先判断文本是否为空:
public void mouseClicked(MouseEvent e) { JTextField clickedField = (JTextField) e.getSource(); String dayText = clickedField.getText().trim(); if (dayText.isEmpty()) { return; // 空白格子不处理 } try { int selectedDay = Integer.parseInt(dayText); // 更新 key 和标签,加载日记 key = year + "_" + SwitchMonth + "_" + selectedDay; NorthMonthDayYear.setText(">>" + year + "," + SwitchMonth + "," + selectedDay + "<<"); ReadRecord(); } catch (NumberFormatException ex) { // 理论上不会走到这里,但保留兜底 } }另外,ReadRecord里弹出确认框询问是否读取,这个交互在频繁切换日期时会很烦。如果一天要翻好几个日期,每次都要点“是”,体验很差。我一般会改成直接加载,或者加一个“不再询问”的复选框。
3.3 保存与删除的异常处理
保存日记的WriteRecord方法用FileWriter逐字符写入。源码里先把CenterText.getText()转成char[],然后循环Record.write(contentarr[i])。这种逐字符写入效率很低,直接Record.write(content)就行。异常处理只有一个空的catch(IOException ex){},这意味着如果磁盘满了或者没有写权限,用户看不到任何提示,只会发现“保存成功”的弹窗没出现。
更合理的写法是:
public void WriteRecord() { String content = CenterText.getText(); try { File diaryDir = new File("Diary"); if (!diaryDir.exists()) { diaryDir.mkdir(); } File myfile = new File(diaryDir, key + ".txt"); 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(), "Error", JOptionPane.ERROR_MESSAGE); } }删除操作除了前面提到的跨平台问题,还有一个隐患:Runtime.exec执行cmd /c del时,如果文件路径包含空格或特殊字符,命令会解析失败。用File.delete()就没有这个问题。
3.4 主函数与程序入口
Main方法是整个程序的入口。源码里主函数应该是在最后调用的,创建calendar实例并设置窗口属性。典型的写法是:
public static void main(String[] args) { // 获取当前日期作为初始显示 Calendar now = Calendar.getInstance(); int year = now.get(Calendar.YEAR); int month = now.get(Calendar.MONTH) + 1; // Calendar 月份从 0 开始 int day = now.get(Calendar.DAY_OF_MONTH); calendar frame = new calendar(year, month, day); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(700, 500); frame.setVisible(true); }注意calendar类的构造函数接收年、月、日三个参数,并在构造函数里完成界面初始化和首次PrintMonth调用。这意味着窗口一打开就会显示当前月份的日历,并且当前日期会被高亮。
提示:如果希望程序启动时自动加载今天的日记,可以在构造函数末尾调用一次
ReadRecord(),但要注意此时key必须已经初始化。
4. 避坑与常见问题:日期计算、文件路径和事件冲突
4.1 日期偏移算错导致星期对不上
现象:日历显示 1 号是星期三,但实际应该是星期五,整体错位两天。
原因:GetStartDay里的基准年和偏移量不匹配。源码用-32767作为起始年、-32768作为起始偏移,这套值能跑通是因为作者可能手工验证过。但如果换一个基准年,或者闰年判断在负数年份上出问题,偏移就会错。
解决:直接用Calendar获取DAY_OF_WEEK,或者用java.time.LocalDate的getDayOfWeek().getValue()。如果必须手算,用 1900 年 1 月 1 日(星期一)作为基准,偏移量设为 1,这样更容易验证。
4.2 文件路径反斜杠在非 Windows 系统上失效
现象:在 macOS 或 Linux 上运行,保存日记时报FileNotFoundException,或者日记文件出现在奇怪的位置。
原因:源码里硬编码了Diary\和cmd /c del,这些是 Windows 专属写法。
解决:用File.separator或者直接写/。删除用File.delete()替代Runtime.exec。如果确实需要调用系统命令,先判断System.getProperty("os.name")。
4.3 点击空白日期格子抛出数字解析异常
现象:点击日历里没有日期的空白格子,控制台报NumberFormatException,程序可能卡住或弹窗报错。
原因:mouseClicked里直接Integer.parseInt(clickedField.getText()),没有判断文本是否为空。
解决:解析前先trim()并判断isEmpty(),空白格子直接return。同时给mouseClicked加try-catch兜底。
4.4 月份下拉框和年份输入框状态不同步
现象:用下拉框选了 6 月,年份输入框还是旧年份,点击查询后显示的日历和预期不一致。
原因:下拉框的ActionListener和年份输入框的ActionListener各自独立更新状态,没有统一的刷新入口。
解决:抽一个refreshCalendar()方法,从YearText和MonthCombobox读取当前值,统一更新year、month、SwitchMonth,然后调用PrintMonth。所有触发点都调这个方法,避免状态分散。
4.5 保存后切换日期再切回来,日记内容没有重新加载
现象:写完日记保存,点到别的日期,再点回来,文本区还是之前的内容,没有从文件重新读取。
原因:ReadRecord只在mouseClicked里调用,但保存后没有清空文本区,也没有强制重新读取。
解决:保存成功后可以选择清空文本区,或者在mouseClicked里无条件调用ReadRecord,让文件内容覆盖当前显示。如果文件不存在,就清空文本区并提示“今天还没有日记”。
5. 验收与答辩:怎么证明这套万年历真的能跑
5.1 功能验收清单
答辩时老师不会只看界面,通常会让你现场操作几个关键路径。下面这张表可以作为自检清单,每项都跑一遍再上台。
| 验收项 | 操作步骤 | 预期结果 |
|---|---|---|
| 启动显示当前月 | 运行程序 | 日历显示当前年月,当天日期高亮 |
| 月份切换 | 下拉框选 2 月,再点下一月 | 2 月天数正确,闰年 29 天,平年 28 天 |
| 年份切换 | 输入 2000 和 1900 分别查询 | 2000 年 2 月 29 天,1900 年 2 月 28 天 |
| 日期点击 | 点击任意日期格子 | 右侧标签更新,文本区加载对应日记或提示无日记 |
| 保存日记 | 输入文字,点 Save | 弹出保存成功,Diary目录下生成对应 txt 文件 |
| 读取日记 | 切换到其他日期再切回 | 文本区显示之前保存的内容 |
| 删除日记 | 点 Delete,确认删除 | 文件被删除,文本区清空,提示删除成功 |
| 回到今天 | 点 Go to today | 日历跳回当前月,当天高亮 |
5.2 答辩时怎么讲清楚设计思路
答辩陈述通常只有五到八分钟,不要逐行念代码。我一般会按这个顺序讲:先讲需求——为什么做万年历加记事本,它解决了什么问题;再讲总体结构——单类继承JFrame,左右分栏,JSplitPane分割;然后讲核心算法——闰年判断、月份天数、起始星期计算;接着讲数据落地——日记按日期命名存成 txt,用FileWriter和FileReader读写;最后讲事件驱动——ActionListener处理按钮,MouseListener处理日期点击。
如果老师问“为什么不用java.time”,可以回答:课程设计的时间点较早,Calendar是当时的主流选择,而且手动实现日期计算更能体现算法理解。如果问“单类结构有什么缺点”,可以承认扩展性差,但强调课程设计场景下便于提交和阅读。
5.3 我踩过的一个坑
当年我第一次做类似项目时,把日期格子的MouseListener注册在了JPanel上而不是每个JTextField上,结果点击任何位置都触发同一个事件,根本分不清点的是哪一天。后来改成给每个格子单独注册监听器,并在事件里用e.getSource()反查是哪个格子,才解决了问题。从那以后我每次做网格类界面,都强制走一遍“每个格子独立注册、事件里反查来源”的流程,再也没翻过车。
希望这份拆解能帮你把课程设计顺利交出去。源码里的日期算法和文件操作虽然朴素,但把这几条链路跑通,Java 桌面开发的基本功就算扎实了。
本文还有配套的精品资源,点击获取