Java万年历记事本课程设计:单文件Swing实现与避坑指南
发布时间:2026/10/28 22:02:02 锦皓数字建站

简介这份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放带滚动条的JTextAreaRightSouth放 Save 和 Delete 两个按钮。7×7 网格是这套界面的核心技巧第一行七个JLabel放SUN到SAT剩下 42 个JTextField正好覆盖任意月份最多 6 周、每周 7 天的排布。42 这个数字不是随便定的一个月最多 31 天加上月初最多 6 个空位6×742 刚好兜住。每个日期格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行数固定 71 行标题 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 -32768GetTotalNumOfDays从-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每个格子都注册了MouseListenermousePressed里根据点击的格子反推是几号更新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我当年第一次交课设时也全踩过演示时删除按钮在别人机器上没反应当场冷汗。从那以后我每次交东西前都强制走一遍“换一台机器、换一个目录、输一个非法值”的三换测试把路径依赖和异常分支提前暴露。希望这套拆解能帮到你照着结构改、照着检查表验交一份自己讲得清楚的方案。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。