news 2026/11/2 19:45:06

单文件Swing万年历记事本:一个类实现日历与日记功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单文件Swing万年历记事本:一个类实现日历与日记功能

简介:这份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 月返回 31,4、6、9、11 月返回 30,2 月交给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 + "_" + day,SwitchMonth是英文月份缩写(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 doesn't exist, delete failured!"); } }

逻辑说明:File.delete()返回布尔值,成功失败都能判断,不依赖操作系统命令,也不怕路径带空格。参数还是key,行为对用户完全一致,只是底层实现换了。这类“用 exec 调 del”的写法在老课程设计里很常见,属于典型的踩坑点,后面避坑章节还会展开。

5. 跑起来与避坑:编译、图标路径和那几个血泪经验

5.1 从零编译运行

这份源码没有构建工具,纯javac+java就能跑。步骤不复杂,但顺序别乱:

# 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”从界面里剥出来跑一遍测试,再谈重构界面——不然改着改着就分不清是算法错了还是按钮没绑对。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 19:44:26

哈希集合解决最长连续序列问题的O(n)算法

1. 最长连续序列问题解析这道题在LeetCode上编号128&#xff0c;属于中等难度但实际面试中出现频率极高的题目。题目要求我们从一个未排序的整数数组中找到数字连续的最长序列长度&#xff0c;且要求算法时间复杂度为O(n)。乍看简单&#xff0c;但其中暗藏多个考察点。我第一次…

作者头像 李华
网站建设 2026/11/2 19:43:25

骁龙8超级至尊版:端侧智能体AI背后的芯片设计变革

说实话&#xff0c;以前每逢高通发布旗舰芯片&#xff0c;我盯着的无非是CPU单核涨了多少、GPU帧率提升了几分&#xff0c;但第六代骁龙8超级至尊版&#xff08;Snapdragon 8 Elite Gen 6&#xff09;发布之后&#xff0c;我脑子里留下最深的反而不是某个跑分数字&#xff0c;而…

作者头像 李华
网站建设 2026/11/2 19:39:34

Vue+WebUploader实现医疗大文件分片上传方案

1. 医疗系统中的病历图片上传挑战在医疗信息化系统中&#xff0c;病历图片上传是一个高频且关键的操作场景。一张普通的CT影像通常在20-50MB之间&#xff0c;而一套完整的检查资料包可能达到500MB以上。传统单文件上传方式在面对这种体量的数据时&#xff0c;往往会遇到以下典型…

作者头像 李华
网站建设 2026/11/2 19:38:52

WinNonlin 6.0 新手入门:药动学参数计算与生物等效性分析实操指南

简介&#xff1a;这份资源是面向药代动力学与生物等效性研究人员的 Phoenix WinNonlin 6.0 规范化操作文档&#xff0c;帮助分析人员按统一流程完成数据录入、NCA 参数计算与等效性检验&#xff0c;降低因操作不规范导致的结果偏差。压缩包内共 1 个 docx 文件&#xff0c;约 1…

作者头像 李华
网站建设 2026/11/2 19:34:09

cena 0.8.2本地评测实战:题包、时限与对拍避坑指南

简介&#xff1a;Cena评测软件0.8.2是一款面向C、C与Pascal语言学习者的代码评测工具&#xff0c;也可供编程教师批改作业、竞赛组织者进行小规模自动判题&#xff0c;帮助用户验证程序正确性、运行效率与编码规范。压缩包共四百四十六个文件&#xff0c;约十点六八兆字节&…

作者头像 李华
网站建设 2026/11/2 19:31:54

Android VK社交SDK接入实战:登录授权、Token管理与分享

1. 先把 VK 社交 SDK 的定位搞清楚&#xff0c;再动手接 做海外方向 Android 应用的同学大概都有过这种经历&#xff1a;产品要上某个区域的商店&#xff0c;运营提需求说“要支持本地主流社交账号登录&#xff0c;还要能把内容分享出去”。这时候打开需求文档一看&#xff0c;…

作者头像 李华