简介:这份JAVA万年历源码面向Java初学者与课程设计开发者,以Swing桌面应用形式实现万年历与记事本二合一功能,帮助读者在实战中掌握日期时间处理、GUI布局与事件驱动编程。资源包内含1个doc文件,大小约317KB,为完整的Java课程设计报告,涵盖总体设计、类结构说明、成员变量与方法表、源代码及运行效果截图,并附有开发总结与心得体会。报告围绕Calendar与java.time日期API、JFrame与JComboBox等Swing组件、ActionListener与MouseListener事件处理、文件读写与异常捕获、闰年判断及月份天数计算等核心知识点展开,同时给出WriteRecord、ReadRecord、IsLeapYear等关键方法的实现思路。已有184人学习,适合需要参考课程设计结构、梳理Swing项目开发流程或准备Java实训作业的读者,可据此理解从界面搭建到数据持久化的完整实现路径。
1. 从一份课程设计说起:为什么“万年历+记事本”是检验 Java 基本功的试金石
很多同学拿到“Java 万年历源码”这个题目时,第一反应是去搜一个现成的日历控件贴上去,跑起来能看就行。但真正做过课程设计答辩的人都知道,导师最爱问的恰恰是那些“看起来简单”的地方:闰年怎么判、某个月第一天是星期几怎么算、跨年切换月份时状态怎么同步、日记文件按什么规则命名才不会互相覆盖。这套方案要解决的,就是把这些散落在Calendar、Swing、IO里的知识点,收拢成一个能跑、能讲、能验收的完整桌面应用——左边万年历、右边记事本,一个类搞定全部逻辑。它适合正在做 Java 课程设计的学生,也适合想用一个小项目把 GUI 事件驱动和文件读写串起来的入门开发者。下面我按“需求→设计→实现→避坑→验收”的顺序,把这份源码拆开讲透。
2. 需求拆解与技术选型:单类结构下如何撑起日历与记事两套逻辑
2.1 功能边界先划清楚,别让需求无限膨胀
这套系统的功能边界其实很清晰,一共就两块:一块是万年历展示与查询,一块是按日期存取的记事本。万年历部分要支持:显示当前年月、通过下拉列表切换月份、输入年份后查询、上一月/下一月翻页、一键回到今天、把当月每一天按星期排布到 7×7 的网格里,并且高亮当前选中的日期。记事本部分要支持:选中某一天后,在右侧文本区写内容、保存成文件、读取已有记录、删除记录,并且保存和删除都要有确认对话框,防止误操作。
把这两块合在一起,就产生了一个关键约束:日历选中的“某一天”必须能唯一映射到一个存储键。源码里用的是key = year + "_" + SwitchMonth + "_" + day,比如2024_Jan_15。这个设计看起来朴素,但它决定了后面文件读写、切换日期时内容刷新、删除时定位文件这一整条链路。很多同学做到一半发现“切换日期后文本区内容不跟着变”,根子就在这个 key 没有在鼠标事件里同步更新。
提示:需求阶段一定要把“选中日期”和“文本内容”的绑定关系写进设计文档,否则编码时很容易漏掉刷新逻辑。
2.2 为什么用 Swing 而不是 JavaFX,为什么坚持单类
技术选型上,这份源码用的是javax.swing全家桶:JFrame做窗口、JPanel配合BorderLayout和GridLayout做布局、JComboBox做月份下拉、JTextField数组做 42 个日期格子、JTextArea做记事区。选 Swing 的理由很实际:课程设计环境通常是 JDK 8 甚至更早,Swing 是标配,不需要额外引入模块;而且 Swing 的事件模型(ActionListener、MouseListener)足够直观,答辩时能讲清楚“谁触发、谁响应”。
坚持单类calendar则是一个有争议但符合课程设计场景的选择。单类的好处是所有成员变量和方法都在一个文件里,助教批改时不用跳来跳去,编译也简单——javac calendar.java就完事。代价是类会很长,成员变量和方法混在一起。我的建议是:如果只是交课程设计,单类没问题;如果想把它当作品集,至少把日期计算逻辑抽成一个DateUtil类,把文件读写抽成一个RecordStore类,主类只负责界面和事件分发。
2.3 日期计算的核心:不依赖 Calendar 也能算对
源码里没有直接用java.util.Calendar来算“某月第一天是星期几”,而是自己写了一套GetTotalNumOfDays+GetStartDay。这套算法的思路是:从基准点开始累加天数,再对 7 取模。核心方法如下:
// 判断闰年:能被4整除且不能被100整除,或能被400整除 public boolean IsLeapYear(int year) { if ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)) return true; else return false; } // 计算某月有多少天,2月要单独判断闰年 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) { if (IsLeapYear(year)) return 29; else return 28; } return 0; } // 累加从基准年到目标年之前的总天数,再加上目标年当月之前的天数 public long GetTotalNumOfDays(int year, int month) { long total = 0; for (int i = -32767; i < year; i++) { if (IsLeapYear(i)) total += 366; else total += 365; } for (int i = 1; i < month; i++) total += GetNumOfDaysInMonth(year, i); return total; } // 用总天数对7取模得到起始星期,基准偏移量 -32768 是源码里的经验值 public int GetStartDay(int year, int month) { int startday0001 = -32768; long totalnumofdays = GetTotalNumOfDays(year, month); return (int) ((totalnumofdays + startday0001) % 7); }这里有几个参数和逻辑需要说清楚。IsLeapYear的闰年规则是标准规则,但注意它接受负数年份也能算,因为循环从-32767开始。GetTotalNumOfDays里的-32767和GetStartDay里的-32768是一对偏移量,目的是让公元 1 年 1 月 1 日对应的星期能对齐到实际历法。这套算法在课程设计里够用,但如果你要算 1582 年以前的日期,格里高利历和儒略历的切换会导致偏差,答辩时如果被问到,可以坦诚说明“本系统按格里高利历统一处理”。
PrintMonthBody负责把算出来的startday和dayinmonth填进 42 个格子里:前startday个格子清空,中间填 1 到dayinmonth,剩下的格子也清空。当前选中的day会用绿色、大号字体、深灰背景高亮,其他日期用白色小字。这个视觉区分在答辩演示时很加分。
3. 界面布局与事件分发:左右分栏怎么搭、鼠标点击怎么联动
3.1 用 JSplitPane 做左右分栏,布局管理器各司其职
整个窗口用JSplitPane水平分割,左边放日历面板LeftPane,右边放记事面板RightPane。源码里写的是new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, RightPane, LeftPane),注意参数顺序决定了左右位置,这里 RightPane 在前所以记事本在左、日历在右,和常见习惯相反。如果你想让日历在左,把两个参数调换即可。
LeftPane内部用BorderLayout:北边LeftNorth放查询区,中间LeftCenter放日期网格,南边放“回到今天”按钮。LeftNorth又用GridLayout(3,1)分成三行:第一行提示文字,第二行年份输入框+月份下拉+查询按钮,第三行上一月按钮+当前年月标签+下一月按钮。LeftCenter用GridLayout(7,7),第一行放SUN到SAT七个标题标签,后面 42 个格子放JTextField数组ShowDays。
// 日期网格初始化:7列标题 + 42个日期格子 LeftCenter.setLayout(new GridLayout(7, 7)); for (int i = 0; i < 7; i++) { TopBarTitle[i] = new JLabel(); TopBarTitle[i].setText(week[i]); // week = {"SUN","MON",...,"SAT"} TopBarTitle[i].setHorizontalAlignment(0); TopBarTitle[i].setBorder(BorderFactory.createRaisedBevelBorder()); LeftCenter.add(TopBarTitle[i]); } for (int i = 0; i < 42; i++) { ShowDays[i] = new JTextField(); ShowDays[i].addMouseListener(this); // 当前类实现了 MouseListener ShowDays[i].setEditable(false); // 日期格子不允许直接编辑 LeftCenter.add(ShowDays[i]); }这里的关键设计是:42 个格子全部注册了MouseListener,但只有填了数字的格子才有意义。setEditable(false)防止用户直接改日期文本,所有日期变更都必须走下拉列表或翻页按钮,保证状态可控。
3.2 事件分发:ActionListener 管按钮,MouseListener 管日期选中
当前类同时实现了ActionListener和MouseListener,所以actionPerformed和mouseClicked等方法都在同一个类里。按钮类事件通过setActionCommand区分:PriorMonth设成"prior",NextMonth设成"next",SouthSave设成"Save",SouthDelete设成"Delete"。在actionPerformed里根据e.getActionCommand()分支处理。
public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); if ("prior".equals(cmd)) { // 上一月:月份减1,若小于1则年份减1、月份置12 priormonth--; if (priormonth < 1) { priormonth = 12; prioryear--; } PrintMonth(prioryear, priormonth); ShowDate.setText(SwitchMonth(priormonth) + " , " + prioryear); } else if ("next".equals(cmd)) { // 下一月:月份加1,若大于12则年份加1、月份置1 priormonth++; if (priormonth > 12) { priormonth = 1; prioryear++; } PrintMonth(prioryear, priormonth); ShowDate.setText(SwitchMonth(priormonth) + " , " + prioryear); } else if ("Save".equals(cmd)) { WriteRecord(); } else if ("Delete".equals(cmd)) { DeleteFile(); } // Query、ToToday 等分支略 }鼠标事件里,mouseClicked是核心:用户点某个日期格子后,要更新day、更新右上角标签NorthMonthDayYear、重新计算key、然后调用ReadRecord看当天有没有日记。
public void mouseClicked(MouseEvent e) { for (int i = 0; i < 42; i++) { if (e.getSource() == ShowDays[i] && !ShowDays[i].getText().equals("")) { day = Integer.parseInt(ShowDays[i].getText()); // 更新右上角日期显示 NorthMonthDayYear.setText(">>" + prioryear + "," + SwitchMonth(priormonth) + "," + day + "<<"); // 重新生成存储键 key = prioryear + "_" + SwitchMonth(priormonth) + "_" + day; // 读取当天已有日记 ReadRecord(); // 刷新高亮 PrintMonth(prioryear, priormonth, day); break; } } }这段逻辑里最容易翻车的是key的生成时机。如果你在切换月份时没有同步更新prioryear和priormonth,那么点击日期后生成的 key 就会指向错误的月份,保存的日记会“串月”。源码里用prioryear/priormonth作为当前显示月份的状态变量,year/month/day作为初始构造参数,这个区分要在答辩时讲清楚。
3.3 记事本的文件读写:按 key 命名,用 FileWriter/FileReader 落盘
日记存储用的是最朴素的方案:在程序运行目录下建一个Diary文件夹,每个日期一个.txt文件,文件名就是key。写入用FileWriter逐字符写,读取用FileReader逐字节读。
public void WriteRecord() { String content = CenterText.getText(); try { File diaryDir = new File("Diary"); if (!diaryDir.exists()) diaryDir.mkdir(); // 目录不存在则创建 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) { JOptionPane.showMessageDialog(this, "Save failed: " + ex.getMessage()); } } public void ReadRecord() { try { File myfile = new File("Diary/" + key + ".txt"); if (myfile.exists()) { int n = JOptionPane.showConfirmDialog(this, "Today has logs, are you read?", "Confirm", JOptionPane.YES_NO_OPTION); if (n == JOptionPane.YES_OPTION) { StringBuilder content = new StringBuilder(); FileReader record = new FileReader(myfile); int b; while ((b = record.read()) != -1) { content.append((char) b); } record.close(); CenterText.setText(content.toString()); } } else { CenterText.setText(""); // 没有记录则清空 } } catch (IOException ex) { CenterText.setText("Today has not logs."); } }这里有两个设计点值得注意。第一,ReadRecord里用了确认对话框,用户可以选择不读取,这样切换日期时不会强行覆盖当前正在编辑的内容——但这个逻辑有个坑:如果用户点了“否”,文本区保留的是上一天的內容,而key已经变成新日期了,此时点保存会把旧内容存到新日期下。更稳妥的做法是在切换日期前先判断文本区是否有未保存修改,或者干脆去掉确认框、直接加载。第二,删除文件用的是Runtime.exec("cmd /c del " + filepath),这在 Windows 下能跑,但跨平台会挂,而且路径里有空格时会失败。建议改成myfile.delete(),一行搞定且跨平台。
4. 避坑与常见问题排查:那些答辩时最容易被追问的细节
4.1 日期格子错位:startday 算错导致整个月偏移
现象:某个月的 1 号没有落在正确的星期列上,比如实际是周三,却显示在周二或周四。原因通常是GetStartDay的偏移量不对,或者GetTotalNumOfDays在累加月份天数时把当前月也算进去了。源码里for (int i = 1; i < month; i++)用的是< month,这是对的;如果写成<= month就会多算一个月。另外startday0001 = -32768这个值如果被改动,整个历法会整体平移。解决办法:用已知日期验证,比如 2024 年 1 月 1 日是星期一,算出来的startday应该是 1(如果 SUN=0 则应为 1)。写个main方法打印几个已知月份的startday做回归测试。
4.2 切换月份后点击日期,日记存到了错误的月份
现象:从 1 月切到 2 月,点击 2 月 5 日写日记,保存后发现文件存成了2024_Jan_5.txt。原因是key生成时用的还是旧的SwitchMonth或旧的priormonth。源码里SwitchMonth既是一个方法名又是一个String变量名,这种命名容易让人混淆。解决办法:把当前显示的年月统一用prioryear和priormonth两个 int 变量维护,key生成时只用这两个变量,不要用SwitchMonth字符串变量。每次翻页或查询后,先更新这两个变量,再刷新界面。
4.3 文件删除用 Runtime.exec,路径含空格或中文就失败
现象:日记文件放在“我的文档”这类含空格路径下,点删除没反应,或者弹“删除失败”。原因是Runtime.exec("cmd /c del " + filepath)把整个字符串当命令解析,路径里的空格会被当成参数分隔符。解决办法:直接用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(""); } else { JOptionPane.showMessageDialog(this, "Delete failed!"); } } else { JOptionPane.showMessageDialog(this, "The file doesn't exist!"); }这样既跨平台,又不受路径字符影响。
4.4 读取日记时确认框点“否”,导致内容与日期不匹配
现象:切换到有日记的日期,弹出“是否读取”,用户点“否”,文本区还留着上一天的内容,此时点保存,旧内容被存到新日期文件里。原因是ReadRecord在用户拒绝后没有清空或恢复文本区。解决办法:要么去掉确认框直接加载,要么在用户点“否”时把文本区清空并提示“已跳过加载”。我一般会在切换日期前先检查CenterText.getText()是否与当前key对应的文件内容一致,不一致就弹“是否保存当前修改”,保存完再切换。
4.5 闰年判断在负数年份或公元 0 年附近出现异常
现象:查询 0 年或负数年份时,日历显示错乱。原因是IsLeapYear对 0 年的判断:0 % 400 == 0 返回 true,但历法上公元 0 年不存在。解决办法:在GetTotalNumOfDays和GetStartDay里对年份做范围限制,比如只允许 1 到 9999 年,超出范围弹提示。课程设计里通常不会查那么早的年份,但答辩时如果被问到“你的系统支持到哪一年”,要能说出边界和原因。
5. 验收与答辩:怎么演示才能让导师点头
验收这套系统,我一般会按“功能清单逐项过 + 边界用例 + 异常操作”三步走。功能清单就是第 2 章列的那些:显示当前月、下拉切月、输入年份查询、上一月/下一月、回到今天、点击日期高亮、写日记保存、读取日记、删除日记。每项都要当场操作,不能只说“这个功能有”。边界用例重点测三个:闰年 2 月(比如 2024 年 2 月应该有 29 天)、跨年翻月(12 月点下一月应该到次年 1 月)、跨月点击日期后保存(验证 key 是否正确)。异常操作测两个:删除不存在的日记文件、在没写内容时点保存。
答辩时导师最爱问的问题通常集中在日期算法和文件存储上。比如“你怎么算某月第一天是星期几”,这时候不要背代码,直接说思路:先算从基准年到目标月的总天数,对 7 取模得到偏移量,再填格子。如果问“为什么不用 Calendar 类”,可以说“课程设计的要求是理解日期计算原理,自己实现一遍比直接调 API 更能体现对闰年、月份天数这些规则的理解;实际项目中我会用 java.time 的 LocalDate,更简洁也更安全”。如果问“日记文件多了怎么管理”,可以提“目前是按日期命名平铺在 Diary 目录下,如果量大了可以按年月建子目录,或者换成 SQLite 存”。
下面这张表可以放在答辩 PPT 里,作为验收对照:
| 验收项 | 操作步骤 | 预期结果 | 常见失败表现 |
|---|---|---|---|
| 闰年 2 月天数 | 查询 2024 年 2 月 | 显示 29 天 | 显示 28 天 |
| 跨年翻月 | 12 月点“下一月” | 跳到次年 1 月 | 月份变 13 或报错 |
| 日期选中高亮 | 点击任意日期 | 该格绿色大字 | 无变化或全部高亮 |
| 保存日记 | 写内容点 Save | 弹“Save success” | 无反应或报 IO 异常 |
| 读取日记 | 切到有记录的日期 | 弹确认框并加载 | 内容为空或串月 |
| 删除日记 | 点 Delete 确认 | 弹“Delete successed” | 文件仍在或报错 |
最后说一个我自己的习惯:每次交课程设计前,我会把程序在另一台没装过开发环境的机器上跑一遍,用java -jar或者直接javac+java命令行启动,确认不依赖 IDE 的默认工作目录。因为Diary文件夹是相对路径创建的,如果从 IDE 里跑和从命令行跑的工作目录不同,日记文件会存到不同地方,答辩演示时容易“找不到之前写的日记”。从那以后我每次做带文件存储的项目,都强制在启动时打印一次System.getProperty("user.dir"),确认工作目录符合预期。希望这套拆解能帮你把万年历课程设计稳稳落地,答辩时被追问也不慌。
本文还有配套的精品资源,点击获取