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

Java万年历源码剖析:Swing日历与记事本功能实现

Java万年历源码剖析:Swing日历与记事本功能实现 ★ FEATURED ARTICLE
简介这份JAVA万年历源码面向Java初学者与课程设计阶段的学生围绕日期时间处理、Swing图形界面与事件驱动编程展开可用于完成万年历记事本类项目或巩固Java基础。压缩包内共1个doc文件约317KB以课程设计报告形式呈现涵盖总体设计、类与方法说明、运行效果展示及完整源代码便于对照理解程序结构。资源已有185人学习说明其在同类课程设计参考中具备一定认可度。读者可从中获取Calendar与java.time日期计算、闰年判断、月份天数推算、JComboBox与JButton等组件布局、ActionListener与MouseListener事件响应以及基于java.io的日记读写与异常处理等实现思路并参考报告中的测试与调试过程快速搭建可运行的万年历记事本程序。1. 一个 JFrame 撑起的万年历记事本为什么值得拿来拆翻到这份 JAVA万年历源码 的时候我第一反应是单文件、单类、Swing 手写布局典型的课程设计产物。但真把它跑起来、点几下月份下拉框、在右侧文本框敲两行字再保存你会发现它把「日历计算 事件分发 本地文件读写」这三件事塞进了同一个类里反而成了一个很好的解剖样本。它适合两类人刚学完 Swing 想找个能跑的小项目练手的同学以及想快速搞懂「日期网格怎么排、按钮怎么联动、日记怎么落盘」的开发者。整份代码只有一个calendar.java没有 Maven、没有第三方依赖JDK 装好就能编译这种低门槛恰恰是它最大的价值——你能在一屏代码里看到完整的 GUI 生命周期。2. 架构与运行单类 Swing 程序是怎么搭起来的2.1 一个类扛下所有成员变量就是它的骨架这份源码最反直觉的地方在于它没有做任何分层。calendar类直接extends JFrame并implements ActionListener, MouseListener也就是说窗口、事件监听、业务逻辑全在一个类里。理解它的第一步不是看方法而是看成员变量因为变量名基本就是它的「模块划分」。从源码里能拎出几组关键字段year / month / day是当前选中的日期yearafterquery / monthafterquery是查询后暂存的目标年月startday存当月第一天是星期几SwitchMonth是月份的英文缩写字符串ischange / ischange_priornext两个布尔量用来标记「用户是否改过年月」剩下的一大堆JPanel / JLabel / JButton / JTextField[] / JTextArea就是界面组件。这种「变量即状态」的写法在工程里会被骂但读它的时候特别直观——你只要盯住year / month / day这三个值在哪个方法里被改就能追出整条交互链路。界面被切成左右两块用JSplitPane水平分割左边LeftPane是万年历右边RightPane是记事本。左边再分LeftNorth年份输入、月份下拉、Query 按钮、上一月/下一月和LeftCenter7×7 网格第一行是SUN到SAT的星期标题剩下 42 个JTextField装日期。右边分RightNorth显示当前年月日、RightCenterJTextArea日记区套了JScrollPane、RightSouthSave / Delete 两个按钮。这个布局结构建议你先在纸上画一遍后面看事件处理会顺很多。2.2 编译运行三步把它跑起来源码是 2012 年前后的写法用的是JFrameActionListener那套老 API好消息是它对 JDK 版本极其宽容JDK 8 到 JDK 17 都能编。我一般这样跑# 1. 建目录把 calendar.java 放进去 mkdir calendar-demo cd calendar-demo # 2. 编译源码里用了 ImageIcon 引外部图片先注释掉或补图见 2.3 javac -encoding UTF-8 calendar.java # 3. 运行主类名就是小写的 calendar java calendar这里有个血泪经验类名是小写的calendar和 Java 命名规范里「类名首字母大写」相反。javac不会拦你但java calendar必须和文件名、类名完全一致写成java Calendar会直接ClassNotFoundException。另外源码里new ImageIcon(../images/Handle.gif)这类相对路径引了外部图片仓库里没带图的话按钮图标会是空白但不影响功能想干净跑通最省事的做法是把这几处ImageIcon参数换成null或者自己丢两张 gif 到上级images目录。2.3 主函数与初始化顺序main方法很短本质就是new calendar(year, month, day)然后setVisible(true)。真正干活的是构造函数它先setTitle然后依次搭LeftPane、LeftCenter、RightPane中间穿插MonthCombobox的 1~12 填充、42 个日期格子的创建与addMouseListener(this)、以及最关键的一句PrintMonth(year, month, day)——在窗口显示之前就把当月网格画好。构造函数末尾把this.year / this.month / this.day赋值再getContentPane().add(split)。public calendar(int year, int month, int day) { setTitle(My CalendarTextbook); // 左面板北边查询区 中间日期网格 南边回今天按钮 LeftPane new JPanel(); // ... 省略组件搭建 ... for (int i 1; i 12; i) { MonthCombobox.addItem(new Integer(i)); // 月份下拉 1~12 } for (int i 0; i 42; i) { ShowDays[i] new JTextField(); ShowDays[i].addMouseListener(this); // 每个日期格子都能点 ShowDays[i].setEditable(false); LeftCenter.add(ShowDays[i]); } PrintMonth(year, month, day); // 先画网格再显示窗口 this.year year; this.month month; this.day day; }参数说明year / month / day是启动时的默认日期一般传Calendar.getInstance()拿到的今天。注意PrintMonth在this.year赋值之前调用靠的是方法参数而不是成员变量这个顺序别调换否则网格会画错月份。ShowDays固定 42 格是因为一个月最多跨 6 周7×642这是日历网格的通用做法。3. 核心算法拆解日期网格到底怎么算出来的3.1 闰年与月天数两个小方法定生死整个日历的正确性压在三个方法上IsLeapYear、GetNumOfDaysInMonth、GetStartDay。前两个是教科书级实现但源码里有个细节值得说——GetNumOfDaysInMonth用了一长串if判断 31 天和 30 天的月份2 月单独走闰年分支最后return 0兜底。这种写法可读性一般但逻辑没错。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; // 非法月份兜底 }参数说明month用的是 1~12 的自然月不是Calendar里 0~11 的索引这一点在跨方法传参时最容易翻车。IsLeapYear的判定顺序也有讲究先判 400 再判 4 和 100能避免 1900 这种「能被 4 整除但不是闰年」的经典坑。3.2 当月第一天是星期几GetStartDay 的取模逻辑这是整份代码里最「玄学」的一段。GetStartDay先调GetTotalNumOfDays算出从某个基准年到目标月的总天数再对一个基准偏移量取模 7。源码里写的是startday0001 -32768然后(totalnumofdays startday0001) % 7。这个-32768是作者为了对齐星期几硬凑的偏移量属于「能跑但说不清」的典型。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从-32767年开始累加这个负年份起点纯粹是为了让偏移量凑成整数实际业务里没人关心公元前。% 7的结果直接当作ShowDays数组的起始下标用——0 对应周日6 对应周六正好和week[]数组{SUN,...,SAT}的顺序对上。如果你要改造成现代写法我一般会换成java.time.LocalDate.of(year, month, 1).getDayOfWeek().getValue() % 7一行顶掉这两个方法还不用背那个-32768。3.3 PrintMonthBody42 个格子的填充与高亮网格填充的核心是PrintMonthBody(startday, dayinmonth, day)。它从startday开始把 1 到dayinmonth依次塞进ShowDays[i]遇到n day就把这一格字体放大、前景变绿、背景变深灰做出「今天高亮」的效果startday之前的格子和月末之后的格子统一清空并刷深灰背景。public void PrintMonthBody(int startday, int dayinmonth, int day) { 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)); ShowDays[i].setBackground(Color.DARK_GRAY); } else { ShowDays[i].setFont(new Font(TimesRoman, Font.BOLD, 12)); ShowDays[i].setForeground(Color.white); ShowDays[i].setBackground(Color.DARK_GRAY); } n; } for (int i 0; i startday; i) { ShowDays[i].setText(); } // 月初留白 for (int i startday dayinmonth; i 42; i) { ShowDays[i].setText(); } // 月末留白 }参数说明startday是当月 1 号落在网格里的下标0~6dayinmonth是当月天数day是要高亮的那一天。源码里有两个重载版本一个带day一个不带不带day的用于翻月时不需要高亮今天的场景。这里有个小坑ShowDays是JTextField而不是JLabel虽然设了setEditable(false)但用户仍能选中文字视觉上会有点怪改成JLabel或JButton会更干净。4. 交互与持久化按钮、鼠标和日记文件怎么串起来4.1 事件分发一个 actionPerformed 管所有按钮因为类实现了ActionListener所有按钮都addActionListener(this)最终全涌进actionPerformed(ActionEvent e)。源码里靠e.getActionCommand()区分来源PriorMonth和NextMonth在构造时被setActionCommand(prior)和nextSave / Delete 也各自设了命令字符串Query 和 ToToday 则靠对象引用比较或命令判断。public void actionPerformed(ActionEvent e) { String cmd e.getActionCommand(); if (prior.equals(cmd)) { // 上一月 if (month 1) { month 12; year--; } else { month--; } SwitchMonth(month); PrintMonth(year, month, day); } else if (next.equals(cmd)) { // 下一月 if (month 12) { month 1; year; } else { month; } SwitchMonth(month); PrintMonth(year, month, day); } else if (Save.equals(cmd)) { WriteRecord(); } else if (Delete.equals(cmd)) { DeleteFile(); } // Query / ToToday 分支略 }逻辑说明翻月时先处理「1 月往前是去年 12 月、12 月往后是明年 1 月」的边界再调SwitchMonth更新英文缩写最后重画网格。SwitchMonth本身是个 12 分支的switch把 1~12 映射成Jan~Dec注意源码里 10 月写成了Qct这是个笔误显示出来是 Qct 不是 Oct二次开发时顺手改掉。4.2 鼠标选日期mouseClicked 里换 key日期格子是JTextField且注册了MouseListener点某一天会触发mouseClicked。源码在这里做的事是从被点的格子里读出文本也就是日号更新day刷新右上角NorthMonthDayYear标签并重新拼出key year _ SwitchMonth _ day。这个key是后面文件读写的唯一凭据格式类似2024_Jun_15。public void mouseClicked(MouseEvent e) { JTextField src (JTextField) e.getSource(); String text src.getText(); if (text ! null !text.isEmpty()) { day Integer.parseInt(text); // 更新选中日 key year _ SwitchMonth _ day; // 重拼文件 key NorthMonthDayYear.setText( year , SwitchMonth , day ); ReadRecord(); // 若当天有日记询问是否读取 } }参数说明e.getSource()拿到的是被点的那个JTextField强转后取文本。这里必须判空因为月初月末的留白格子文本是空串直接parseInt会抛NumberFormatException。ReadRecord在点击时就被调用意味着你每点一天都会弹一次「今天有日志是否读取」的确认框交互上略烦但逻辑是通的。4.3 日记落盘WriteRecord / ReadRecord / DeleteFile持久化没有用数据库就是纯文件。WriteRecord先new File(Diary).mkdir()建目录再用FileWriter往Diary/key.txt写CenterText的内容写完弹「Save success!」。ReadRecord用FileReader逐字符读回读之前先showConfirmDialog问用户要不要读。DeleteFile更粗暴直接Runtime.getRuntime().exec(cmd /c del filepath)调系统命令删文件。public void WriteRecord() { String content CenterText.getText(); try { File diaryDir new File(Diary); diaryDir.mkdir(); // 目录不存在就建 File myfile new File(Diary/ key .txt); // key 形如 2024_Jun_15 FileWriter record new FileWriter(myfile); record.write(content); record.close(); JOptionPane.showMessageDialog(this, Save success!); } catch (IOException ex) { ex.printStackTrace(); // 源码里是空 catch建议补日志 } }参数说明key决定了文件归属哪一天所以点日期、翻月之后key必须同步更新否则会写串文件。DeleteFile里的cmd /c del是 Windows 专用换到 Linux/macOS 直接失效这是这份代码跨平台性最差的一处二次开发时我一般改成myfile.delete()一行搞定还不用起子进程。另外源码的catch块基本是空的文件写失败你完全看不到提示调试时建议至少ex.printStackTrace()。5. 避坑与排查跑这份老代码最容易翻车的五件事5.1 类名小写导致 ClassNotFound现象javac编译通过java calendar却报Could not find or load main class。原因类名是calendar有人习惯性敲java Calendar大小写不匹配。解决严格用java calendar或者把类名改成CalendarApp并同步改文件名后者更符合规范。5.2 ImageIcon 相对路径找不到图现象程序能起但上一月/下一月/回今天按钮是空白。原因new ImageIcon(../images/prior.gif)依赖上级目录的images文件夹源码包里没带图。解决把这几处换成new ImageIcon()或直接new JButton(上一月)用文字功能不受影响。5.3 空格子点击抛 NumberFormatException现象点月初或月末的空白日期格控制台刷异常。原因mouseClicked里对空文本直接Integer.parseInt。解决解析前加if (text null || text.trim().isEmpty()) return;这也是 4.2 里强调判空的原因。5.4 删除日记在非 Windows 上失效现象点 Delete 弹了确认框但文件还在。原因Runtime.exec(cmd /c del ...)只在 Windows 有效。解决改成new File(Diary/ key .txt).delete()跨平台且不用处理子进程。5.5 翻月后 key 没更新导致日记写串现象翻到 7 月保存日记结果写进了 6 月的文件。原因key只在mouseClicked里重拼翻月按钮的actionPerformed分支没同步更新key。解决在翻月逻辑末尾补一句key year _ SwitchMonth _ day;保证任何改变年月日的路径都刷新 key。6. 二次开发把它从课程作业改成能用的桌面小工具真要把这份源码用起来我建议按「先修 bug、再换算法、最后拆结构」三步走。第一步是第 5 章那五个坑尤其 5.4 和 5.5不修的话数据会丢、文件会串属于必须处理的正确性问题。第二步是把日期计算整体换成java.timeGetStartDay和GetTotalNumOfDays两个方法可以直接删掉用LocalDate替代import java.time.LocalDate; import java.time.DayOfWeek; // 当月 1 号是星期几0周日对齐 week[] 数组 int startday LocalDate.of(year, month, 1) .getDayOfWeek() .getValue() % 7; // 当月天数 int dayinmonth LocalDate.of(year, month, 1).lengthOfMonth();这两行替换掉原来那一坨负年份累加闰年判断也一并省了IsLeapYear可以保留但不再被调用。第三步是拆结构把calendar这个巨类按职责切成CalendarPanel只管网格渲染、DiaryService只管文件读写、MainFrame只管组装和事件转发三个类。拆的时候有个技巧——先别动 UI 代码只把WriteRecord / ReadRecord / DeleteFile抽到一个DiaryService里构造时传入Diary目录路径这样文件逻辑立刻可单测UI 那边只留一行diaryService.save(key, text)。改造点原实现建议替换收益星期计算负年份累加 偏移取模LocalDate.getDayOfWeek()去掉玄学偏移可读性拉满文件删除Runtime.exec(cmd /c del)File.delete()跨平台无子进程类结构单类 800 行拆 Panel / Service / Frame可测试、可复用异常处理空 catch至少打印堆栈 弹窗提示出错不再当黑匣子验证方法也简单改完之后拿几个边界日期跑一遍——闰年 2 月 29 日、跨年 12 月 31 日翻到 1 月 1 日、1900 年 2 月不是闰年、2000 年 2 月是闰年看网格首格位置和天数对不对再点几天分别存日记、删日记确认文件落在Diary/下且文件名和选中日期一致。这套回归清单我每次改日期类代码都会走一遍比肉眼盯屏幕靠谱得多。说个我自己的教训早年改这类单文件项目我总想一口气重构成 MVC结果 UI 和逻辑一起动改到一半连原来能跑的功能都跑不起来了连后悔药都没得吃。从那以后我每次动这种老代码都强制先跑通原版、记下基线行为再一小步一小步替换每步都编译运行验证一次。这份万年历源码不大正好适合拿来练这个习惯——先让它跑起来再让它算对最后让它好改。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站