简介:这份JAVA万年历源码面向Java初学者与课程设计实践者,以Swing构建图形界面,将万年历显示与记事本功能合为一体,帮助读者在完整可运行的项目中理解日期时间处理、界面布局与事件驱动编程。源码围绕Calendar与java.time的日期操作展开,涵盖闰年判断、月份起始天与当月天数计算、月份下拉切换、日记的写入读取与删除确认,以及ActionListener、MouseListener等事件响应机制,并涉及文件读写与异常处理,适合作为课程设计参考或Swing入门练手素材。资源包共1个文件,为doc格式的课程设计报告,约317KB,内含总体设计、类与方法说明、运行效果描述及完整源代码,便于对照理解程序结构与实现思路。目前已有185人学习,可帮助读者快速把握项目组织方式与功能拆解逻辑。
1. 从一份单文件课设说起:万年历记事本要解决的真实需求
课程设计最怕的不是功能多,而是功能散。很多同学一上来就想做“大而全”的桌面应用,结果两周下来界面没搭完,日期逻辑还错着。这份 JAVA万年历源码 走的是另一条路:一个calendar类、一个JFrame、左右分栏,左边万年历、右边记事本,把“看日期”和“记事情”这两件最日常的事绑在一起。它面向的是软件技术专业课程设计场景,目标很明确——用 Swing 把日期计算、事件分发、文件读写串成一条能跑通的链路,而不是堆砌框架。对正在找 Java 课设参考、想交一份结构完整方案的人来说,这套源码的价值不在代码量,而在于它把“日期算法 + 界面布局 + 本地存储”三件事压进了一个可讲清楚的最小闭环里。
2. 总体设计:左右分栏与单类职责的取舍
2.1 为什么把日历和记事本放进同一个类
这份源码只有一个calendar类,继承JFrame,同时实现ActionListener和MouseListener。从工程角度看,单类承担了界面构建、事件响应、日期计算、文件读写四类职责,耦合度偏高;但从课设交付角度看,它降低了编译和运行门槛——一个.java文件就能跑,不需要额外配置包结构。我一般会跟做课设的同学说:先保证能跑通、能讲清,再谈拆分。这个项目的取舍逻辑是“可演示优先”,所有面板、按钮、标签都作为成员变量持有,事件回调里直接访问,省去了大量传参。
界面骨架用JSplitPane做水平分割,右侧是记事本区RightPane,左侧是日历区LeftPane。左侧再分三层:LeftNorth放年份输入框、月份下拉框和查询按钮;LeftCenter用GridLayout(7,7)铺 7 个星期标题加 42 个日期格子;LeftPane底部放“回到今天”按钮。右侧用BorderLayout,北边是当前选中日期的标题标签,中间是带滚动条的JTextArea,南边是保存和删除按钮。这种布局的好处是职责边界在视觉上很清楚,答辩时指着界面就能讲模块划分。
2.2 核心成员变量与状态管理
源码里的成员变量大致分四组:日期状态(year、month、day、yearafterquery、monthafterquery)、计算中间量(startday、priormonth、prioryear)、界面组件(各类JPanel、JLabel、JButton、JTextField[]、JComboBox)、以及标志位(ischange、ischange_priornext)。日期格子用JTextField[42]而不是JLabel,是为了统一设置背景色和字体,同时保留鼠标监听能力。42 个格子对应 6 行 × 7 列,足够覆盖任意月份的最大排布。
private JTextField ShowDays[] = new JTextField[42]; private JComboBox MonthCombobox; private JTextArea CenterText; private String week[] = {"SUN","MON","TUE","WED","THU","FRI","SAT"};这段声明里有两个设计点值得说。第一,ShowDays固定 42 个,避免了按月动态增删组件带来的重绘问题;第二,星期标题用字符串数组而不是枚举,是为了直接塞进JLabel的setText,减少类型转换。参数上,week数组顺序决定了日历列的顺序,如果要做周一开头的日历,改这个数组和GetStartDay的偏移量即可,不需要动布局代码。
2.3 日期计算链路的三个方法
万年历的核心不是界面,是“某年某月第一天是星期几”。源码用IsLeapYear、GetTotalNumOfDays、GetStartDay三个方法串起这条链路。IsLeapYear按“能被 400 整除,或能被 4 整除但不能被 100 整除”判断;GetTotalNumOfDays从基准年累加到目标年,再累加目标月之前的月份天数;GetStartDay把总天数对 7 取模得到起始星期。这里源码用了一个startday0001 = -32768的偏移量,属于历史遗留写法,实际含义是把基准点对齐到某个已知星期。
public boolean IsLeapYear(int year) { return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0); } 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; }GetNumOfDaysInMonth用了一串if而不是数组查表,可读性一般,但胜在直白,答辩时不容易被问住。真正需要注意的是GetTotalNumOfDays的循环起点:源码从-32767开始累加,这个范围对课设演示够用,但如果要做公元前的日期或者极端年份,循环次数会很大。通常的做法是改用java.time.LocalDate的lengthOfMonth()和getDayOfWeek(),一行就能替代整条链路,但那就偏离了“手写日期算法”的课设初衷。我的建议是:课设阶段保留手写逻辑,讲清楚原理;如果后续要扩展成正式工具,再替换成java.time。
3. 模块实现:日历渲染、记事本存储与事件分发
3.1 月份切换与 42 格渲染逻辑
日历渲染的入口是PrintMonth(year, month, day),它先算出startday和当月天数dayinmonth,再调用PrintMonthBody填充格子。填充规则是:从startday开始,依次写入 1 到dayinmonth;startday之前的格子清空,startday + dayinmonth之后的格子也清空。当前选中日期day用绿色、加粗、大字号高亮,其余日期用白色小字,背景统一深灰。
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); } }这段代码有两个容易翻车的细节。第一,setHorizontalAlignment(0)里的 0 对应SwingConstants.CENTER,直接写数字虽然能跑,但可读性差,建议改成常量。第二,清空格子时只清setText和背景色,没有重置字体和前景色,如果之前某个格子被高亮过,复用时可能残留绿色字体。稳妥的做法是在清空循环里一并setForeground(Color.white)和setFont复位。月份切换通过MonthCombobox的ActionListener触发,取getSelectedIndex() + 1作为月份,再调用PrintMonth重绘。年份查询则读YearText的文本,Integer.parseInt后更新year,同样重绘。
3.2 记事本的文件命名与读写策略
记事本的存储设计很朴素:在程序运行目录下建一个Diary文件夹,每个日期对应一个.txt文件,文件名由key决定。key的拼接方式是year + "_" + SwitchMonth + "_" + day,其中SwitchMonth是英文月份缩写。这个设计的好处是文件名自带日期语义,用户直接去文件夹里也能看懂;坏处是依赖英文缩写,如果SwitchMonth方法里某个case写错(源码里 10 月写成了"Qct"),文件名就会跟着错。
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()); } }写文件用FileWriter直接覆盖,读文件用FileReader逐字符拼接。这里有个课设里高频出现的坑:ReadRecord里先new FileReader(myfile)再判断myfile.exists(),如果文件不存在,构造FileReader时就会抛FileNotFoundException,后面的exists判断根本走不到。正确的顺序是先exists再构造流。另外,删除文件源码用了Runtime.exec("cmd /c del ..."),这在跨平台场景下直接失效,而且路径里有空格就会断。更稳的写法是myfile.delete(),一行搞定,还不用处理IOException。
3.3 事件监听的分发与鼠标选中日期
整个类实现了ActionListener和MouseListener,所有按钮通过addActionListener(this)注册,日期格子通过addMouseListener(this)注册。actionPerformed里根据getActionCommand()或事件源区分动作:Save、Delete、Query、prior、next、ToToday。鼠标事件里真正有用的是mouseClicked,它从被点击的JTextField里取文本作为day,更新右侧标题标签和key,然后调用ReadRecord尝试加载当天日记。
public void mouseClicked(MouseEvent e) { JTextField cell = (JTextField) e.getSource(); String text = cell.getText(); if (text == null || text.isEmpty()) return; day = Integer.parseInt(text); key = year + "_" + SwitchMonth + "_" + day; NorthMonthDayYear.setText(">>" + year + "," + SwitchMonth + "," + day + "<<"); ReadRecord(); }这段逻辑的关键在于key的重算时机:必须在读取日记之前更新,否则读到的还是上一天的记录。另一个细节是空格子直接return,避免parseInt抛NumberFormatException。事件分发用getActionCommand而不是e.getSource() == button,好处是同一个监听器可以处理多个按钮,坏处是命令字符串写错时编译期发现不了,只能运行时排查。我一般会在注册时统一用常量定义命令名,减少拼写错误。
4. 避坑与排查:课设里最容易翻车的五个点
4.1 日期格子残留高亮
现象是切换月份后,某些本该空白的格子还留着上个月的绿色数字或粗体。原因在PrintMonthBody的清空循环只重置了文本和背景,没有重置前景色和字体。解决方式是在两个清空循环里补上setForeground(Color.white)和setFont(new Font("TimesRoman", Font.BOLD, 12)),保证每个格子每次渲染前都回到初始样式。
4.2 文件读取顺序导致异常吞掉
现象是第一次打开某天日记时,控制台没有报错,但界面也没提示“无记录”。原因是ReadRecord先构造FileReader再判断文件是否存在,文件不存在时直接进catch,而catch里只写了CenterText.setText("Today has not logs."),把真实异常信息丢了。解决方式是先myfile.exists()判断,不存在就直接提示并返回;存在再构造流,catch里打印ex.getMessage()方便排查。
4.3 删除文件依赖系统命令
现象是在非 Windows 环境或者路径含空格时,删除按钮点了没反应。原因是Runtime.exec("cmd /c del " + filepath)把平台和路径都写死了。解决方式是改用File.delete(),返回布尔值判断成功与否,再弹对应提示。这个改动同时去掉了对IOException的依赖,代码更短也更稳。
4.4 月份缩写拼写错误
现象是 10 月的日记存成了Qct而不是Oct,导致按日期找文件时对不上。原因是SwitchMonth的switch里手写字符串,case 10写成了"Qct"。解决方式是改用String[] months = {"Jan","Feb",...}数组查表,或者直接用java.time.Month.of(month).name()取前三位,从根上消除手写拼写风险。
4.5 年份输入非数字导致崩溃
现象是在年份输入框里敲了字母再点查询,程序直接抛异常。原因是Integer.parseInt(YearText.getText())没有做校验。解决方式是在解析前用try-catch包住NumberFormatException,捕获后弹提示并把输入框重置为当前年份。更细一点的做法是用JFormattedTextField限制只能输入数字,但课设阶段加个try-catch就够交差了。
5. 验收与答辩:怎么把这份源码讲成一份完整方案
验收这份课设,我一般不看代码行数,看三件事能不能当场演示:日历能不能正确翻月翻年、闰年二月是不是 29 天、日记能不能存了再读回来。答辩前建议自己走一遍固定用例:选 2024 年 2 月看是不是 29 天,选 2023 年 2 月看是不是 28 天,在 1 月 1 日和 12 月 31 日各存一条日记再切走切回,确认能读出来。这三组用例覆盖了日期算法、边界月份和文件读写三条主链路,演示时按顺序点,节奏很稳。
讲的时候按“需求—设计—实现—验证”四段走,每段控制在两分钟内。需求段说清楚“为什么把日历和记事本放一起”——看日期和记事情是同一个场景下的连续动作,分开两个工具反而多一步切换。设计段指着界面讲JSplitPane的左右分栏和GridLayout(7,7)的 42 格,说明为什么固定 42 格而不是动态生成。实现段挑GetStartDay和WriteRecord两个方法展开,前者讲日期链路,后者讲文件命名规则。验证段直接跑上面三组用例,跑完补一句“删除功能我改成了File.delete(),因为原来的系统命令跨平台会失效”,这一句能体现你做过排查,比背代码管用。
| 验收项 | 操作 | 预期结果 |
|---|---|---|
| 闰年二月 | 查询 2024 年 2 月 | 显示 29 天,2 月 29 日可选中 |
| 平年二月 | 查询 2023 年 2 月 | 显示 28 天,3 月 1 日前无残留 |
| 跨月切换 | 连续点上一月/下一月 | 标题、格子、选中日期同步更新 |
| 日记存取 | 选中某日写入内容并保存,切走再切回 | 内容原样读回 |
| 删除确认 | 点删除并确认 | 文件消失,界面提示删除成功 |
答辩时被问到“为什么不用java.time”,别慌,就答“课设要求手写日期算法,java.time我了解,LocalDate.lengthOfMonth()和getDayOfWeek()能替代这三个方法,但那样就练不到取模和累加了”。被问到“单类是不是太臃肿”,答“是,正式项目我会拆成CalendarPanel、DiaryPanel、DateUtil三个类,课设阶段优先保证单文件可运行”。这两个回答既承认了局限,又展示了你知道更好的做法。
从那以后我每次带课设答辩,都强制自己先按验收表跑一遍再上台,因为血泪经验是:代码能跑和演示能过是两回事,中间差的就是那几组边界用例。希望这份拆解能帮到你,把手里那份源码讲成一份站得住的方案。
本文还有配套的精品资源,点击获取