☰
Java万年历记事本课程设计:单文件Swing实现与避坑指南
2026/10/28 22:01:56 网站建设 项目流程

简介:这份JAVA万年历源码是一份面向Java初学者与课程设计学习者的完整项目资料,以万年历记事本为案例,串联日期时间处理、Swing图形界面设计、事件监听、文件读写与异常处理等核心知识点,适合用于课程设计参考、Swing入门练习或巩固java.util.Calendar与java.time的实战场景。压缩包内共1个doc文件,约317KB,以课程设计报告形式呈现,包含总体设计、类与方法说明、运行效果描述及完整源代码,便于对照理解程序结构与实现思路。目前已有185人学习下载。读者可从中获取日历界面布局、月份下拉选择、闰年判断、当月天数计算、日记保存与删除等模块的具体实现,并参考报告中的变量表、方法表梳理代码组织方式,对完成同类Java桌面项目与撰写设计文档均有直接帮助。

1. 万年历记事本:一个单文件 Java 桌面程序要解决的真实需求

课程设计里最容易被低估的题目,往往不是算法题,而是这种“看起来简单、做起来到处是坑”的桌面小系统。万年历记事本就是一个典型:左边日历要能翻月翻年、闰年二月不能错、星期对齐不能歪;右边记事本要能按“年_月_日”这个键把内容落到磁盘,下次点回同一天还能读出来。它面向的是需要交一份完整 Java 课程设计的学生,也适合想练手 Swing 事件驱动与文件读写的入门开发者。整套方案只有一个calendar类,继承JFrame并实现ActionListener、MouseListener,用BorderLayout、GridLayout、FlowLayout拼出左右分栏,用JTextField[42]铺满六行七列的日期格,用JTextArea承载日记正文。单文件不等于单薄,恰恰相反,日期计算、界面刷新、事件分发、文件持久化四件事全压在一个类里,任何一处没想清楚都会在演示时翻车。下面按“需求怎么立、结构怎么分、代码怎么落、坑怎么避、答辩怎么过”的顺序,把这套源码拆成一份可以直接照着改的完整方案。

2. 总体结构设计:一个类里的四层职责与左右分栏布局

2.1 为什么单文件也要分层看

源码里只有一个calendar类,但读代码时不能真把它当成一坨。按职责拆,它其实有四层:数据层是year、month、day、startday、key这些成员变量;计算层是IsLeapYear、GetTotalNumOfDays、GetStartDay、GetNumOfDaysInMonth四个方法;界面层是LeftPane、RightPane以及挂在它们下面的标签、按钮、文本区;控制层是actionPerformed和mousePressed这类事件回调。分层不是为了好看,而是为了改的时候知道动哪里:换皮肤只碰界面层,修闰年只碰计算层,加导出功能只碰控制层加数据层。

我一般会建议学生在报告里画一张职责表,把成员变量和方法按这四层归类,答辩时老师一问“你这个类怎么组织的”,直接指表就行。源码里成员变量声明得很密,YearLabel、MonthLabel、MonthCombobox、ShowDays[]、YearText、Ask、ShowDate、Blank、TopBarTitle[]、ToToday、Query、week[]全堆在一起,初看很乱,但按“左面板控件 / 右面板控件 / 状态变量”三组重新排一遍,逻辑立刻清楚。

2.2 左右分栏与 7×7 网格的布局账

界面主干用JSplitPane把RightPane和LeftPane水平分开,左边日历、右边记事本。左面板再分三块:LeftNorth放查询区(年份输入框、月份下拉框、Query 按钮、上一月/下一月按钮、当前显示的年月标签),LeftCenter放 7×7 网格,ToToday按钮钉在BorderLayout.SOUTH。右面板同样三块:RightNorth显示“>>年,月,日<<”,RightCenter放带滚动条的JTextArea,RightSouth放 Save 和 Delete 两个按钮。

7×7 网格是这套界面的核心技巧:第一行七个JLabel放SUN到SAT,剩下 42 个JTextField正好覆盖任意月份最多 6 周、每周 7 天的排布。42 这个数字不是随便定的,一个月最多 31 天,加上月初最多 6 个空位,6×7=42 刚好兜住。每个日期格setEditable(false),只用来显示,点击靠MouseListener接管,这样既避免了用户误改日期文本,又能精确拿到点的是哪一格。

// 左面板中央:7 列标题 + 42 个日期格 LeftCenter.setLayout(new GridLayout(7, 7)); for (int i = 0; i < 7; i++) { TopBarTitle[i] = new JLabel(week[i]); // SUN..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); // 点击某天触发记事本切换 ShowDays[i].setEditable(false); // 只读,防止误改 LeftCenter.add(ShowDays[i]); }

