资讯详情

资讯详情

Java万年历+记事本课程设计:单类实现日历算法与文件读写

简介这份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来算“某月第一天是星期几”而是自己写了一套GetTotalNumOfDaysGetStartDay。这套算法的思路是从基准点开始累加天数再对 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设成priorNextMonth设成nextSouthSave设成SaveSouthDelete设成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如果 SUN0 则应为 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 doesnt 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或者直接javacjava命令行启动确认不依赖 IDE 的默认工作目录。因为Diary文件夹是相对路径创建的如果从 IDE 里跑和从命令行跑的工作目录不同日记文件会存到不同地方答辩演示时容易“找不到之前写的日记”。从那以后我每次做带文件存储的项目都强制在启动时打印一次System.getProperty(user.dir)确认工作目录符合预期。希望这套拆解能帮你把万年历课程设计稳稳落地答辩时被追问也不慌。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →