
简介这份JAVA万年历源码面向Java初学者与课程设计需求者以单文件calendar类实现万年历与记事本二合一桌面应用帮助读者在真实项目中理解日期时间处理、Swing界面设计与事件驱动编程。压缩包内共1个doc文件约317KB为完整的Java课程设计报告涵盖总体设计、类与方法说明、源代码及运行效果截图可对照文档梳理Calendar与java.time的用法、闰年判断、月份起始天与总天数计算等核心逻辑。项目还涉及JComboBox月份选择、JButton动作监听、JTextArea日记输入以及基于java.io的文件读写与try-catch异常处理界面采用多种布局管理器组合结构清晰。目前已有185人学习适合需要参考完整课设文档与可运行源码、快速完成类似项目的读者。1. 单文件 Swing 万年历记事本一个类撑起日历与日记翻到这份calendar.java的时候我愣了一下——整个项目就一个源文件、一个类既画日历又管日记还顺手把闰年判断、月份切换、文件读写全塞进去了。它解决的是很具体的一类需求桌面上放一个小工具左边翻日历看日子右边给某一天写点东西点保存就落盘成 txt。适合谁刚学完 Java 语法、想找一个能完整跑起来、又能看懂每一行的 Swing 练手项目的同学以及需要给课程设计交一份“有界面、有交互、有持久化”作业的开发者。它不依赖任何第三方库JDK 自带 AWT、Swing、IO 就够这也是它作为 JAVA 万年历源码最讨喜的地方拿到就能编译改起来没有黑盒。2. 架构与目录一个类如何拆出日历面板和记事本面板2.1 整体结构JFrame JSplitPane 的左右分栏这个项目的“架构”谈不上分层但布局思路是清晰的。主类calendar直接继承JFrame并实现ActionListener、MouseListener两个接口窗口内容用一个JSplitPane水平切开左边是日历区LeftPane右边是记事本区RightPane。日历区再往下拆成北、中两块北边放年份输入框、月份下拉框、查询按钮和前后月切换按钮中间用GridLayout(7,7)铺出 7 列星期标题加 42 个日期格子。记事本区则是北边一个日期标题标签、中间一个带滚动条的JTextArea、南边保存和删除两个按钮。这种“一个类全包”的写法在课程设计里很常见好处是逻辑集中、不用来回跳文件坏处是成员变量多到一张表都列不完。原文的变量表里能看到YearLabel、MonthCombobox、ShowDays[]、CenterText、SouthSave这些基本就是按界面区域命名的读代码时对着界面找变量很快。2.2 目录与文件构成项目本身极简落到磁盘上大概是这样calendar/ ├── calendar.java # 唯一源文件含主类与 main 方法 ├── images/ # 按钮图标资源prior.gif / next.gif / Handle.gif │ ├── prior.gif │ ├── next.gif │ └── Handle.gif └── Diary/ # 运行时自动创建的日记目录 └── 2012_Jun_25.txt # 按“年_月_日”命名的日记文件Diary目录不是预先放好的而是WriteRecord()里用Diary.mkdir()在第一次保存时创建。日记文件名由成员变量key拼成格式是年_月英文缩写_日比如2012_Jun_25.txt。这个命名规则很关键后面读、删都靠它定位文件。提示images目录里的图标是可选资源源码里用new ImageIcon(../images/prior.gif)这种相对路径引用。如果图标缺失按钮会退化成无图状态程序仍能跑只是不好看。第一次运行前把图标补上或者把这几处ImageIcon换成纯文字按钮能省掉一堆路径报错。2.3 事件分发一个 actionPerformed 管所有按钮所有按钮的点击最终都汇到actionPerformed(ActionEvent e)里靠e.getActionCommand()或e.getSource()区分。源码里给PriorMonth、NextMonth、SouthSave、SouthDelete都显式设了setActionCommand这是好习惯——比用getSource() someButton更稳也更好读。下面这段是典型的分发骨架我按原逻辑整理过public void actionPerformed(ActionEvent e) { String cmd e.getActionCommand(); if (prior.equals(cmd)) { // 上一月 changemonthmessage--; if (changemonthmessage 1) { // 跨年回退 changemonthmessage 12; changeyearmessage--; } refreshCalendar(changeyearmessage, changemonthmessage); } else if (next.equals(cmd)) { // 下一月 changemonthmessage; if (changemonthmessage 12) { // 跨年进位 changemonthmessage 1; changeyearmessage; } refreshCalendar(changeyearmessage, changemonthmessage); } else if (Save.equals(cmd)) { WriteRecord(); // 写日记 } else if (Delete.equals(cmd)) { DeleteFile(); // 删日记 } else if (e.getSource() Query) { AboutActionListenerWay(); // 按输入年份下拉月份查询 } }逻辑说明changemonthmessage和changeyearmessage是“当前正在显示的”年月和构造时传入的year、month分开存避免翻月把初始值改乱。跨年判断放在翻月里做 1退到 12 月并年份减一 12进到 1 月并年份加一这是万年历翻页最常见的边界处理。参数上refreshCalendar是我抽出来的刷新方法实际源码里是直接调PrintMonth(year, month)再更新ShowDate标签效果一样。3. 核心算法拆解闰年、起始星期与 42 格填充3.1 闰年判断与每月天数日历能不能算对第一道关就是闰年。源码里的IsLeapYear写得很标准public boolean IsLeapYear(int year) { // 能被400整除或能被4整除但不能被100整除 if ((year % 400 0) || (year % 4 0 year % 100 ! 0)) return true; else return false; }GetNumOfDaysInMonth则按月份返回天数1、3、5、7、8、10、12 月返回 314、6、9、11 月返回 302 月交给IsLeapYear决定 28 还是 29。这两个方法被GetTotalNumOfDays反复调用是整个日期计算的底座。参数就是普通的int year, int month没有做越界校验——传 13 月会返回 0实际调用链里月份都来自下拉框 1~12所以不会出问题但二次开发时如果从外部传参记得自己兜一下。3.2 起始星期从公元 1 年累加天数再取模GetStartDay是这个项目里最“手工”的一段。它没有用java.util.Calendar现成的get(Calendar.DAY_OF_WEEK)而是自己从公元 1 年一路累加到目标年月再对 7 取模public int GetStartDay(int year, int month) { int startday0001 -32768; // 基准偏移量 long totalnumofdays GetTotalNumOfDays(year, month); return (int) ((totalnumofdays startday0001) % 7); } public long GetTotalNumOfDays(int year, int month) { long total 0; for (int i -32767; i year; i) { // 逐年累加 total IsLeapYear(i) ? 366 : 365; } for (int i 1; i month; i) { // 再加当年已过月份 total GetNumOfDaysInMonth(year, i); } return total; }逻辑说明GetTotalNumOfDays返回的是“从基准年到目标月之前的总天数”GetStartDay加上偏移量-32768后对 7 取模得到该月 1 号是星期几0 表示周日。这个-32768是作者调出来的经验值用来把结果对齐到week[] {SUN,MON,...}的下标。参数上year支持负数年份循环从 -32767 起所以理论上能算公元前但界面输入框只接受正整数实际用不到。注意这段循环在年份很大时会明显变慢因为每年都要判断一次闰年。日常用 1900~2100 年完全无感但如果要做“跳到 5000 年”这类功能建议换成java.time.LocalDate.of(year, month, 1).getDayOfWeek().getValue()一行顶几十行还不用维护那个魔数偏移。3.3 42 格填充PrintMonthBody 的边界处理日历面板固定 42 个格子6 行 × 7 列PrintMonthBody负责把 1 号到月末填进去并把前后空白格清空public void PrintMonthBody(int startday, int dayinmonth, int day) { // 填本月日期startday 是 1 号所在列 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].setForeground(Color.white); ShowDays[i].setFont(new Font(TimesRoman, Font.BOLD, 12)); } ShowDays[i].setBackground(Color.DARK_GRAY); n; } // 清空 1 号之前的格子 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决定 1 号从第几列开始dayinmonth决定填多少天day是“今天”用来做绿色高亮。三个参数缺一不可源码里还保留了一个不带day的重载版本用于翻月时不需要高亮今天的场景。这里有个容易忽略的点ShowDays是JTextField数组而不是JLabel作者用文本框是为了让格子有边框感同时setEditable(false)禁止输入再挂MouseListener实现点击选日期。用文本框当格子样式上确实比标签更像日历代价是每个格子都带光标焦点逻辑得靠setEditable(false)压住。4. 记事本持久化按日期命名文件的读写删4.1 保存WriteRecord 与 key 的生成日记的落盘逻辑不复杂核心是key这个文件名。构造时key year _ SwitchMonth _ daySwitchMonth是英文月份缩写Jan、Feb……所以文件名形如2012_Jun_25.txt。保存时先建目录再写文件public void WriteRecord() { String content CenterText.getText(); try { File diary new File(Diary); diary.mkdir(); // 目录已存在时返回 false无害 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) { ex.printStackTrace(); // 原码吞异常建议至少打日志 } }逻辑说明FileWriter默认是覆盖模式同一天重复保存会直接盖掉旧内容不会追加——这符合“一天一篇日记”的语义。参数上key在切换日期时必须同步更新否则会把今天的日记写到昨天的文件里。源码里key的更新散落在鼠标点击和查询逻辑中二次开发时建议抽一个updateKey(year, month, day)统一维护减少漏改。4.2 读取ReadRecord 的存在性判断顺序有坑ReadRecord的本意是“切到某天时如果那天有日记就问要不要读出来”。但源码里new FileReader(myfile)写在if (myfile.exists())之前文件不存在时FileReader构造直接抛FileNotFoundException被外层catch接住后把文本区设成 “Today has not logs.”。功能上碰巧能跑但异常被当成正常分支用调试时容易误判。我一般会改成先判断再打开public void ReadRecord() { File myfile new File(Diary\\ key .txt); if (!myfile.exists()) { CenterText.setText(Today has not logs.); return; // 先判存在避免异常当流程 } int n JOptionPane.showConfirmDialog(this, Today has logs, are you read?, Confirm, JOptionPane.YES_NO_CANCEL_OPTION); if (n ! JOptionPane.YES_OPTION) return; try (FileReader record new FileReader(myfile)) { StringBuilder content new StringBuilder(); int b; while ((b record.read()) ! -1) { content.append((char) b); } CenterText.setText(content.toString()); } catch (IOException ex) { CenterText.setText(Read failed: ex.getMessage()); } }逻辑说明把“文件不存在”从异常路径改成正常返回读失败才进catch语义更清楚。try-with-resources保证FileReader一定关闭比手动close()稳。参数上没变还是靠key定位文件改动只在控制流。4.3 删除DeleteFile 的跨平台问题删除逻辑用Runtime.exec(cmd /c del filepath)调系统命令这在 Windows 上能跑换到 Linux 或 macOS 直接失效而且路径里有空格时命令会被截断。更稳的做法是用File.delete()public void DeleteFile() { File myfile new File(Diary\\ key .txt); int n JOptionPane.showConfirmDialog(this, Are you sure delete the file?, Confirm, JOptionPane.YES_NO_CANCEL_OPTION); if (n ! JOptionPane.YES_OPTION) return; if (myfile.exists() myfile.delete()) { // 跨平台无需拼命令 JOptionPane.showMessageDialog(this, Delete successed!); CenterText.setText(Today has not logs.); } else { JOptionPane.showMessageDialog(this, The file doesnt exist, delete failured!); } }逻辑说明File.delete()返回布尔值成功失败都能判断不依赖操作系统命令也不怕路径带空格。参数还是key行为对用户完全一致只是底层实现换了。这类“用 exec 调 del”的写法在老课程设计里很常见属于典型的踩坑点后面避坑章节还会展开。5. 跑起来与避坑编译、图标路径和那几个血泪经验5.1 从零编译运行这份源码没有构建工具纯javacjava就能跑。步骤不复杂但顺序别乱# 1. 进入源码所在目录 cd calendar # 2. 编译指定 UTF-8 防止中文注释乱码报错 javac -encoding UTF-8 calendar.java # 3. 运行类名是小写 calendar和文件名一致 java calendar如果main方法里没有直接new calendar(...)需要自己补一个入口。原文的Main方法描述里提到“唯一的主函数”常见写法是取当前日期作为初始值public static void main(String[] args) { Calendar now Calendar.getInstance(); int y now.get(Calendar.YEAR); int m now.get(Calendar.MONTH) 1; // Calendar 月份从 0 开始 int d now.get(Calendar.DAY_OF_MONTH); new calendar(y, m, d).setVisible(true); }逻辑说明Calendar.MONTH是 0~11必须加 1 才是人类月份这是 Java 日期 API 最经典的坑之一。setVisible(true)别忘否则窗口不显示程序看起来像卡住。5.2 避坑与常见问题排查现象一编译报“找不到符号 ImageIcon”或运行后按钮空白。原因images目录没放在源码同级或相对路径../images/prior.gif解析不到。解决把images目录放到运行目录的上一级或者干脆把三处new ImageIcon(...)改成new JButton(上一月)这类文字按钮先保证功能跑通再补图标。现象二中文注释导致编译失败报 illegal character。原因源文件是 GBK 编码javac默认按平台编码读。解决编译时加-encoding UTF-8或-encoding GBK看文件实际编码二者要匹配别一个 UTF-8 一个 GBK 混着来。现象三点保存提示成功但找不到日记文件。原因Diary目录是相对当前工作目录创建的从 IDE 里跑和从命令行跑工作目录不同文件落在不同地方。解决统一在源码目录下用命令行启动或者把new File(Diary)改成绝对路径/用户目录路径比如System.getProperty(user.home) /Diary。现象四删除日记在非 Windows 系统上无效。原因Runtime.exec(cmd /c del ...)是 Windows 专用命令。解决换成File.delete()见 4.3 的改法一行搞定跨平台。现象五翻月后写日记内容串到别的日期。原因key没跟着当前显示的年月日更新仍指向旧日期。解决在翻月、查询、点击日期格子的每个入口都调一次统一的updateKey把key year _ SwitchMonth _ day收口到一处别散着写。提示这几个坑里编码和key同步是最容易反复翻车的。我第一次跑这类单文件 Swing 项目时就是中文注释没加-encoding卡了半天后来养成习惯——拿到任何老 Java 源码先javac -encoding UTF-8试一遍再谈功能。6. 二次开发把单文件拆开、把日期计算换成 java.time真要把这份源码往“能继续维护”的方向改我一般按三步走每步都能独立验证不至于一次改崩。第一步拆类。现在calendar一个类同时管界面、算法、IO改任何一处都得通读全文。可以拆成CalendarPanel日历网格与翻月、NotePanel文本区与保存删除、DateUtil闰年、起始星期、月天数三个类主类只负责组装JSplitPane和转发事件。拆的时候先抽DateUtil因为它没有界面依赖抽出来立刻能写单元测试验证。第二步换日期引擎。把GetStartDay、GetTotalNumOfDays这套手写累加换成java.time代码量和出错面都小一个量级import java.time.LocalDate; import java.time.YearMonth; public class DateUtil { // 某月 1 号是星期几周一1 ... 周日7 public static int startDayOfWeek(int year, int month) { return LocalDate.of(year, month, 1).getDayOfWeek().getValue(); } // 某月天数自动处理闰年 public static int daysInMonth(int year, int month) { return YearMonth.of(year, month).lengthOfMonth(); } }逻辑说明getDayOfWeek().getValue()返回 1~7周一至周日和原来 0~6周日起的约定不同填充 42 格时要做一次(value % 7)的换算或者把星期标题数组改成周一起头。YearMonth.lengthOfMonth()直接给出当月天数闰年判断内置IsLeapYear可以整个删掉。参数就是普通的年月越界会抛DateTimeException比返回 0 更早暴露问题。第三步改存储。把Diary\\key.txt这种 Windows 反斜杠路径换成Paths.get(Diary, key .txt)删除用Files.deleteIfExists读写用Files.readString/Files.writeString顺带把编码固定成 UTF-8避免换机器后中文日记变乱码。改完拿几天数据回归一遍保存、切日期、读回、删除四个动作都过一遍确认文件名和内容对得上。改造点原实现建议实现验证方式日期计算手写逐年累加 魔数偏移java.time.LocalDate/YearMonth对比 1900、2000、2024 年 2 月起始星期文件路径Diary\\ keyPaths.get(Diary, key .txt)在 Windows 与 Linux 各跑一次删除Runtime.exec(cmd /c del)Files.deleteIfExists删除后确认文件消失且无异常编码平台默认读写显式 UTF-8写中文日记再读回比对这套改法不追求一步到位先抽DateUtil加测试再换存储最后动界面拆分每步都能单独回滚。从那以后我每次接手这种单文件老项目都强制先把“算法”和“IO”从界面里剥出来跑一遍测试再谈重构界面——不然改着改着就分不清是算法错了还是按钮没绑对。希望帮到你。本文还有配套的精品资源点击获取