首页 / 资讯中心 / 文章详情

Java万年历记事本课程设计:Swing界面与日期算法实战

Java万年历记事本课程设计:Swing界面与日期算法实战 ★ FEATURED ARTICLE
简介这份JAVA万年历源码面向Java初学者与课程设计开发者以单文件calendar类实现万年历与记事本二合一桌面应用帮助读者在实战中掌握日期时间处理、Swing界面设计与事件驱动编程。资源包共1个doc文件约317KB内容为完整的Java课程设计报告涵盖总体设计、变量表与方法表、源代码清单及运行效果说明可对照理解calendar类的成员变量与方法职责划分。项目涉及Calendar与java.time日期操作、JComboBox月份下拉、JButton与ActionListener事件响应、JTextArea日记输入、WriteRecord与ReadRecord文件读写、IsLeapYear闰年判断、GetStartDay与GetNumOfDaysInMonth日期计算以及FlowLayout、BorderLayout等布局管理器的组合使用并包含保存、删除确认对话框等异常处理细节。目前已有184人学习适合需要课程设计参考、想系统梳理Java GUI与文件操作知识点的读者借鉴其代码组织与实现思路。1. 从一份课程设计需求说起为什么“万年历记事本”是Java桌面开发的经典命题每年到了学期中后段总有不少同学在找一个能交得出手的 Java 课程设计。要求通常不复杂有界面、能交互、有数据落地、代码量适中最好还能体现对 Java 核心知识点的综合运用。但真正动手时才会发现单纯做一个“能显示日期的窗口”太单薄做一个“纯文本编辑器”又和日期没关系。于是“万年历记事本”这个组合命题就反复出现——它把日期计算、Swing 界面、事件驱动、文件读写四条线拧在一起刚好够一个完整的课程设计体量。这份 JAVA 万年历源码 就是围绕这个命题展开的。它用单个calendar类承载全部逻辑左侧是万年历面板右侧是日记编辑区选中某一天就能写日记、保存、读取、删除。适合正在做 Java 课程设计、需要一份结构清晰且能跑通的参考实现的同学也适合想快速回顾 Swing 事件模型和日期算法的开发者。下面按“需求→设计→实现→避坑→验收”的顺序把这份源码拆开讲透。2. 总体架构设计单类承载与左右分栏的职责划分2.1 为什么选择单文件单类结构这份源码最显眼的特征是整个系统只有一个calendar类继承JFrame同时实现ActionListener和MouseListener。从工程角度看这当然不是最优解但对于课程设计场景它有明确的合理性。单类结构意味着所有成员变量、方法、事件处理都集中在一个文件里编译和运行只需要一条javac calendar.java加java calendar。对于需要提交源码、附设计报告、还要在答辩时快速定位代码的场合这种结构反而降低了阅读成本。评审老师打开文件就能看到完整的变量表和方法表不需要在多个包之间跳转。当然代价是类会变得很长。源码里成员变量接近三十个方法也有二十多个。如果不做分区注释后期维护会很痛苦。我一般会建议在类内部用注释块把变量按“日历状态”“界面组件”“文件路径”三组分开方法按“初始化”“日期计算”“界面刷新”“文件操作”“事件响应”五组排列。这样即使单文件也能保持可读性。2.2 左右面板的布局与组件职责界面采用JSplitPane水平分割右侧放日历面板RightPane左侧放记事本面板LeftPane。注意源码里分割顺序是RightPane在前、LeftPane在后所以视觉上日历在左、记事本在右和标题描述一致。日历面板LeftPane用BorderLayout组织北部LeftNorth放查询区中部LeftCenter放日期网格南部放“回到今天”按钮。查询区内部再用GridLayout(3,1)分成三行——第一行提示文字第二行年份输入框加月份下拉框加查询按钮第三行上一月按钮、当前年月标签、下一月按钮。日期网格是GridLayout(7,7)第一行是星期标题SUN到SAT后面 42 个JTextField用来显示日期数字。为什么是 42 而不是 31因为一个月最多跨 6 周6×742 刚好覆盖所有可能的月份排列。每个日期格都注册了MouseListener点击后触发日记读取。记事本面板RightPane同样用BorderLayout北部显示当前选中的年月日中部是JTextArea加JScrollPane南部是保存和删除按钮。当前选中的日期会拼成一个key格式是年_月_日比如2012_Jun_25这个 key 直接作为日记文件名。提示JSplitPane的分割条位置默认按组件首选大小分配如果发现左右比例不理想可以在setDividerLocation()里指定像素值或者在窗口setSize()之后调用。2.3 日期计算的核心算法链路日历显示的核心是三个方法IsLeapYear、GetNumOfDaysInMonth、GetStartDay。调用链是PrintMonth→GetStartDayGetNumOfDaysInMonth→PrintMonthBody。IsLeapYear的判断规则是标准的能被 400 整除或者能被 4 整除但不能被 100 整除。源码里写成(year%4000)||(year%40year%100!0)逻辑正确。GetNumOfDaysInMonth用一串if判断月份1、3、5、7、8、10、12 月返回 31 天4、6、9、11 月返回 30 天2 月根据闰年返回 29 或 28 天。这里没有用switch也没有用数组写法比较直白但功能没问题。GetStartDay是最容易出问题的地方。源码里的做法是先算从某个基准年到目标年的总天数再加上目标年从 1 月到目标月的天数最后对 7 取模。基准年用的是-32767起始偏移量是-32768。这套算法能跑通但基准值的选择比较“玄学”换一个基准年就需要重新推导偏移量。更稳妥的做法是用java.util.Calendar直接获取// 用 Calendar 获取某月第一天的星期偏移 public int getStartDay(int year, int month) { Calendar cal Calendar.getInstance(); cal.set(year, month - 1, 1); // Calendar 月份从 0 开始 // Calendar.DAY_OF_WEEK 返回 1周日 到 7周六 // 转换为 0周日 到 6周六 的偏移 return cal.get(Calendar.DAY_OF_WEEK) - 1; }这样就不需要手动累加天数也不用担心基准年偏移量算错。源码里保留手动算法可以作为算法练习但实际项目里用Calendar更省心。2.4 日记数据的文件存储方案日记存储用的是最朴素的FileWriter和FileReader没有用缓冲流也没有用序列化。保存时先Diary.mkdir()创建目录然后以Diary\年_月_日.txt为路径写入。读取时先判断文件是否存在存在则弹出确认框询问是否读取用户点“是”才把内容加载到文本区。删除操作比较特殊源码里用的是Runtime.getRuntime().exec(cmd /c del filepath)。这在 Windows 上能跑但跨平台会直接翻车。更通用的做法是用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(Today has not logs.); } }另外文件路径里的反斜杠\在 Windows 上没问题但在 Linux 或 macOS 上需要改成/。用File.separator或者直接写/Java 在 Windows 上也能识别/会更安全。3. 关键模块实现从月份切换到事件响应的完整链路3.1 月份切换与界面刷新月份切换有两个入口下拉框选择和上一月/下一月按钮。下拉框的ActionListener会读取MonthCombobox.getSelectedIndex()1得到月份年份从YearText解析。按钮则通过setActionCommand(prior)和setActionCommand(next)区分方向。切换月份后需要做三件事更新SwitchMonth字符串英文缩写、更新ShowDate标签显示、重新调用PrintMonth刷新日期网格。源码里SwitchMonth方法用switch把 1 到 12 映射成Jan到Dec注意case 10写成了Qct这是一个笔误实际显示会变成Qct而不是Oct。刷新日期网格时PrintMonthBody会先清空所有 42 个格子然后从startday位置开始填入 1 到dayinmonth。如果当前日期等于正在显示的日期格子会变成绿色大字体加暗灰背景作为高亮。这个高亮逻辑在PrintMonthBody的两个重载版本里都有区别只是是否传入day参数。注意切换月份时如果不清空旧数据上个月的日期数字会残留在格子里。源码里用两个for循环分别清空startday之前和startdaydayinmonth之后的格子中间部分被新数字覆盖所以不会残留。3.2 鼠标点击日期与日记联动每个日期格子ShowDays[i]都注册了MouseListener。mouseClicked方法里会读取被点击格子的文本解析成日期然后更新key和NorthMonthDayYear标签最后调用ReadRecord尝试加载对应日记。这里有一个细节ShowDays数组是JTextField虽然setEditable(false)禁止了键盘输入但鼠标点击仍然会触发事件。源码里没有区分左键和右键也没有判断格子是否为空。如果点到空白格子getText()返回空字符串Integer.parseInt会抛NumberFormatException。稳妥的做法是在解析前先判断文本是否为空public void mouseClicked(MouseEvent e) { JTextField clickedField (JTextField) e.getSource(); String dayText clickedField.getText().trim(); if (dayText.isEmpty()) { return; // 空白格子不处理 } try { int selectedDay Integer.parseInt(dayText); // 更新 key 和标签加载日记 key year _ SwitchMonth _ selectedDay; NorthMonthDayYear.setText( year , SwitchMonth , selectedDay ); ReadRecord(); } catch (NumberFormatException ex) { // 理论上不会走到这里但保留兜底 } }另外ReadRecord里弹出确认框询问是否读取这个交互在频繁切换日期时会很烦。如果一天要翻好几个日期每次都要点“是”体验很差。我一般会改成直接加载或者加一个“不再询问”的复选框。3.3 保存与删除的异常处理保存日记的WriteRecord方法用FileWriter逐字符写入。源码里先把CenterText.getText()转成char[]然后循环Record.write(contentarr[i])。这种逐字符写入效率很低直接Record.write(content)就行。异常处理只有一个空的catch(IOException ex){}这意味着如果磁盘满了或者没有写权限用户看不到任何提示只会发现“保存成功”的弹窗没出现。更合理的写法是public void WriteRecord() { String content CenterText.getText(); try { File diaryDir new File(Diary); if (!diaryDir.exists()) { diaryDir.mkdir(); } File myfile new File(diaryDir, 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(), Error, JOptionPane.ERROR_MESSAGE); } }删除操作除了前面提到的跨平台问题还有一个隐患Runtime.exec执行cmd /c del时如果文件路径包含空格或特殊字符命令会解析失败。用File.delete()就没有这个问题。3.4 主函数与程序入口Main方法是整个程序的入口。源码里主函数应该是在最后调用的创建calendar实例并设置窗口属性。典型的写法是public static void main(String[] args) { // 获取当前日期作为初始显示 Calendar now Calendar.getInstance(); int year now.get(Calendar.YEAR); int month now.get(Calendar.MONTH) 1; // Calendar 月份从 0 开始 int day now.get(Calendar.DAY_OF_MONTH); calendar frame new calendar(year, month, day); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(700, 500); frame.setVisible(true); }注意calendar类的构造函数接收年、月、日三个参数并在构造函数里完成界面初始化和首次PrintMonth调用。这意味着窗口一打开就会显示当前月份的日历并且当前日期会被高亮。提示如果希望程序启动时自动加载今天的日记可以在构造函数末尾调用一次ReadRecord()但要注意此时key必须已经初始化。4. 避坑与常见问题日期计算、文件路径和事件冲突4.1 日期偏移算错导致星期对不上现象日历显示 1 号是星期三但实际应该是星期五整体错位两天。原因GetStartDay里的基准年和偏移量不匹配。源码用-32767作为起始年、-32768作为起始偏移这套值能跑通是因为作者可能手工验证过。但如果换一个基准年或者闰年判断在负数年份上出问题偏移就会错。解决直接用Calendar获取DAY_OF_WEEK或者用java.time.LocalDate的getDayOfWeek().getValue()。如果必须手算用 1900 年 1 月 1 日星期一作为基准偏移量设为 1这样更容易验证。4.2 文件路径反斜杠在非 Windows 系统上失效现象在 macOS 或 Linux 上运行保存日记时报FileNotFoundException或者日记文件出现在奇怪的位置。原因源码里硬编码了Diary\和cmd /c del这些是 Windows 专属写法。解决用File.separator或者直接写/。删除用File.delete()替代Runtime.exec。如果确实需要调用系统命令先判断System.getProperty(os.name)。4.3 点击空白日期格子抛出数字解析异常现象点击日历里没有日期的空白格子控制台报NumberFormatException程序可能卡住或弹窗报错。原因mouseClicked里直接Integer.parseInt(clickedField.getText())没有判断文本是否为空。解决解析前先trim()并判断isEmpty()空白格子直接return。同时给mouseClicked加try-catch兜底。4.4 月份下拉框和年份输入框状态不同步现象用下拉框选了 6 月年份输入框还是旧年份点击查询后显示的日历和预期不一致。原因下拉框的ActionListener和年份输入框的ActionListener各自独立更新状态没有统一的刷新入口。解决抽一个refreshCalendar()方法从YearText和MonthCombobox读取当前值统一更新year、month、SwitchMonth然后调用PrintMonth。所有触发点都调这个方法避免状态分散。4.5 保存后切换日期再切回来日记内容没有重新加载现象写完日记保存点到别的日期再点回来文本区还是之前的内容没有从文件重新读取。原因ReadRecord只在mouseClicked里调用但保存后没有清空文本区也没有强制重新读取。解决保存成功后可以选择清空文本区或者在mouseClicked里无条件调用ReadRecord让文件内容覆盖当前显示。如果文件不存在就清空文本区并提示“今天还没有日记”。5. 验收与答辩怎么证明这套万年历真的能跑5.1 功能验收清单答辩时老师不会只看界面通常会让你现场操作几个关键路径。下面这张表可以作为自检清单每项都跑一遍再上台。验收项操作步骤预期结果启动显示当前月运行程序日历显示当前年月当天日期高亮月份切换下拉框选 2 月再点下一月2 月天数正确闰年 29 天平年 28 天年份切换输入 2000 和 1900 分别查询2000 年 2 月 29 天1900 年 2 月 28 天日期点击点击任意日期格子右侧标签更新文本区加载对应日记或提示无日记保存日记输入文字点 Save弹出保存成功Diary目录下生成对应 txt 文件读取日记切换到其他日期再切回文本区显示之前保存的内容删除日记点 Delete确认删除文件被删除文本区清空提示删除成功回到今天点 Go to today日历跳回当前月当天高亮5.2 答辩时怎么讲清楚设计思路答辩陈述通常只有五到八分钟不要逐行念代码。我一般会按这个顺序讲先讲需求——为什么做万年历加记事本它解决了什么问题再讲总体结构——单类继承JFrame左右分栏JSplitPane分割然后讲核心算法——闰年判断、月份天数、起始星期计算接着讲数据落地——日记按日期命名存成 txt用FileWriter和FileReader读写最后讲事件驱动——ActionListener处理按钮MouseListener处理日期点击。如果老师问“为什么不用java.time”可以回答课程设计的时间点较早Calendar是当时的主流选择而且手动实现日期计算更能体现算法理解。如果问“单类结构有什么缺点”可以承认扩展性差但强调课程设计场景下便于提交和阅读。5.3 我踩过的一个坑当年我第一次做类似项目时把日期格子的MouseListener注册在了JPanel上而不是每个JTextField上结果点击任何位置都触发同一个事件根本分不清点的是哪一天。后来改成给每个格子单独注册监听器并在事件里用e.getSource()反查是哪个格子才解决了问题。从那以后我每次做网格类界面都强制走一遍“每个格子独立注册、事件里反查来源”的流程再也没翻过车。希望这份拆解能帮你把课程设计顺利交出去。源码里的日期算法和文件操作虽然朴素但把这几条链路跑通Java 桌面开发的基本功就算扎实了。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站