这段代码的关键参数是GridLayout(7, 7)的两个 7:行数固定 7(1 行标题 + 6 行日期),列数固定 7(一周七天)。ShowDays数组长度必须和网格剩余格数严格一致,写成 41 或 43 都会导致GridLayout自动补格或越界。setEditable(false)之后JTextField仍然能响应鼠标事件,这是它能同时当“显示控件”和“点击热区”用的前提。

2.3 状态变量与刷新时机

源码里ischange、ischange_priornext、priormonth、prioryear、changeyearmessage、changemonthmessage这些布尔和整型变量,本质是在记录“用户到底改了年还是改了月”。因为年份是文本框输入、月份是下拉框选择,两个入口都可能触发重绘,如果不区分来源,翻月时可能把用户刚输入的年份覆盖掉。通常的做法是:Query 按钮读YearText和MonthCombobox的当前值,统一走一次PrintMonth(year, month, day);上一月/下一月按钮只改month再重绘,年份不动。把“读输入”和“改状态”分开,是这类单类程序不乱的底线。

3. 日期计算核心:闰年、起始星期与月天数的三段式实现

3.1 闰年判断是所有日期问题的根

IsLeapYear只有一行判断,但它是后面所有计算的基石:(year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)。能被 400 整除的是闰年,能被 4 整除但不能被 100 整除的也是闰年,其余不是。这个规则写错,二月天数就错,二月天数错,从三月开始每个月的起始星期全歪,整个日历直接报废。课程设计里最常见的错误是把% 100 != 0漏掉,导致 1900 年被判成闰年——虽然演示时未必翻到 1900 年,但老师随手输一个整百年就能抓出来。

public boolean IsLeapYear(int year) { // 400 整除必闰;4 整除且非 100 整除才闰 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把大月小月分开,二月单独走闰年分支,最后return 0是防御性写法,月份传进来非法时不会返回一个看似合理的数。参数year只在二月分支真正被用到,但签名里必须保留,否则闰年判断拿不到年份。

3.2 起始星期怎么算:总天数取模

GetStartDay的思路是“从某个基准日累加到目标月 1 号,总天数对 7 取模”。源码里基准写成startday0001 = -32768,GetTotalNumOfDays从-32767年开始逐年累加 365 或 366,再累加当年 1 月到目标月前一个月的天数。这个写法能跑,但基准值选得很随意,-32768这个魔数没有任何注释,换个人接手根本不知道为什么是这个数。

更稳妥的做法是把基准定在“公元 1 年 1 月 1 日是星期一”这个已知事实上,用(totalDays + 偏移) % 7得到 0 到 6 的星期索引,再和week[]数组对齐。我一般会让学生把偏移量单独提成一个常量并写注释,答辩时能讲清楚“这个数是怎么来的”,比背代码强得多。

public int GetStartDay(int year, int month) { long total = GetTotalNumOfDays(year, month); // 基准日到目标月1号的天数 // 基准日选公元1年1月1日(星期一),偏移量按实际基准调整 int offset = 1; return (int) ((total + offset) % 7); } public long GetTotalNumOfDays(int year, int month) { long total = 0; for (int i = 1; i < year; i++) { // 整年累加 total += IsLeapYear(i) ? 366 : 365; } for (int i = 1; i < month; i++) { // 当年已过月份累加 total += GetNumOfDaysInMonth(year, i); } return total; }

这里把起始年从负数改成 1,是为了让“公元 1 年”这个基准可解释;offset的具体取值取决于你把基准日定义成星期几,改完必须拿几个已知日期(比如某年某月 1 号是星期几)反推验证。GetTotalNumOfDays返回long而不是int,是因为年份跨度大时天数会超过int范围,虽然课程设计一般用不到,但类型选对能少一类玄学 bug。

3.3 把计算结果铺进 42 个格子

PrintMonthBody(startday, dayinmonth, day)干三件事:从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++, n++) { 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].setForeground(Color.white); ShowDays[i].setFont(new Font("TimesRoman", Font.BOLD, 12)); } ShowDays[i].setBackground(Color.DARK_GRAY); } 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); } }

参数startday是 0 到 6 的星期索引,dayinmonth是当月天数,day是要高亮的日。三个循环的边界都围绕startday和dayinmonth展开,任何一个传错都会出现“日期串到上一月”或“最后几天不显示”的现象。源码里这个方法有两个重载,一个带day一个不带,带day的用于初始化高亮当天,不带的用于翻月后重绘——翻月时没有“当天”概念,所以不高亮,这个设计是合理的。

4. 记事本与文件持久化:按日期键读写与删除的完整链路

4.1 key 的拼法决定了日记能不能对上号

右面板顶部标签显示">>" + year + "," + SwitchMonth + "," + day + "<<",同时把key拼成year + "_" + SwitchMonth + "_" + day。这个key就是日记文件的文件名主干,保存时写Diary\key.txt,读取时按同一个key找文件。SwitchMonth是Jan到Dec的英文缩写,用英文而不是数字,是为了让文件名可读,但也带来一个隐患:SwitchMonth方法里十月写成了"Qct",这是源码里的一个笔误,会导致十月日记存成Qct开头的文件,和显示逻辑对不上。这种错在演示时不一定暴露,但属于必须改掉的血泪经验。

public void SwitchMonth(int month) { switch (month) { case 1: SwitchMonth = "Jan"; break; case 2: SwitchMonth = "Feb"; break; // ... 3~9 月同理 case 10: SwitchMonth = "Oct"; break; // 源码误写为 Qct,必须修正 case 11: SwitchMonth = "Nov"; break; case 12: SwitchMonth = "Dec"; break; } }

key的拼法一旦定下就不能中途改,否则老日记全部读不出来。我一般会建议在类里加一个buildKey(int year, int month, int day)方法统一生成,而不是在构造函数和事件回调里各拼一次,避免两处拼法不一致。

4.2 保存、读取、删除三个动作的边界

WriteRecord先new File("Diary").mkdir()确保目录存在,再用FileWriter把CenterText的内容逐字符写出,最后弹“Save success!”。ReadRecord先判断文件是否存在,存在则弹确认框问“Today has logs, are you read?”,用户点 YES 才逐字符读进CenterText,文件不存在则把文本区设成“Today has not logs.”。DeleteFile弹确认框,确认后调Runtime.exec("cmd /c del " + filepath)删文件。

public void WriteRecord() { String content = CenterText.getText(); try { File dir = new File("Diary"); if (!dir.exists()) dir.mkdir(); // 目录不存在先建 File file = new File(dir, key + ".txt"); FileWriter writer = new FileWriter(file); writer.write(content); writer.close(); JOptionPane.showMessageDialog(this, "Save success!"); } catch (IOException ex) { JOptionPane.showMessageDialog(this, "Save failed: " + ex.getMessage()); } }

保存这段的关键是new File(dir, key + ".txt")用父目录加子文件名的构造方式,比字符串拼"Diary\\" + key + ".txt"更稳,跨平台时路径分隔符由File自己处理。异常分支里把ex.getMessage()弹出来,比源码里空catch强太多——空catch是调试阶段最大的黑匣子,文件写失败了你完全不知道是权限问题还是路径问题。

删除用Runtime.exec("cmd /c del ...")是这套源码里最该改的地方:它依赖 Windows 命令行,换系统就废,而且filepath里如果带空格还会被截断。正确做法是直接用File.delete(),返回布尔值判断成败,既跨平台又能拿到结果。

public void DeleteFile() { File file = new File("Diary", key + ".txt"); int choice = JOptionPane.showConfirmDialog(this, "Are you sure delete the file?", "Confirm", JOptionPane.YES_NO_OPTION); if (choice == JOptionPane.YES_OPTION) { if (file.exists() && file.delete()) { JOptionPane.showMessageDialog(this, "Delete successed!"); CenterText.setText("Today has not logs."); } else { JOptionPane.showMessageDialog(this, "Delete failed!"); } } }

4.3 点击日期格如何驱动记事本切换

ShowDays每个格子都注册了MouseListener,mousePressed里根据点击的格子反推是几号,更新day和key,再调ReadRecord把当天日记读出来。反推的方式通常是遍历ShowDays数组找到被点中的索引,减去startday再加 1 就是日。这里有个容易忽略的点:点到空白格(月初月末的空位)时getText()是空串,Integer.parseInt会抛异常,必须先判断非空再解析。

public void mousePressed(MouseEvent e) { for (int i = 0; i < 42; i++) { if (e.getSource() == ShowDays[i] && !ShowDays[i].getText().isEmpty()) { day = Integer.parseInt(ShowDays[i].getText()); key = year + "_" + SwitchMonth + "_" + day; // 同步更新文件键 NorthMonthDayYear.setText(">>" + year + "," + SwitchMonth + "," + day + "<<"); ReadRecord(); // 读当天日记 return; } } }

e.getSource() == ShowDays[i]用引用比较定位格子,比拿坐标算行列更直接。key必须在读文件之前更新,否则读的还是上一天的日记——这个顺序错了,现象是“点了新日期但内容没变”,很容易被当成鼠标事件没触发。

5. 避坑与排查:这类单类 Swing 程序最容易翻车的五个点

5.1 翻月后日期串月

现象是点“下一月”之后,日历上同时出现上个月末尾的日期和这个月开头的日期,或者 31 号的月份显示成 30 天。原因通常是PrintMonthBody只填了新日期,没清空上一轮残留的格子,或者GetNumOfDaysInMonth的月份分支漏了某个大月。解决办法是每次重绘前先把 42 个格子全部setText("")再填,并且用一组已知日期(比如 2024 年 2 月 29 天、2023 年 2 月 28 天)逐个月份验证天数。

5.2 日记存了读不出来

现象是保存时提示成功,点回同一天却显示“Today has not logs.”。原因多半是key在保存和读取两处拼法不一致,比如保存时用了SwitchMonth修正后的Oct,读取时某条路径还留着旧的Qct;或者Diary目录的相对路径依赖启动位置,从不同目录启动程序时文件落到了别处。解决办法是把key生成收敛到一个方法,目录用绝对路径或基于用户目录的固定路径,保存后立刻用文件管理器确认文件真的落在预期位置。

5.3 空catch吞掉所有错误

源码里WriteRecord、ReadRecord、DeleteFile多处catch (IOException ex) {}什么都不做。现象是操作没反应、没提示、也没报错,完全靠猜。原因是异常被静默吞掉,堆栈都没打印。解决办法是至少ex.printStackTrace(),更好的做法是弹窗把ex.getMessage()带出来。调试阶段我一般会临时把catch块改成弹窗加打印,确认链路通了再收回去。

5.4 删除依赖系统命令

现象是在非 Windows 环境或路径带空格时删除失败,甚至误删别的文件。原因是Runtime.exec("cmd /c del " + filepath)把路径拼进命令行,既不可移植又有注入风险。解决办法是改用File.delete(),用返回值判断成败,路径用new File(dir, name)构造。这条改动量很小,但能消掉答辩现场换机器演示时最大的不确定性。

5.5 起始星期的魔数没人讲得清

现象是日历整体错位一天或几天,但闰年、天数都对。原因是GetStartDay里的基准值-32768和偏移量没有注释,改年份范围时偏移没跟着调。解决办法是把基准日显式定义成“公元 1 年 1 月 1 日为星期一”,偏移量提成常量并写注释,再用两三个已知日期反推验证。答辩时老师问“你这个星期几怎么算的”,能把这个基准讲明白,比背公式有说服力。

6. 验收与答辩:用一张检查表把演示风险压到最低

验收这套东西,别等到答辩当天才第一次完整跑。我一般会让学生提前做一张检查表,按“日期正确性、交互完整性、文件持久化、异常提示”四组逐项过,每项都写清楚预期现象和实际现象,对不上就当场记下来改。日期组至少覆盖:平年二月 28 天、闰年二月 29 天、31 天大月、30 天小月、跨年翻月(12 月点下一月到次年 1 月)、整百年 1900 非闰和 2000 闰。交互组覆盖:年份输入框回车触发查询、月份下拉切换、上一月/下一月按钮、点日期格切换记事本、Go to today 回到当天。文件组覆盖:保存后文件真实存在、读回内容一致、删除后文件消失、删除不存在的文件有提示。异常组覆盖:年份输入非数字、点空白日期格、Diary 目录不存在时首次保存。

检查项操作预期现象常见偏差
闰年二月输入 2024 年 2 月显示 29 天显示 28 天,闰年判断漏%400
整百年输入 1900 年 2 月显示 28 天显示 29 天,漏%100!=0
跨年翻月12 月点下一月跳到次年 1 月月份变 13 或年份不变
保存读回写内容点 Save 再点同一天内容原样读回提示成功但读不到,key 不一致
删除确认点 Delete 选取消文件保留直接删除,确认框没接返回值
非法年份年份框输入字母点 Query有提示不崩溃抛NumberFormatException无提示

答辩陈述有个很实用的技巧:不要按代码顺序讲,按“用户操作顺序”讲。打开程序看到什么、点下一月发生什么、点某一天右边怎么变、写内容点保存文件落在哪、点删除怎么确认,一条线走完,老师跟着你的鼠标看,比对着代码念方法名强得多。每个操作后面补一句“这里如果出错,我会看哪个变量/哪个文件”,把排查思路带出来,显得你是真跑过而不是背稿。

代码里那些魔数、空catch、cmd /c del,我当年第一次交课设时也全踩过,演示时删除按钮在别人机器上没反应,当场冷汗。从那以后我每次交东西前都强制走一遍“换一台机器、换一个目录、输一个非法值”的三换测试,把路径依赖和异常分支提前暴露。希望这套拆解能帮到你,照着结构改、照着检查表验,交一份自己讲得清楚的方案。

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

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

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

立即咨询